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.

Você vai aprender
  • O que é um cookie e o caminho que ele faz (servidor → navegador → servidor).
  • Como criar um cookie com setcookie e 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, httponly e samesite, 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)
O cookie viaja em cabeçalhos HTTP: 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 com 0), é 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
Cookie é dado do visitante. Ele fica no computador da pessoa, e ela pode editar, apagar ou inventar cookies (no navegador, F12 → Armazenamento; no 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çãoExemploO que faz
expirestime() + 3600Quando 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).
securetrueO navegador só devolve o cookie em conexões https://. (Os navegadores abrem uma exceção para localhost, para testes.)
httponlytrueO 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.
Receita para cookies importantes: '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 path e domain com que o cookie foi criado. Para o navegador, tema com path=/ e tema com path=/loja sã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ém unset($_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 mensagemSignifica
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 3Onde 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 Warning ou Notice anterior: a própria mensagem de aviso é saída.
A regra: 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

LimiteValor aproximadoConsequência
Tamanho de um cookiecerca de 4 KB (nome + valor + opções)Maior que isso, o navegador ignora o cookie em silêncio
Cookies por sitepelo menos 50 nos navegadores atuaisOs mais antigos são descartados
TráfegoTodos os cookies do site vão em todo pedidoCookies 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.

Criei o cookie, mas $_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 cookie não volta em outra página do site

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' => '/'.

Mandei apagar, e o cookie continua lá

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.

O cookie 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ódigoO 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 poteGuarda e devolve cookies nos testes

Exercícios

1. Uma semana

Complete para o cookie durar 7 dias:

setcookie('idioma', 'pt', time() + 60 * 60 * 24 * );

2. Longe do JavaScript

Complete a opção que impede o JavaScript da página de ler o cookie:

setcookie('id', $id, ['' => true]);

3. Ler com padrão

Complete para ler o cookie idioma, com 'pt' se ele não existir:

$idioma = ;

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 mesmo path/domain.
  • setcookie vem 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.