PHP Cookies
Cada pedido que chega ao PHP é um recomeço: o programa roda, responde e esquece tudo. Se a Ana escolhe o tema escuro no site da padaria, como o site vai lembrar disso na próxima página? Uma das respostas é o cookie: um pequeno texto que o servidor pede ao navegador para guardar e devolver em todos os pedidos seguintes. Nesta aula você cria, lê e apaga cookies, entende cada opção do setcookie e conhece o erro mais famoso dessa área: headers already sent.
- O que é um cookie e o caminho que ele faz (servidor → navegador → servidor).
- Como criar um cookie com
setcookiee definir quanto tempo ele dura. - Como ler cookies em
$_COOKIE, e por que o cookie novo só aparece no pedido seguinte. - As opções
path,domain,secure,httponlyesamesite, com o array de opções. - Como apagar um cookie.
- O erro
Cannot modify header information - headers already sent: o que é e como evitar. - Limites de tamanho, o que nunca guardar num cookie e o lado da privacidade.
A ideia: a comanda do restaurante
Num restaurante movimentado, o garçom não decora o que cada mesa pediu. Na primeira visita, ele deixa uma comanda na mesa com um número. Toda vez que você chama, ele olha a comanda e sabe quem você é.
O cookie é essa comanda: quem escreve é o servidor, quem guarda é o navegador, e o navegador a mostra, sozinho, em todo pedido seguinte para o mesmo site.
NAVEGADOR SERVIDOR (PHP)
1. GET /tema.php ─────────────────────────────▶ setcookie('tema', 'escuro')
◀───────────────────────────── Set-Cookie: tema=escuro; …
guarda: tema=escuro (o PHP esquece tudo)
2. GET /ler.php
Cookie: tema=escuro ────────────────────────▶ $_COOKIE['tema'] → 'escuro'
◀───────────────────────────── página com o tema escuro
3. (todo pedido seguinte ao mesmo site leva "Cookie: tema=escuro", até expirar)
Set-Cookie na resposta (o servidor pede para guardar) e Cookie no pedido (o navegador devolve).Dois detalhes desse desenho explicam quase tudo o que vem a seguir:
- O cookie vai num cabeçalho, que viaja antes do conteúdo da página. Por isso existe o erro headers already sent.
- O cookie criado no passo 1 só volta no passo 2. No mesmo pedido em que você o cria, ele ainda não está em
$_COOKIE.
Criar um cookie: setcookie
<?php
setcookie('tema', 'escuro', time() + 60 * 60 * 24 * 30);
echo "Cookie enviado.\n";
echo "O que chegou neste pedido: ";
var_dump($_COOKIE);
- setcookie(
- "Definir cookie": pede ao navegador para guardar um cookie.
- 'tema',
- O nome do cookie (será a chave em
$_COOKIE). - 'escuro',
- O valor. Sempre texto.
- time() + 60 * 60 * 24 * 30
- Até quando ele vale (a expiração).
time()é o "agora", em segundos (veja Datas e Horas); somamos 60 segundos × 60 minutos × 24 horas × 30 dias. Sem esse argumento (ou com0), é um cookie de sessão: some quando o navegador é fechado.
O -i do curl mostra os cabeçalhos da resposta. Olhe a linha Set-Cookie, e o $_COOKIE vazio:
$ curl -si localhost:8000/tema.php
HTTP/1.1 200 OK
Host: localhost:8000
Date: Sat, 17 Oct 2026 00:05:00 GMT
Connection: close
X-Powered-By: PHP/8.5.8
Set-Cookie: tema=escuro; expires=Mon, 16 Nov 2026 00:05:00 GMT; Max-Age=2592000
Content-type: text/html; charset=UTF-8
Cookie enviado.
O que chegou neste pedido: array(0) {
}
O PHP traduziu o time() + … em duas formas: expires (a data, no fuso de Greenwich, por isso 00:05 e não 21:05) e Max-Age=2592000 (os segundos que faltam: 30 dias). E o $_COOKIE está vazio, porque o navegador ainda nem recebeu o cookie.
O pedido seguinte
Para o curl se comportar como navegador e guardar os cookies, usamos um "pote de cookies": -c pote grava os cookies recebidos num arquivo, -b pote envia os cookies do arquivo.
$ curl -s -c pote localhost:8000/tema.php > /dev/null
$ cat pote
# Netscape HTTP Cookie File
# https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.
localhost FALSE / FALSE 1794787500 tema escuro
$ curl -s -b pote localhost:8000/tema.php
Cookie enviado.
O que chegou neste pedido: array(1) {
["tema"]=>
string(6) "escuro" « agora sim: veio no cabeçalho Cookie do pedido
}
O arquivo pote é parecido com o que o navegador guarda: o site (localhost), o caminho (/), a expiração (em segundos desde 1970), o nome e o valor.
Ler um cookie: $_COOKIE
$_COOKIE é uma superglobal como $_GET (veja Superglobais): um array com os cookies que o navegador mandou neste pedido. Leia sempre com um valor padrão, porque o cookie pode não existir (primeira visita, cookie expirado, pessoa que apagou os cookies):
<?php $tema = $_COOKIE['tema'] ?? 'claro'; echo "Tema: $tema\n";
$ curl -s -b pote localhost:8000/ler.php Tema: escuro
curl, -b "nome=valor"). Trate $_COOKIE como $_GET: valide e escape. Nunca guarde num cookie algo como usuario=ana ou admin=1 para controlar acesso: basta a pessoa trocar o valor. Para isso existem as Sessões.Exemplo: contador de visitas
<?php
$visitas = (int) ($_COOKIE['visitas'] ?? 0) + 1;
setcookie('visitas', (string) $visitas, time() + 60 * 60 * 24 * 365);
echo "Esta é a sua visita número $visitas.\n";
$ curl -s -b potev -c potev localhost:8000/visitas.php Esta é a sua visita número 1. $ curl -s -b potev -c potev localhost:8000/visitas.php Esta é a sua visita número 2. $ curl -s -b potev -c potev localhost:8000/visitas.php Esta é a sua visita número 3. $ curl -s -b "visitas=99" localhost:8000/visitas.php Esta é a sua visita número 100. « o visitante escolheu o valor do cookie
- $_COOKIE['visitas'] ?? 0
- O número que o navegador devolveu, ou 0 na primeira visita.
- (int) ( … ) + 1
- Converte para inteiro (o cookie é texto; um valor estranho como
"abc"vira 0) e soma a visita atual. - setcookie('visitas', (string) $visitas, …)
- Grava o número novo, que vai voltar no próximo pedido. O valor precisa ser texto, daí o
(string).
A última linha mostra o aviso acima na prática: com -b "visitas=99", o "visitante" inventou o cookie. Para um contador de brincadeira, tudo bem. Para qualquer coisa que importe, não.
O contador de visitas. Rode e clique em Visitar de novo várias vezes na aba Resultado: o editor guarda os cookies entre um pedido e outro desta caixa, como um navegador. Depois clique em Esquecer, que apaga o cookie (veja como na próxima seção).
<?php
if (isset($_GET['esquecer'])) {
setcookie('visitas', '', time() - 3600);
echo "<p>Cookie apagado. Na próxima visita, a contagem recomeça.</p>";
} else {
$visitas = (int) ($_COOKIE['visitas'] ?? 0) + 1;
setcookie('visitas', (string) $visitas, time() + 60 * 60 * 24 * 365);
echo "<p>Esta é a sua visita número $visitas.</p>";
}
?>
<form><button>Visitar de novo</button></form>
<form><button name="esquecer" value="1">Esquecer</button></form>
<p>$_COOKIE neste pedido:</p>
<pre><?php print_r($_COOKIE); ?></pre>
As opções: o array de opções
O setcookie aceita mais configurações além de nome, valor e expiração. A forma moderna (desde o PHP 7.3) é passar um array de opções no terceiro argumento:
<?php
setcookie('tema', 'escuro', [
'expires' => time() + 60 * 60 * 24 * 30,
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax',
]);
setcookie('aviso', 'Ana Souza');
echo "ok\n";
$ curl -si localhost:8000/opcoes.php HTTP/1.1 200 OK Host: localhost:8000 Date: Sat, 17 Oct 2026 00:05:00 GMT Connection: close X-Powered-By: PHP/8.5.8 Set-Cookie: tema=escuro; expires=Mon, 16 Nov 2026 00:05:00 GMT; Max-Age=2592000; path=/; secure; HttpOnly; SameSite=Lax Set-Cookie: aviso=Ana%20Souza Content-type: text/html; charset=UTF-8 ok
Cada opção virou um pedaço do cabeçalho Set-Cookie. Repare também no segundo cookie: o espaço de Ana Souza viajou como %20. O setcookie codifica o valor sozinho, e o PHP decodifica ao montar $_COOKIE.
| Opção | Exemplo | O que faz |
|---|---|---|
expires | time() + 3600 | Quando o cookie vence (em segundos desde 1970). 0 ou ausente: some ao fechar o navegador. |
path | '/' | Em quais caminhos do site o navegador devolve o cookie. '/' = o site todo. Sem essa opção, vale a pasta da página que criou o cookie (um cookie criado em /loja/carrinho.php não volta em /index.php). |
domain | 'padaria.com.br' | Para qual site. Ausente: só o site exato que criou. Com o domínio, vale também para os subdomínios (loja.padaria.com.br). |
secure | true | O navegador só devolve o cookie em conexões https://. (Os navegadores abrem uma exceção para localhost, para testes.) |
httponly | true | O JavaScript da página não consegue ler o cookie (document.cookie não o mostra). Se um ataque XSS passar (Escapar a Saída), ele não rouba esse cookie. |
samesite | 'Lax' | Se o cookie vai junto em pedidos que começam em outro site. 'Strict': nunca. 'Lax': só quando a pessoa clica num link para o seu site (não em formulários POST nem imagens de outros sites). 'None': sempre (exige secure). Protege contra ataques em que outro site faz o navegador da vítima enviar pedidos para o seu. |
'path' => '/', 'secure' => true (com o site em HTTPS), 'httponly' => true e 'samesite' => 'Lax'. Só deixe o httponly de fora se o seu JavaScript realmente precisar ler o cookie.A forma antiga, com tudo em argumentos separados, ainda funciona e aparece muito em códigos mais velhos:
// setcookie(nome, valor, expires, path, domain, secure, httponly)
setcookie('tema', 'escuro', time() + 3600, '/', '', true, true);
Ela não tem lugar para o samesite, e é fácil errar a ordem. Prefira o array.
Apagar um cookie
Não existe "apagar" no protocolo: o servidor pede para o navegador substituir o cookie por um já vencido. O jeito clássico é uma data no passado:
<?php
setcookie('tema', '', time() - 3600);
echo "Cookie apagado.\n";
$ curl -si -b pote -c pote localhost:8000/apagar.php HTTP/1.1 200 OK … Set-Cookie: tema=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0 Content-type: text/html; charset=UTF-8 Cookie apagado. $ curl -s -b pote localhost:8000/ler.php Tema: claro « o cookie sumiu do pote, e voltou o valor padrão
O PHP, ao ver um valor vazio, mandou tema=deleted com vencimento em 1970 e Max-Age=0: o navegador descarta na hora. Dois cuidados:
- Use o mesmo
pathedomaincom que o cookie foi criado. Para o navegador,temacompath=/etemacompath=/lojasão cookies diferentes. - O
$_COOKIE['tema']do pedido atual continua lá (ele veio no pedido). Se o resto da página usa o valor, faça tambémunset($_COOKIE['tema']).
"Headers already sent": o erro mais famoso
A Ana escreveu uma saudação antes de criar o cookie:
<?php
echo "Bem-vinda!\n";
setcookie('tema', 'escuro');
$ curl -si localhost:8000/erro.php HTTP/1.1 200 OK Host: localhost:8000 Date: Sat, 17 Oct 2026 00:05:00 GMT Connection: close X-Powered-By: PHP/8.5.8 Content-type: text/html; charset=UTF-8 Bem-vinda! <br /> <b>Warning</b>: Cannot modify header information - headers already sent by (output started at /home/ana/site/c/erro.php:2) in <b>/home/ana/site/c/erro.php</b> on line <b>3</b><br />
Não há Set-Cookie nos cabeçalhos: o cookie não foi criado. Lembre do desenho: uma resposta HTTP é cabeçalhos primeiro, conteúdo depois, como uma carta em que o envelope é fechado antes de o carteiro sair. No momento em que o echo da linha 2 escreveu a primeira letra do conteúdo, o PHP precisou mandar os cabeçalhos (o envelope) pela rede. Quando a linha 3 tentou acrescentar um Set-Cookie, o envelope já tinha saído.
| Parte da mensagem | Significa |
|---|---|
Cannot modify header information | "Não consigo mudar os cabeçalhos" (o setcookie é um cabeçalho) |
headers already sent | "os cabeçalhos já foram enviados" |
(output started at …/erro.php:2) | A pista mais importante: o arquivo e a linha que escreveram a primeira saída. É lá que está o problema. |
in …/erro.php on line 3 | Onde está o setcookie que falhou. |
A saída invisível
O caso mais traiçoeiro é quando você não vê nenhum echo. Este arquivo tem só uma linha em branco antes do <?php (a linha 1 está vazia; o <?php está na linha 2):
<?php
setcookie("tema", "escuro");
echo "ok\n";
$ curl -si localhost:8000/erro2.php … Content-type: text/html; charset=UTF-8 <br /> <b>Warning</b>: Cannot modify header information - headers already sent by (output started at /home/ana/site/c/erro2.php:1) in <b>/home/ana/site/c/erro2.php</b> on line <b>3</b><br /> ok
Tudo o que está fora de <?php … ?> é saída, inclusive uma linha vazia ou um espaço. O "output started at …:1" aponta a linha 1: a linha em branco. Outros culpados comuns:
- Um arquivo incluído (include e require) que termina com
?>seguido de uma linha em branco. Por isso a recomendação: em arquivos só de PHP, não feche o?>no final. - O BOM: três bytes invisíveis que alguns editores (o Bloco de Notas antigo do Windows) colocam no começo de arquivos UTF-8. Salve como "UTF-8 sem BOM".
- Um
WarningouNoticeanterior: a própria mensagem de aviso é saída.
setcookie, session_start e header vêm antes de qualquer saída. Organize a página assim: primeiro todo o PHP que decide coisas (ler formulário, validar, cookies, redirecionar), depois o HTML. Você vai encontrar o mesmo erro com header() em header e Redirecionamento.Alguns servidores ligam a configuração output_buffering no php.ini: o PHP guarda a saída num "buffer" (área de espera) antes de mandar, e o erro só aparece depois de alguns kilobytes de saída. É por isso que um código com esse problema às vezes "funciona no servidor e não no meu computador" (ou o contrário). Não conte com isso: siga a regra.
Limites e privacidade
| Limite | Valor aproximado | Consequência |
|---|---|---|
| Tamanho de um cookie | cerca de 4 KB (nome + valor + opções) | Maior que isso, o navegador ignora o cookie em silêncio |
| Cookies por site | pelo menos 50 nos navegadores atuais | Os mais antigos são descartados |
| Tráfego | Todos os cookies do site vão em todo pedido | Cookies grandes deixam cada imagem, CSS e página mais lentos |
O que guardar: preferências pequenas e sem segredo (tema, idioma, "já viu o aviso"), e o identificador da sessão (que o PHP cria sozinho, em Sessões).
O que nunca guardar: senhas, números de cartão, CPF, nada que dê acesso ("logado=sim", "admin=1"), nada que você não queira que a pessoa leia ou mude.
Privacidade: cookies também servem para seguir pessoas. Os "cookies de terceiros" (de anúncios e rastreadores embutidos em vários sites) contam a uma empresa por onde você passou. Por isso leis como a LGPD (no Brasil) e a GDPR (na Europa) exigem que o site informe que cookies usa e peça consentimento para os que não são essenciais, e os navegadores vêm bloqueando cookies de terceiros. Cookies essenciais para o site funcionar (sessão de login, carrinho) não precisam de consentimento, mas precisam ser informados na política de privacidade.
Quando dá errado
Cannot modify header information - headers already sent by (output started at …)
<b>Warning</b>: Cannot modify header information - headers already sent by (output started at /home/ana/site/c/erro.php:2) in <b>/home/ana/site/c/erro.php</b> on line <b>3</b><br />
O que aconteceu: algo foi escrito antes do setcookie. O cookie não foi criado.
Como resolver: vá ao arquivo e à linha do "output started at" e tire a saída de lá (um echo, HTML, linha em branco, BOM, um aviso). Mova o setcookie para o topo, antes de qualquer HTML.
$_COOKIE está vazio
O que chegou neste pedido: array(0) {
}
O que aconteceu: é o comportamento normal: o cookie só volta no próximo pedido.
Como resolver: no pedido que cria o cookie, use a variável que você acabou de gravar ($tema = $_GET['tema'] ?? $_COOKIE['tema'] ?? 'claro';), e não o $_COOKIE.
O que aconteceu: sem path, o cookie vale só para a pasta da página que o criou (e as de dentro). Criado em /loja/carrinho.php, ele não é enviado para /index.php.
Como resolver: 'path' => '/'.
O que aconteceu: o path (ou domain) do "apagar" é diferente do usado na criação, então o navegador entende que é outro cookie. Ou o setcookie de apagar caiu no erro de headers already sent.
Como resolver: apague com as mesmas opções da criação: setcookie('tema', '', ['expires' => time() - 3600, 'path' => '/']), antes de qualquer saída.
secure não volta
O que aconteceu: o site está em http:// (sem S). Com secure, o navegador só devolve o cookie em https:// (fora a exceção do localhost).
Como resolver: use HTTPS no site (o certo) ou, só em testes num endereço que não seja localhost, tire o secure.
Referência
| Código | O que faz |
|---|---|
setcookie('n', 'v') | Cookie que some ao fechar o navegador |
setcookie('n', 'v', time() + 3600) | Cookie que dura 1 hora |
setcookie('n', 'v', ['expires' => …, 'path' => '/', 'secure' => true, 'httponly' => true, 'samesite' => 'Lax']) | Forma moderna, com todas as opções |
$_COOKIE['n'] ?? 'padrão' | Lê o cookie (que veio neste pedido) |
setcookie('n', '', time() - 3600) | Apaga (com o mesmo path/domain da criação) |
headers_sent() | true se os cabeçalhos já foram enviados (dá para conferir antes do setcookie) |
curl -c pote -b pote | Guarda e devolve cookies nos testes |
Exercícios
1. Uma semana
Complete para o cookie durar 7 dias:
2. Longe do JavaScript
Complete a opção que impede o JavaScript da página de ler o cookie:
3. Ler com padrão
Complete para ler o cookie idioma, com 'pt' se ele não existir:
4. Logo depois de setcookie('tema', 'escuro'), no mesmo pedido, quanto vale $_COOKIE['tema'] ?? 'nada' (sem cookie anterior)?
5. O "output started at index.php:1" aponta a linha 1, mas ela só tem <?php. O que provavelmente está errado?
6. O que é seguro guardar num cookie?
Missão: a página foi aberta com ?tema=escuro, mas mostra "Tema: claro", porque o cookie novo só chega no próximo pedido. Faça duas coisas: quando vier $_GET['tema'], grave o cookie tema por 30 dias; e escolha o tema com $_GET['tema'] ?? $_COOKIE['tema'] ?? 'claro'. Depois clique nos links na aba Resultado e em "Recarregar" (sem ?tema): o cookie lembra a escolha.
<?php
setcookie('tema', $_GET['tema'] ?? 'claro');
$tema = $_COOKIE['tema'] ?? 'claro';
?>
<p>Tema: <?= htmlspecialchars($tema) ?></p>
<p><a href="?tema=claro">Claro</a> | <a href="?tema=escuro">Escuro</a> | <a href="?">Recarregar</a></p>
<?php
if (isset($_GET['tema'])) {
setcookie('tema', $_GET['tema'], time() + 60 * 60 * 24 * 30);
}
$tema = $_GET['tema'] ?? $_COOKIE['tema'] ?? 'claro';
?>
<p>Tema: <?= htmlspecialchars($tema) ?></p>
<p><a href="?tema=claro">Claro</a> | <a href="?tema=escuro">Escuro</a> | <a href="?">Recarregar</a></p>
Missão: o cookie de idioma está na forma antiga, sem proteção. Reescreva com o array de opções: expires em 1 ano, path '/', httponly true e samesite 'Lax'.
<?php
setcookie('idioma', 'pt-BR', time() + 60 * 60 * 24 * 365);
echo "<p>Idioma salvo.</p>";
<?php
setcookie('idioma', 'pt-BR', [
'expires' => time() + 60 * 60 * 24 * 365,
'path' => '/',
'httponly' => true,
'samesite' => 'Lax',
]);
echo "<p>Idioma salvo.</p>";
Resumo
- Cookie = texto que o servidor pede ao navegador para guardar (
Set-Cookie) e que volta em todo pedido seguinte (Cookie). setcookie(nome, valor, opções); a expiração étime() + segundos; sem ela, o cookie some ao fechar o navegador.- Leia com
$_COOKIE['nome'] ?? padrão. O cookie criado agora só aparece no próximo pedido. - Opções:
path('/'),domain,secure(só HTTPS),httponly(longe do JavaScript),samesite('Lax'). Use o array de opções. - Apagar = gravar vencido (
time() - 3600), com o mesmopath/domain. setcookievem antes de qualquer saída; senão, headers already sent. A pista é o "output started at".- Cookie é dado do visitante: pequeno (até ~4 KB), sem segredos, validado e escapado. Para login, use Sessões.