PHP Projeto Formulário de Contato
A lojinha da Ana vende cestas de café da manhã e quer receber mensagens dos clientes pelo site. Neste projeto você constrói o formulário de contato completo: ele confere cada campo, mostra o erro ao lado do campo certo, não apaga o que a pessoa digitou, guarda as mensagens num arquivo e não grava a mesma mensagem duas vezes quando alguém aperta F5.
- Guardar os campos num array
$dadose os erros num array$erroscom o nome do campo como chave. - Validar nome, e-mail (
filter_var), assunto e mensagem (mb_strlen), e mostrar cada erro no lugar certo. - Devolver os valores para
input,selectetextareacom segurança. - Salvar cada mensagem como uma linha de CSV com
fputcsv(e a versão em JSON). - O padrão Post/Redirect/Get: por que o F5 duplica envios e como o
header('Location: …')resolve. - Listar as mensagens recebidas numa tabela, sem deixar uma mensagem maliciosa estragar a página.
Antes de começar
Faça antes o projeto da calculadora: ele mostra, com calma, o "esqueleto" que vamos repetir aqui (o PHP no topo prepara os dados, o HTML embaixo mostra). As aulas que este projeto usa:
| O que usamos | Aula |
|---|---|
$_POST, $_GET, ?? | $_GET e $_POST |
filter_var, mb_strlen, mensagens de erro | Validar Dados, Acentos e mbstring |
htmlspecialchars | Escapar a Saída (XSS) |
fopen, fputcsv, fgetcsv | Ler e Escrever Arquivos, CSV |
json_encode, json_decode | JSON |
header('Location: …'), exit | header e Redirecionamento |
date, fuso horário | Datas e Horas |
Crie a pasta e suba o servidor (veja O Servidor Embutido); deixe o terminal aberto e use outro para o curl:
$ mkdir contato $ cd contato $ php -S localhost:8000 [Fri Oct 16 20:50:12 2026] PHP 8.5.8 Development Server (http://localhost:8000) started
Cada etapa tem uma caixa "Experimente" no modo web com o arquivo completo daquela etapa: a aba Resultado é a página, e o formulário funciona nela. Os blocos de código das etapas mostram só o que é novo.
As etapas
-
Etapa 1 · O formulário que não esquece
Quatro campos: nome, e-mail, assunto (uma lista) e a mensagem (um
textarea, a caixa de texto de várias linhas). Em vez de quatro variáveis soltas, os valores ficam juntos no array$dados, e cada campo já volta preenchido depois do envio.<?php $assuntos = [ 'duvida' => 'Dúvida', 'pedido' => 'Pedido', 'elogio' => 'Elogio', 'reclamacao' => 'Reclamação', ]; $dados = [ 'nome' => trim($_POST['nome'] ?? ''), 'email' => trim($_POST['email'] ?? ''), 'assunto' => $_POST['assunto'] ?? 'duvida', 'mensagem' => trim($_POST['mensagem'] ?? ''), ]; function e(string $texto): string { return htmlspecialchars($texto); } ?> <!DOCTYPE html> <html lang="pt-BR"> <head> <meta charset="utf-8"> <title>Fale com a Ana</title> </head> <body> <h1>Fale com a lojinha da Ana</h1> <form method="post"> <p><label>Nome<br><input name="nome" value="<?= e($dados['nome']) ?>"></label></p> <p><label>E-mail<br><input name="email" value="<?= e($dados['email']) ?>"></label></p> <p><label>Assunto<br> <select name="assunto"> <?php foreach ($assuntos as $valor => $texto): ?> <option value="<?= $valor ?>"<?= $valor === $dados['assunto'] ? ' selected' : '' ?>><?= $texto ?></option> <?php endforeach; ?> </select></label></p> <p><label>Mensagem<br><textarea name="mensagem" rows="5" cols="40"><?= e($dados['mensagem']) ?></textarea></label></p> <p><button>Enviar</button></p> </form> </body> </html>Trecho O que faz $assuntos = ['duvida' => 'Dúvida', …]As opções da lista: a chave (sem acento nem espaço) é o que viaja no formulário; o valor é o texto que a pessoa vê. $dados = ['nome' => trim($_POST['nome'] ?? ''), …]Lê os quatro campos de uma vez. Na primeira visita (GET), nada foi enviado e tudo vale ''(o assunto começa em'duvida').function e(string $texto): stringUm "apelido" curto para htmlspecialchars. Como ele vai aparecer muitas vezes, o nome curto deixa o HTML legível. É um costume comum em projetos PHP.value="<?= e($dados['nome']) ?>"Devolve o que foi digitado para o campo, escapado. <textarea><?= e($dados['mensagem']) ?></textarea>O textareanão temvalue: o conteúdo fica entre as tags. Sem espaços nem quebras de linha extras, senão eles entram no texto.$valor === $dados['assunto'] ? ' selected' : ''Marca a opção que foi enviada. A primeira visita mostra tudo vazio; um envio volta com os campos preenchidos:
$ curl -s localhost:8000/ | grep -E 'input|option|textarea' <p><label>Nome<br><input name="nome" value=""></label></p> <p><label>E-mail<br><input name="email" value=""></label></p> <option value="duvida" selected>Dúvida</option> <option value="pedido">Pedido</option> <option value="elogio">Elogio</option> <option value="reclamacao">Reclamação</option> <p><label>Mensagem<br><textarea name="mensagem" rows="5" cols="40"></textarea></label></p> $ curl -s -d 'nome=Beto&email=beto@exemplo.com&assunto=pedido&mensagem=Entregam no sábado?' localhost:8000/ | grep -E 'input|selected|textarea' <p><label>Nome<br><input name="nome" value="Beto"></label></p> <p><label>E-mail<br><input name="email" value="beto@exemplo.com"></label></p> <option value="pedido" selected>Pedido</option> <p><label>Mensagem<br><textarea name="mensagem" rows="5" cols="40">Entregam no sábado?</textarea></label></p>E se alguém tentar "furar" o formulário? O
">tentaria fechar ovalue, e o</textarea>tentaria fechar a caixa de texto antes da hora. Com oe(), tudo vira texto inofensivo:$ curl -s -d 'nome="><script>alert(1)</script>&mensagem=</textarea><b>oi</b>' localhost:8000/ | grep -E 'input|textarea' <p><label>Nome<br><input name="nome" value=""><script>alert(1)</script>"></label></p> <p><label>E-mail<br><input name="email" value=""></label></p> <p><label>Mensagem<br><textarea name="mensagem" rows="5" cols="40"></textarea><b>oi</b></textarea></label></p>
Missão: acrescente um campo opcional Telefone (
name="telefone"), logo depois do e-mail, que também volte preenchido. Você vai precisar de uma linha nova em$dadose de um parágrafo novo no formulário. A caixa já envia um telefone para você conferir.<?php $assuntos = [ 'duvida' => 'Dúvida', 'pedido' => 'Pedido', 'elogio' => 'Elogio', 'reclamacao' => 'Reclamação', ]; $dados = [ 'nome' => trim($_POST['nome'] ?? ''), 'email' => trim($_POST['email'] ?? ''), 'assunto' => $_POST['assunto'] ?? 'duvida', 'mensagem' => trim($_POST['mensagem'] ?? ''), ]; function e(string $texto): string { return htmlspecialchars($texto); } ?> <!DOCTYPE html> <html lang="pt-BR"> <head> <meta charset="utf-8"> <title>Fale com a Ana</title> </head> <body> <h1>Fale com a lojinha da Ana</h1> <form method="post"> <p><label>Nome<br><input name="nome" value="<?= e($dados['nome']) ?>"></label></p> <p><label>E-mail<br><input name="email" value="<?= e($dados['email']) ?>"></label></p> <p><label>Assunto<br> <select name="assunto"> <?php foreach ($assuntos as $valor => $texto): ?> <option value="<?= $valor ?>"<?= $valor === $dados['assunto'] ? ' selected' : '' ?>><?= $texto ?></option> <?php endforeach; ?> </select></label></p> <p><label>Mensagem<br><textarea name="mensagem" rows="5" cols="40"><?= e($dados['mensagem']) ?></textarea></label></p> <p><button>Enviar</button></p> </form> </body> </html><?php $assuntos = [ 'duvida' => 'Dúvida', 'pedido' => 'Pedido', 'elogio' => 'Elogio', 'reclamacao' => 'Reclamação', ]; $dados = [ 'nome' => trim($_POST['nome'] ?? ''), 'email' => trim($_POST['email'] ?? ''), 'telefone' => trim($_POST['telefone'] ?? ''), 'assunto' => $_POST['assunto'] ?? 'duvida', 'mensagem' => trim($_POST['mensagem'] ?? ''), ]; function e(string $texto): string { return htmlspecialchars($texto); } ?> <!DOCTYPE html> <html lang="pt-BR"> <head> <meta charset="utf-8"> <title>Fale com a Ana</title> </head> <body> <h1>Fale com a lojinha da Ana</h1> <form method="post"> <p><label>Nome<br><input name="nome" value="<?= e($dados['nome']) ?>"></label></p> <p><label>E-mail<br><input name="email" value="<?= e($dados['email']) ?>"></label></p> <p><label>Telefone<br><input name="telefone" value="<?= e($dados['telefone']) ?>"></label></p> <p><label>Assunto<br> <select name="assunto"> <?php foreach ($assuntos as $valor => $texto): ?> <option value="<?= $valor ?>"<?= $valor === $dados['assunto'] ? ' selected' : '' ?>><?= $texto ?></option> <?php endforeach; ?> </select></label></p> <p><label>Mensagem<br><textarea name="mensagem" rows="5" cols="40"><?= e($dados['mensagem']) ?></textarea></label></p> <p><button>Enviar</button></p> </form> </body> </html> -
Etapa 2 · Validar cada campo
Agora as regras. Cada erro entra em
$erroscom o nome do campo como chave:$erros['email'] = '…'. Assim, na hora de mostrar, sabemos exatamente ao lado de qual campo cada mensagem vai.$erros = []; if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (mb_strlen($dados['nome']) < 2) { $erros['nome'] = 'Escreva o seu nome.'; } if (filter_var($dados['email'], FILTER_VALIDATE_EMAIL) === false) { $erros['email'] = 'Esse e-mail não parece válido.'; } if (!array_key_exists($dados['assunto'], $assuntos)) { $erros['assunto'] = 'Escolha um assunto da lista.'; } $tamanho = mb_strlen($dados['mensagem']); if ($tamanho < 10 || $tamanho > 1000) { $erros['mensagem'] = "A mensagem precisa ter entre 10 e 1000 caracteres (tem $tamanho)."; } }Regra Por quê mb_strlen($dados['nome']) < 2Conta as letras do nome. O strlencomum conta bytes, e cada letra acentuada vale 2 bytes em UTF-8:strlen('Zé')é 3. Para texto em português, use as funçõesmb_(veja Acentos e mbstring).filter_var($email, FILTER_VALIDATE_EMAIL)"Filtrar a variável como e-mail": devolve o próprio e-mail se ele tiver um formato válido, ou false. No laboratório,ana@exemplo.compassou;ana@exemplo,ana.exemplo.comeana@localhostderamfalse. Formato válido não quer dizer que o e-mail existe; só que parece um e-mail.=== falseCompara com falsede forma estrita, porque o retorno bom é um texto.array_key_exists($dados['assunto'], $assuntos)O assunto tem que ser uma das chaves da lista. Qualquer pessoa pode enviar assunto=spampelocurl."… (tem $tamanho)."Aspas duplas trocam $tamanhopelo valor (veja Interpolação). Dizer quanto falta ajuda a pessoa a corrigir.Ao lado de cada campo, o erro aparece só se existir. O mesmo padrão se repete para os quatro campos:
<p><label>E-mail<br><input name="email" value="<?= e($dados['email']) ?>"></label> <?php if (isset($erros['email'])): ?><br><span class="erro"><?= e($erros['email']) ?></span><?php endif; ?> </p>E, antes do formulário, uma confirmação quando não houve erro nenhum (provisória: na etapa 3 ela muda):
<?php if ($_SERVER['REQUEST_METHOD'] === 'POST' && !$erros): ?> <p class="sucesso">Tudo certo, <?= e($dados['nome']) ?>! (ainda não salvamos nada)</p> <?php endif; ?>
$ curl -s -d 'nome=A&email=ana@exemplo&assunto=spam&mensagem=Oi' localhost:8000/ | grep erro <br><span class="erro">Escreva o seu nome.</span> </p> <br><span class="erro">Esse e-mail não parece válido.</span> </p> <br><span class="erro">Escolha um assunto da lista.</span> </p> <br><span class="erro">A mensagem precisa ter entre 10 e 1000 caracteres (tem 2).</span> </p> $ curl -s -d 'nome=Beto&email=beto@exemplo.com&assunto=pedido&mensagem=Entregam no sábado?' localhost:8000/ | grep -E 'erro|sucesso' <p class="sucesso">Tudo certo, Beto! (ainda não salvamos nada)</p>E orequireddo HTML?Você pode (e deve) pôr
requiredetype="email"nos campos: o navegador avisa antes de enviar, o que é mais rápido para a pessoa. Mas isso é só conforto: ocurlacima ignorou tudo isso. A validação que protege é a do PHP.Missão: a Ana anda recebendo propaganda. Acrescente uma regra: se a mensagem contiver
http, o erro do campo mensagem éLinks não são permitidos na mensagem.Usestr_contains($dados['mensagem'], 'http').<?php $assuntos = [ 'duvida' => 'Dúvida', 'pedido' => 'Pedido', 'elogio' => 'Elogio', 'reclamacao' => 'Reclamação', ]; $dados = [ 'nome' => trim($_POST['nome'] ?? ''), 'email' => trim($_POST['email'] ?? ''), 'assunto' => $_POST['assunto'] ?? 'duvida', 'mensagem' => trim($_POST['mensagem'] ?? ''), ]; $erros = []; if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (mb_strlen($dados['nome']) < 2) { $erros['nome'] = 'Escreva o seu nome.'; } if (filter_var($dados['email'], FILTER_VALIDATE_EMAIL) === false) { $erros['email'] = 'Esse e-mail não parece válido.'; } if (!array_key_exists($dados['assunto'], $assuntos)) { $erros['assunto'] = 'Escolha um assunto da lista.'; } $tamanho = mb_strlen($dados['mensagem']); if ($tamanho < 10 || $tamanho > 1000) { $erros['mensagem'] = "A mensagem precisa ter entre 10 e 1000 caracteres (tem $tamanho)."; } } function e(string $texto): string { return htmlspecialchars($texto); } ?> <!DOCTYPE html> <html lang="pt-BR"> <head> <meta charset="utf-8"> <title>Fale com a Ana</title> </head> <body> <h1>Fale com a lojinha da Ana</h1> <?php if ($_SERVER['REQUEST_METHOD'] === 'POST' && !$erros): ?> <p class="sucesso">Tudo certo, <?= e($dados['nome']) ?>! (ainda não salvamos nada)</p> <?php endif; ?> <form method="post"> <p><label>Nome<br><input name="nome" value="<?= e($dados['nome']) ?>"></label> <?php if (isset($erros['nome'])): ?><br><span class="erro"><?= e($erros['nome']) ?></span><?php endif; ?> </p> <p><label>E-mail<br><input name="email" value="<?= e($dados['email']) ?>"></label> <?php if (isset($erros['email'])): ?><br><span class="erro"><?= e($erros['email']) ?></span><?php endif; ?> </p> <p><label>Assunto<br> <select name="assunto"> <?php foreach ($assuntos as $valor => $texto): ?> <option value="<?= $valor ?>"<?= $valor === $dados['assunto'] ? ' selected' : '' ?>><?= $texto ?></option> <?php endforeach; ?> </select></label> <?php if (isset($erros['assunto'])): ?><br><span class="erro"><?= e($erros['assunto']) ?></span><?php endif; ?> </p> <p><label>Mensagem<br><textarea name="mensagem" rows="5" cols="40"><?= e($dados['mensagem']) ?></textarea></label> <?php if (isset($erros['mensagem'])): ?><br><span class="erro"><?= e($erros['mensagem']) ?></span><?php endif; ?> </p> <p><button>Enviar</button></p> </form> </body> </html><?php $assuntos = [ 'duvida' => 'Dúvida', 'pedido' => 'Pedido', 'elogio' => 'Elogio', 'reclamacao' => 'Reclamação', ]; $dados = [ 'nome' => trim($_POST['nome'] ?? ''), 'email' => trim($_POST['email'] ?? ''), 'assunto' => $_POST['assunto'] ?? 'duvida', 'mensagem' => trim($_POST['mensagem'] ?? ''), ]; $erros = []; if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (mb_strlen($dados['nome']) < 2) { $erros['nome'] = 'Escreva o seu nome.'; } if (filter_var($dados['email'], FILTER_VALIDATE_EMAIL) === false) { $erros['email'] = 'Esse e-mail não parece válido.'; } if (!array_key_exists($dados['assunto'], $assuntos)) { $erros['assunto'] = 'Escolha um assunto da lista.'; } $tamanho = mb_strlen($dados['mensagem']); if ($tamanho < 10 || $tamanho > 1000) { $erros['mensagem'] = "A mensagem precisa ter entre 10 e 1000 caracteres (tem $tamanho)."; } if (str_contains($dados['mensagem'], 'http')) { $erros['mensagem'] = 'Links não são permitidos na mensagem.'; } } function e(string $texto): string { return htmlspecialchars($texto); } ?> <!DOCTYPE html> <html lang="pt-BR"> <head> <meta charset="utf-8"> <title>Fale com a Ana</title> </head> <body> <h1>Fale com a lojinha da Ana</h1> <?php if ($_SERVER['REQUEST_METHOD'] === 'POST' && !$erros): ?> <p class="sucesso">Tudo certo, <?= e($dados['nome']) ?>! (ainda não salvamos nada)</p> <?php endif; ?> <form method="post"> <p><label>Nome<br><input name="nome" value="<?= e($dados['nome']) ?>"></label> <?php if (isset($erros['nome'])): ?><br><span class="erro"><?= e($erros['nome']) ?></span><?php endif; ?> </p> <p><label>E-mail<br><input name="email" value="<?= e($dados['email']) ?>"></label> <?php if (isset($erros['email'])): ?><br><span class="erro"><?= e($erros['email']) ?></span><?php endif; ?> </p> <p><label>Assunto<br> <select name="assunto"> <?php foreach ($assuntos as $valor => $texto): ?> <option value="<?= $valor ?>"<?= $valor === $dados['assunto'] ? ' selected' : '' ?>><?= $texto ?></option> <?php endforeach; ?> </select></label> <?php if (isset($erros['assunto'])): ?><br><span class="erro"><?= e($erros['assunto']) ?></span><?php endif; ?> </p> <p><label>Mensagem<br><textarea name="mensagem" rows="5" cols="40"><?= e($dados['mensagem']) ?></textarea></label> <?php if (isset($erros['mensagem'])): ?><br><span class="erro"><?= e($erros['mensagem']) ?></span><?php endif; ?> </p> <p><button>Enviar</button></p> </form> </body> </html> -
Etapa 3 · Salvar a mensagem num arquivo CSV
Quando não há erros, a mensagem vai para o fim do arquivo
mensagens.csv, uma linha por mensagem. CSV ("valores separados por vírgula") é o formato de planilha mais simples que existe: abre no LibreOffice Calc e no Excel.<?php date_default_timezone_set('America/Sao_Paulo'); ... $erros = []; $enviado = false; if ($_SERVER['REQUEST_METHOD'] === 'POST') { // ... as quatro validações da etapa 2 ... if (!$erros) { $linha = [date('Y-m-d H:i:s'), $dados['nome'], $dados['email'], $dados['assunto'], $dados['mensagem']]; $arquivo = fopen(__DIR__ . '/mensagens.csv', 'a'); fputcsv($arquivo, $linha, ',', '"', ''); fclose($arquivo); $enviado = true; } }- date_default_timezone_set(…)
- Diz ao PHP o fuso horário. Sem isso (e sem configurar o
php.ini), o PHP usa UTC: no laboratório,date_default_timezone_get()respondeuUTC, e as mensagens sairiam com 3 horas a mais. - fopen(…, 'a')
- Abre o arquivo no modo
'a'(append, "acrescentar"): escreve no fim, sem apagar o que já existe, e cria o arquivo se ele não existir. - __DIR__ . '/mensagens.csv'
__DIR__é a pasta do próprioindex.php. Usar o caminho completo evita surpresas quando o PHP roda a partir de outra pasta.- fputcsv($arquivo, $linha, ',', '"', '')
- Escreve o array como uma linha CSV, cuidando sozinho das vírgulas e aspas dentro do texto. Os três últimos argumentos são o separador, a aspa e o caractere de escape; o escape vazio
''é o recomendado e, no PHP 8.4+, precisa ser informado (veja "Quando dá errado"). - fclose($arquivo)
- Fecha o arquivo e garante que tudo foi gravado.
$ curl -s -d 'nome=Beto Souza&email=beto@exemplo.com&assunto=pedido&mensagem=Vocês entregam no sábado? Queria duas cestas.' localhost:8000/ | grep sucesso <p class="sucesso">Obrigada, Beto Souza! Sua mensagem foi registrada.</p> $ cat mensagens.csv "2026-10-16 20:50:07","Beto Souza",beto@exemplo.com,pedido,"Vocês entregam no sábado? Queria duas cestas."
Repare que o
fputcsvpôs aspas só onde precisava (textos com espaço). E a vírgula e as aspas dentro da mensagem da Carla não quebram a linha: o texto ganha aspas em volta e as aspas internas viram duas (""), a regra do CSV:$ curl -s -d 'nome=Carla&email=carla@exemplo.com&assunto=elogio&mensagem=Adorei a cesta "grande", obrigada!' localhost:8000/ | grep sucesso <p class="sucesso">Obrigada, Carla! Sua mensagem foi registrada.</p> $ curl -s -d 'nome=Carla&email=carla@exemplo.com&assunto=elogio&mensagem=Adorei a cesta "grande", obrigada!' localhost:8000/ | grep sucesso <p class="sucesso">Obrigada, Carla! Sua mensagem foi registrada.</p> « o mesmo envio de novo: é o que o F5 faz $ cat mensagens.csv "2026-10-16 20:50:07","Beto Souza",beto@exemplo.com,pedido,"Vocês entregam no sábado? Queria duas cestas." "2026-10-16 20:50:07",Carla,carla@exemplo.com,elogio,"Adorei a cesta ""grande"", obrigada!" "2026-10-16 20:50:07",Carla,carla@exemplo.com,elogio,"Adorei a cesta ""grande"", obrigada!" « duplicada!
Achou o problema? A página de "Obrigada" é a resposta de um POST. Se a Carla apertar F5 nela, o navegador pergunta "Reenviar os dados do formulário?" e, se ela confirmar, o mesmo POST vai de novo: mensagem duplicada. A próxima etapa resolve.
Missão: depois de salvar, conte quantas mensagens o arquivo já tem e mostre
Esta é a mensagem número N.abaixo do agradecimento. Dica:file(…)devolve um array com as linhas do arquivo, ecountconta. Envie o formulário mais vezes na aba Resultado e veja o número subir.<?php date_default_timezone_set('America/Sao_Paulo'); $assuntos = [ 'duvida' => 'Dúvida', 'pedido' => 'Pedido', 'elogio' => 'Elogio', 'reclamacao' => 'Reclamação', ]; $dados = [ 'nome' => trim($_POST['nome'] ?? ''), 'email' => trim($_POST['email'] ?? ''), 'assunto' => $_POST['assunto'] ?? 'duvida', 'mensagem' => trim($_POST['mensagem'] ?? ''), ]; $erros = []; $enviado = false; if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (mb_strlen($dados['nome']) < 2) { $erros['nome'] = 'Escreva o seu nome.'; } if (filter_var($dados['email'], FILTER_VALIDATE_EMAIL) === false) { $erros['email'] = 'Esse e-mail não parece válido.'; } if (!array_key_exists($dados['assunto'], $assuntos)) { $erros['assunto'] = 'Escolha um assunto da lista.'; } $tamanho = mb_strlen($dados['mensagem']); if ($tamanho < 10 || $tamanho > 1000) { $erros['mensagem'] = "A mensagem precisa ter entre 10 e 1000 caracteres (tem $tamanho)."; } if (!$erros) { $linha = [date('Y-m-d H:i:s'), $dados['nome'], $dados['email'], $dados['assunto'], $dados['mensagem']]; $arquivo = fopen(__DIR__ . '/mensagens.csv', 'a'); fputcsv($arquivo, $linha, ',', '"', ''); fclose($arquivo); $enviado = true; } } function e(string $texto): string { return htmlspecialchars($texto); } ?> <!DOCTYPE html> <html lang="pt-BR"> <head> <meta charset="utf-8"> <title>Fale com a Ana</title> </head> <body> <h1>Fale com a lojinha da Ana</h1> <?php if ($enviado): ?> <p class="sucesso">Obrigada, <?= e($dados['nome']) ?>! Sua mensagem foi registrada.</p> <?php endif; ?> <form method="post"> <p><label>Nome<br><input name="nome" value="<?= e($dados['nome']) ?>"></label> <?php if (isset($erros['nome'])): ?><br><span class="erro"><?= e($erros['nome']) ?></span><?php endif; ?> </p> <p><label>E-mail<br><input name="email" value="<?= e($dados['email']) ?>"></label> <?php if (isset($erros['email'])): ?><br><span class="erro"><?= e($erros['email']) ?></span><?php endif; ?> </p> <p><label>Assunto<br> <select name="assunto"> <?php foreach ($assuntos as $valor => $texto): ?> <option value="<?= $valor ?>"<?= $valor === $dados['assunto'] ? ' selected' : '' ?>><?= $texto ?></option> <?php endforeach; ?> </select></label> <?php if (isset($erros['assunto'])): ?><br><span class="erro"><?= e($erros['assunto']) ?></span><?php endif; ?> </p> <p><label>Mensagem<br><textarea name="mensagem" rows="5" cols="40"><?= e($dados['mensagem']) ?></textarea></label> <?php if (isset($erros['mensagem'])): ?><br><span class="erro"><?= e($erros['mensagem']) ?></span><?php endif; ?> </p> <p><button>Enviar</button></p> </form> </body> </html><?php date_default_timezone_set('America/Sao_Paulo'); $assuntos = [ 'duvida' => 'Dúvida', 'pedido' => 'Pedido', 'elogio' => 'Elogio', 'reclamacao' => 'Reclamação', ]; $dados = [ 'nome' => trim($_POST['nome'] ?? ''), 'email' => trim($_POST['email'] ?? ''), 'assunto' => $_POST['assunto'] ?? 'duvida', 'mensagem' => trim($_POST['mensagem'] ?? ''), ]; $erros = []; $enviado = false; if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (mb_strlen($dados['nome']) < 2) { $erros['nome'] = 'Escreva o seu nome.'; } if (filter_var($dados['email'], FILTER_VALIDATE_EMAIL) === false) { $erros['email'] = 'Esse e-mail não parece válido.'; } if (!array_key_exists($dados['assunto'], $assuntos)) { $erros['assunto'] = 'Escolha um assunto da lista.'; } $tamanho = mb_strlen($dados['mensagem']); if ($tamanho < 10 || $tamanho > 1000) { $erros['mensagem'] = "A mensagem precisa ter entre 10 e 1000 caracteres (tem $tamanho)."; } if (!$erros) { $linha = [date('Y-m-d H:i:s'), $dados['nome'], $dados['email'], $dados['assunto'], $dados['mensagem']]; $arquivo = fopen(__DIR__ . '/mensagens.csv', 'a'); fputcsv($arquivo, $linha, ',', '"', ''); fclose($arquivo); $enviado = true; $total = count(file(__DIR__ . '/mensagens.csv')); } } function e(string $texto): string { return htmlspecialchars($texto); } ?> <!DOCTYPE html> <html lang="pt-BR"> <head> <meta charset="utf-8"> <title>Fale com a Ana</title> </head> <body> <h1>Fale com a lojinha da Ana</h1> <?php if ($enviado): ?> <p class="sucesso">Obrigada, <?= e($dados['nome']) ?>! Sua mensagem foi registrada.</p> <p>Esta é a mensagem número <?= $total ?>.</p> <?php endif; ?> <form method="post"> <p><label>Nome<br><input name="nome" value="<?= e($dados['nome']) ?>"></label> <?php if (isset($erros['nome'])): ?><br><span class="erro"><?= e($erros['nome']) ?></span><?php endif; ?> </p> <p><label>E-mail<br><input name="email" value="<?= e($dados['email']) ?>"></label> <?php if (isset($erros['email'])): ?><br><span class="erro"><?= e($erros['email']) ?></span><?php endif; ?> </p> <p><label>Assunto<br> <select name="assunto"> <?php foreach ($assuntos as $valor => $texto): ?> <option value="<?= $valor ?>"<?= $valor === $dados['assunto'] ? ' selected' : '' ?>><?= $texto ?></option> <?php endforeach; ?> </select></label> <?php if (isset($erros['assunto'])): ?><br><span class="erro"><?= e($erros['assunto']) ?></span><?php endif; ?> </p> <p><label>Mensagem<br><textarea name="mensagem" rows="5" cols="40"><?= e($dados['mensagem']) ?></textarea></label> <?php if (isset($erros['mensagem'])): ?><br><span class="erro"><?= e($erros['mensagem']) ?></span><?php endif; ?> </p> <p><button>Enviar</button></p> </form> </body> </html> -
Etapa 4 · Post/Redirect/Get: acabar com o envio duplicado
O nome descreve a receita: o navegador faz o POST; o PHP salva e, em vez de mostrar a página, responde "vá para outro endereço" (Redirect); o navegador obedece e faz um GET nesse endereço. A página que a pessoa vê no fim é a resposta de um GET, e o F5 repete só o GET, que não grava nada.
if (!$erros) { $linha = [date('Y-m-d H:i:s'), $dados['nome'], $dados['email'], $dados['assunto'], $dados['mensagem']]; $arquivo = fopen(__DIR__ . '/mensagens.csv', 'a'); fputcsv($arquivo, $linha, ',', '"', ''); fclose($arquivo); header('Location: index.php?enviado=1', true, 303); exit; }E a confirmação agora olha o endereço, não uma variável:
<?php if (isset($_GET['enviado'])): ?> <p class="sucesso">Obrigada! Sua mensagem foi registrada. Responderemos em breve.</p> <?php endif; ?>
- header(
- Envia um cabeçalho HTTP: uma instrução para o navegador que vai antes do conteúdo da página.
- 'Location: index.php?enviado=1'
- "O que você procura está neste endereço." O
?enviado=1é só um aviso para a próxima página saber que deu certo. - true, 303
- Substitui um
Locationanterior e usa o código 303 See Other ("veja outro"), que manda o navegador buscar o novo endereço com GET. Sem o 303, o PHP usa 302, que os navegadores também tratam como GET; o 303 deixa a intenção explícita. - exit;
- Para o script aqui. Sem ele, o PHP continuaria e geraria a página inteira à toa (o navegador nem a mostraria).
O
curl -imostra os cabeçalhos da resposta. Repare: nada de HTML, só a ordem de ir para outro lugar:$ curl -si -d 'nome=Carla&email=carla@exemplo.com&assunto=elogio&mensagem=Adorei a cesta "grande", obrigada!' localhost:8000/ HTTP/1.1 303 See Other Host: localhost:8000 Date: Fri, 16 Oct 2026 23:50:13 GMT Connection: close X-Powered-By: PHP/8.5.8 Location: index.php?enviado=1 Content-type: text/html; charset=UTF-8
Com
-L, ocurlsegue o redirecionamento, como o navegador. A página final tem o agradecimento e o formulário limpo, pronto para outra mensagem:$ curl -sL -d 'nome=Davi&email=davi@exemplo.com&assunto=duvida&mensagem=Qual o horário de vocês?' localhost:8000/ | grep -E 'sucesso|value=' <p class="sucesso">Obrigada! Sua mensagem foi registrada. Responderemos em breve.</p> <p><label>Nome<br><input name="nome" value=""></label> <p><label>E-mail<br><input name="email" value=""></label> <option value="duvida" selected>Dúvida</option> <option value="pedido">Pedido</option> <option value="elogio">Elogio</option> <option value="reclamacao">Reclamação</option> $ cat mensagens.csv "2026-10-16 20:50:13",Carla,carla@exemplo.com,elogio,"Adorei a cesta ""grande"", obrigada!" "2026-10-16 20:50:13",Davi,davi@exemplo.com,duvida,"Qual o horário de vocês?"O terminal do servidor conta a história inteira: o POST recebeu
303, e logo depois veio o GET para/index.php?enviado=1:[Fri Oct 16 20:50:13 2026] 127.0.0.1:57254 [303]: POST / [Fri Oct 16 20:50:13 2026] 127.0.0.1:57264 [200]: GET /index.php?enviado=1
E quando há erros?Aí não redirecionamos: a resposta do próprio POST mostra o formulário com os erros e os valores digitados. Redirecionar faria a pessoa perder tudo. O PRG é só para o caminho de sucesso.
Missão: agradeça pelo nome. Leve o nome no endereço do redirecionamento (
'Location: index.php?enviado=' . urlencode($dados['nome'])) e, na página, mostreObrigada, <?= e($_GET['enviado']) ?>!. Ourlencodeprotege espaços e acentos no endereço; oe()protege a página, porque qualquer um pode digitar?enviado=<script>na barra de endereço.<?php date_default_timezone_set('America/Sao_Paulo'); $assuntos = [ 'duvida' => 'Dúvida', 'pedido' => 'Pedido', 'elogio' => 'Elogio', 'reclamacao' => 'Reclamação', ]; $dados = [ 'nome' => trim($_POST['nome'] ?? ''), 'email' => trim($_POST['email'] ?? ''), 'assunto' => $_POST['assunto'] ?? 'duvida', 'mensagem' => trim($_POST['mensagem'] ?? ''), ]; $erros = []; if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (mb_strlen($dados['nome']) < 2) { $erros['nome'] = 'Escreva o seu nome.'; } if (filter_var($dados['email'], FILTER_VALIDATE_EMAIL) === false) { $erros['email'] = 'Esse e-mail não parece válido.'; } if (!array_key_exists($dados['assunto'], $assuntos)) { $erros['assunto'] = 'Escolha um assunto da lista.'; } $tamanho = mb_strlen($dados['mensagem']); if ($tamanho < 10 || $tamanho > 1000) { $erros['mensagem'] = "A mensagem precisa ter entre 10 e 1000 caracteres (tem $tamanho)."; } if (!$erros) { $linha = [date('Y-m-d H:i:s'), $dados['nome'], $dados['email'], $dados['assunto'], $dados['mensagem']]; $arquivo = fopen(__DIR__ . '/mensagens.csv', 'a'); fputcsv($arquivo, $linha, ',', '"', ''); fclose($arquivo); header('Location: index.php?enviado=1', true, 303); exit; } } function e(string $texto): string { return htmlspecialchars($texto); } ?> <!DOCTYPE html> <html lang="pt-BR"> <head> <meta charset="utf-8"> <title>Fale com a Ana</title> </head> <body> <h1>Fale com a lojinha da Ana</h1> <?php if (isset($_GET['enviado'])): ?> <p class="sucesso">Obrigada! Sua mensagem foi registrada. Responderemos em breve.</p> <?php endif; ?> <form method="post"> <p><label>Nome<br><input name="nome" value="<?= e($dados['nome']) ?>"></label> <?php if (isset($erros['nome'])): ?><br><span class="erro"><?= e($erros['nome']) ?></span><?php endif; ?> </p> <p><label>E-mail<br><input name="email" value="<?= e($dados['email']) ?>"></label> <?php if (isset($erros['email'])): ?><br><span class="erro"><?= e($erros['email']) ?></span><?php endif; ?> </p> <p><label>Assunto<br> <select name="assunto"> <?php foreach ($assuntos as $valor => $texto): ?> <option value="<?= $valor ?>"<?= $valor === $dados['assunto'] ? ' selected' : '' ?>><?= $texto ?></option> <?php endforeach; ?> </select></label> <?php if (isset($erros['assunto'])): ?><br><span class="erro"><?= e($erros['assunto']) ?></span><?php endif; ?> </p> <p><label>Mensagem<br><textarea name="mensagem" rows="5" cols="40"><?= e($dados['mensagem']) ?></textarea></label> <?php if (isset($erros['mensagem'])): ?><br><span class="erro"><?= e($erros['mensagem']) ?></span><?php endif; ?> </p> <p><button>Enviar</button></p> </form> </body> </html><?php date_default_timezone_set('America/Sao_Paulo'); $assuntos = [ 'duvida' => 'Dúvida', 'pedido' => 'Pedido', 'elogio' => 'Elogio', 'reclamacao' => 'Reclamação', ]; $dados = [ 'nome' => trim($_POST['nome'] ?? ''), 'email' => trim($_POST['email'] ?? ''), 'assunto' => $_POST['assunto'] ?? 'duvida', 'mensagem' => trim($_POST['mensagem'] ?? ''), ]; $erros = []; if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (mb_strlen($dados['nome']) < 2) { $erros['nome'] = 'Escreva o seu nome.'; } if (filter_var($dados['email'], FILTER_VALIDATE_EMAIL) === false) { $erros['email'] = 'Esse e-mail não parece válido.'; } if (!array_key_exists($dados['assunto'], $assuntos)) { $erros['assunto'] = 'Escolha um assunto da lista.'; } $tamanho = mb_strlen($dados['mensagem']); if ($tamanho < 10 || $tamanho > 1000) { $erros['mensagem'] = "A mensagem precisa ter entre 10 e 1000 caracteres (tem $tamanho)."; } if (!$erros) { $linha = [date('Y-m-d H:i:s'), $dados['nome'], $dados['email'], $dados['assunto'], $dados['mensagem']]; $arquivo = fopen(__DIR__ . '/mensagens.csv', 'a'); fputcsv($arquivo, $linha, ',', '"', ''); fclose($arquivo); header('Location: index.php?enviado=' . urlencode($dados['nome']), true, 303); exit; } } function e(string $texto): string { return htmlspecialchars($texto); } ?> <!DOCTYPE html> <html lang="pt-BR"> <head> <meta charset="utf-8"> <title>Fale com a Ana</title> </head> <body> <h1>Fale com a lojinha da Ana</h1> <?php if (isset($_GET['enviado'])): ?> <p class="sucesso">Obrigada, <?= e($_GET['enviado']) ?>! Sua mensagem foi registrada. Responderemos em breve.</p> <?php endif; ?> <form method="post"> <p><label>Nome<br><input name="nome" value="<?= e($dados['nome']) ?>"></label> <?php if (isset($erros['nome'])): ?><br><span class="erro"><?= e($erros['nome']) ?></span><?php endif; ?> </p> <p><label>E-mail<br><input name="email" value="<?= e($dados['email']) ?>"></label> <?php if (isset($erros['email'])): ?><br><span class="erro"><?= e($erros['email']) ?></span><?php endif; ?> </p> <p><label>Assunto<br> <select name="assunto"> <?php foreach ($assuntos as $valor => $texto): ?> <option value="<?= $valor ?>"<?= $valor === $dados['assunto'] ? ' selected' : '' ?>><?= $texto ?></option> <?php endforeach; ?> </select></label> <?php if (isset($erros['assunto'])): ?><br><span class="erro"><?= e($erros['assunto']) ?></span><?php endif; ?> </p> <p><label>Mensagem<br><textarea name="mensagem" rows="5" cols="40"><?= e($dados['mensagem']) ?></textarea></label> <?php if (isset($erros['mensagem'])): ?><br><span class="erro"><?= e($erros['mensagem']) ?></span><?php endif; ?> </p> <p><button>Enviar</button></p> </form> </body> </html> -
Etapa 5 · Ler as mensagens recebidas
A Ana precisa ler o que chegou. Um segundo arquivo,
mensagens.php, lê o CSV e monta uma tabela. É aqui que ohtmlspecialcharsfaz a maior diferença: o texto das mensagens foi escrito por qualquer pessoa da internet.<?php $mensagens = []; $caminho = __DIR__ . '/mensagens.csv'; if (file_exists($caminho)) { $arquivo = fopen($caminho, 'r'); while (($linha = fgetcsv($arquivo, null, ',', '"', '')) !== false) { $mensagens[] = $linha; } fclose($arquivo); } $mensagens = array_reverse($mensagens); // as mais novas primeiro ?> <!DOCTYPE html> <html lang="pt-BR"> <head> <meta charset="utf-8"> <title>Mensagens recebidas</title> </head> <body> <h1>Mensagens recebidas (<?= count($mensagens) ?>)</h1> <?php if (!$mensagens): ?> <p>Nenhuma mensagem ainda.</p> <?php else: ?> <table border="1" cellpadding="4"> <tr><th>Quando</th><th>Nome</th><th>E-mail</th><th>Assunto</th><th>Mensagem</th></tr> <?php foreach ($mensagens as [$quando, $nome, $email, $assunto, $texto]): ?> <tr> <td><?= htmlspecialchars($quando) ?></td> <td><?= htmlspecialchars($nome) ?></td> <td><?= htmlspecialchars($email) ?></td> <td><?= htmlspecialchars($assunto) ?></td> <td><?= nl2br(htmlspecialchars($texto)) ?></td> </tr> <?php endforeach; ?> </table> <?php endif; ?> </body> </html>Trecho O que faz file_exists($caminho)Antes da primeira mensagem, o arquivo não existe; sem essa checagem, o fopendaria umWarning.while (($linha = fgetcsv(…)) !== false)Lê uma linha por vez, já separada num array, até o fim do arquivo (quando fgetcsvdevolvefalse).array_reverse($mensagens)O arquivo tem as mais antigas primeiro; invertemos para ver as novas no topo. foreach ($mensagens as [$quando, $nome, …])Desestruturação: cada posição da linha vai direto para uma variável com nome (veja Desestruturação). nl2br(htmlspecialchars($texto))Primeiro escapa; depois troca as quebras de linha por <br />, para a mensagem manter os parágrafos. A ordem importa: ao contrário, o<br />seria escapado e apareceria escrito na tela.Com um CSV que tem uma mensagem "maldosa" (a segunda linha), a página continua inteira:
"2026-10-16 20:50:12","Beto Souza",beto@exemplo.com,pedido,"Vocês entregam no sábado? Queria duas cestas." "2026-10-16 20:52:40","Carla <script>",carla@exemplo.com,duvida,"Testando: <b>negrito</b> e ""aspas"""
$ curl -s localhost:8000/mensagens.php | sed -n '/<h1>/,/<\/table>/p' <h1>Mensagens recebidas (2)</h1> <table border="1" cellpadding="4"> <tr><th>Quando</th><th>Nome</th><th>E-mail</th><th>Assunto</th><th>Mensagem</th></tr> <tr> <td>2026-10-16 20:52:40</td> <td>Carla <script></td> <td>carla@exemplo.com</td> <td>duvida</td> <td>Testando: <b>negrito</b> e "aspas"</td> </tr> <tr> <td>2026-10-16 20:50:12</td> <td>Beto Souza</td> <td>beto@exemplo.com</td> <td>pedido</td> <td>Vocês entregam no sábado? Queria duas cestas.</td> </tr> </table>Esta página não tem senhaDo jeito que está, qualquer um que souber o endereço
/mensagens.phplê as mensagens. Num site de verdade, ela ficaria atrás de um login: é exatamente o que o projeto Login com Sessão ensina. E o arquivomensagens.csvdeveria ficar fora da pasta pública do site, para não ser baixado direto pelo navegador.Missão: esta caixa tem duas abas,
mensagens.phpe omensagens.csvde exemplo. Faça a página aceitar um filtro no endereço: com?assunto=duvida, só aparecem as dúvidas. Antes doarray_reverse, usearray_filter($mensagens, fn($m) => $m[3] === $_GET['assunto'])quandoisset($_GET['assunto']).<?php $mensagens = []; $caminho = __DIR__ . '/mensagens.csv'; if (file_exists($caminho)) { $arquivo = fopen($caminho, 'r'); while (($linha = fgetcsv($arquivo, null, ',', '"', '')) !== false) { $mensagens[] = $linha; } fclose($arquivo); } $mensagens = array_reverse($mensagens); // as mais novas primeiro ?> <!DOCTYPE html> <html lang="pt-BR"> <head> <meta charset="utf-8"> <title>Mensagens recebidas</title> </head> <body> <h1>Mensagens recebidas (<?= count($mensagens) ?>)</h1> <?php if (!$mensagens): ?> <p>Nenhuma mensagem ainda.</p> <?php else: ?> <table border="1" cellpadding="4"> <tr><th>Quando</th><th>Nome</th><th>E-mail</th><th>Assunto</th><th>Mensagem</th></tr> <?php foreach ($mensagens as [$quando, $nome, $email, $assunto, $texto]): ?> <tr> <td><?= htmlspecialchars($quando) ?></td> <td><?= htmlspecialchars($nome) ?></td> <td><?= htmlspecialchars($email) ?></td> <td><?= htmlspecialchars($assunto) ?></td> <td><?= nl2br(htmlspecialchars($texto)) ?></td> </tr> <?php endforeach; ?> </table> <?php endif; ?> </body> </html>"2026-10-16 20:50:12","Beto Souza",beto@exemplo.com,pedido,"Vocês entregam no sábado? Queria duas cestas." "2026-10-16 20:52:40","Carla <script>",carla@exemplo.com,duvida,"Testando: <b>negrito</b> e ""aspas"""
<?php $mensagens = []; $caminho = __DIR__ . '/mensagens.csv'; if (file_exists($caminho)) { $arquivo = fopen($caminho, 'r'); while (($linha = fgetcsv($arquivo, null, ',', '"', '')) !== false) { $mensagens[] = $linha; } fclose($arquivo); } if (isset($_GET['assunto'])) { $mensagens = array_filter($mensagens, fn($m) => $m[3] === $_GET['assunto']); } $mensagens = array_reverse($mensagens); // as mais novas primeiro ?> <!DOCTYPE html> <html lang="pt-BR"> <head> <meta charset="utf-8"> <title>Mensagens recebidas</title> </head> <body> <h1>Mensagens recebidas (<?= count($mensagens) ?>)</h1> <?php if (!$mensagens): ?> <p>Nenhuma mensagem ainda.</p> <?php else: ?> <table border="1" cellpadding="4"> <tr><th>Quando</th><th>Nome</th><th>E-mail</th><th>Assunto</th><th>Mensagem</th></tr> <?php foreach ($mensagens as [$quando, $nome, $email, $assunto, $texto]): ?> <tr> <td><?= htmlspecialchars($quando) ?></td> <td><?= htmlspecialchars($nome) ?></td> <td><?= htmlspecialchars($email) ?></td> <td><?= htmlspecialchars($assunto) ?></td> <td><?= nl2br(htmlspecialchars($texto)) ?></td> </tr> <?php endforeach; ?> </table> <?php endif; ?> </body> </html> -
Etapa 6 (alternativa) · Salvar em JSON em vez de CSV
CSV é ótimo para abrir numa planilha. Se as mensagens forem lidas por outro programa (uma API, um painel em JavaScript), JSON costuma ser mais prático: cada mensagem guarda os nomes dos campos. A troca fica toda numa função:
function salvarMensagem(array $mensagem): void { $caminho = __DIR__ . '/mensagens.json'; $lista = []; if (file_exists($caminho)) { $lista = json_decode(file_get_contents($caminho), true) ?? []; } $lista[] = $mensagem; file_put_contents( $caminho, json_encode($lista, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE), LOCK_EX ); }E, no lugar das quatro linhas do CSV, uma só:
salvarMensagem(['quando' => date('Y-m-d H:i:s')] + $dados);Trecho O que faz json_decode(…, true)Lê o JSON como array associativo (o true). Se o arquivo estiver vazio ou estragado, devolvenull, e o?? []recomeça com uma lista vazia.$lista[] = $mensagemAcrescenta a nova mensagem no fim da lista. JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODEArquivo bonito, com recuo, e acentos escritos como acentos (sem a opção, sábadovirariasábado).LOCK_EXTrava o arquivo enquanto escreve, para dois envios ao mesmo tempo não se atropelarem na escrita. ['quando' => …] + $dadosO +junta dois arrays associativos: a data primeiro, depois os quatro campos.$ curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' -d 'nome=Beto Souza&email=beto@exemplo.com&assunto=pedido&mensagem=Vocês entregam no sábado?' localhost:8000/ 303 http://localhost:8000/index.php?enviado=1 $ curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' -d 'nome=Carla&email=carla@exemplo.com&assunto=elogio&mensagem=Adorei a cesta "grande"!' localhost:8000/ 303 http://localhost:8000/index.php?enviado=1 $ cat mensagens.json [ { "quando": "2026-10-16 20:51:52", "nome": "Beto Souza", "email": "beto@exemplo.com", "assunto": "pedido", "mensagem": "Vocês entregam no sábado?" }, { "quando": "2026-10-16 20:51:52", "nome": "Carla", "email": "carla@exemplo.com", "assunto": "elogio", "mensagem": "Adorei a cesta \"grande\"!" } ](O
-o /dev/null -w '…'faz ocurlmostrar só o código da resposta e o endereço do redirecionamento.)Qual usar? Para uma lojinha, os dois servem. O CSV só acrescenta uma linha (rápido, mesmo com milhares de mensagens); o JSON lê e regrava o arquivo inteiro a cada envio, o que fica lento com muitos dados. Quando o volume crescer, o lugar certo é um banco de dados, como no projeto Tarefas com SQLite.
Quando dá errado
Um arquivo com uma linha em branco antes do <?php e um header('Location: …') logo depois:
$ curl -si localhost:8000/teste.php HTTP/1.1 200 OK Host: localhost:8000 Date: Fri, 16 Oct 2026 23:51:44 GMT Connection: close X-Powered-By: PHP/8.5.8 Content-type: text/html; charset=UTF-8 <br /> <b>Warning</b>: Cannot modify header information - headers already sent by (output started at /home/ana/contato/teste.php:1) in <b>/home/ana/contato/teste.php</b> on line <b>3</b><br />
O que significa: "não posso mudar os cabeçalhos: eles já foram enviados (a saída começou em teste.php:1)". Os cabeçalhos vão antes do conteúdo; a linha em branco já era conteúdo, então o PHP já tinha mandado os cabeçalhos (com 200 OK), e o redirecionamento não aconteceu. Como consertar: o <?php tem que ser a primeiríssima coisa do arquivo, e nenhum echo ou HTML pode vir antes do header(). A mensagem diz exatamente o arquivo e a linha culpados. (Algumas instalações têm output_buffering ligado no php.ini e escondem esse erro; não conte com isso.)
Aparece no PHP 8.4+ quando você chama fputcsv($arquivo, $linha) só com dois argumentos (o mesmo vale para fgetcsv). O que significa: "informe o parâmetro $escape, porque o valor padrão dele vai mudar". Como consertar: passe os três: fputcsv($arquivo, $linha, ',', '"', ''). O escape vazio segue o padrão CSV de verdade (aspas dobradas).
Alguém enviou nome[]=Ana (com colchetes), e o PHP transformou o campo num array:
$ curl -s -d 'nome[]=Ana' localhost:8000/index.php | head -8
<br />
<b>Fatal error</b>: Uncaught TypeError: trim(): Argument #1 ($string) must be of type string, array given in /home/ana/contato/index.php:12
Stack trace:
#0 /home/ana/contato/index.php(12): trim(Array)
#1 {main}
thrown in <b>/home/ana/contato/index.php</b> on line <b>12</b><br />
Não é um furo de segurança (a página só parou), mas é feio. Como consertar: leia o campo só se for texto: is_string($_POST['nome'] ?? null) ? trim($_POST['nome']) : ''. É um dos desafios do fim.
No laboratório, tiramos a permissão de escrita de uma pasta (chmod a-w trancada) e enviamos uma mensagem válida para o index.php dela:
$ curl -s -d 'nome=Beto&email=beto@exemplo.com&assunto=pedido&mensagem=Entregam no sábado?' localhost:8000/trancada/index.php | head -12
<br />
<b>Warning</b>: fopen(/home/ana/contato/trancada/mensagens.csv): Failed to open stream: Permission denied in <b>/home/ana/contato/trancada/index.php</b> on line <b>37</b><br />
<br />
<b>Fatal error</b>: Uncaught TypeError: fputcsv(): Argument #1 ($stream) must be of type resource, false given in /home/ana/contato/trancada/index.php:38
Stack trace:
#0 /home/ana/contato/trancada/index.php(38): fputcsv(false, Array, ',', '"', '')
#1 {main}
thrown in <b>/home/ana/contato/trancada/index.php</b> on line <b>38</b><br />
Dois erros em cadeia: o fopen não conseguiu abrir ("permissão negada") e devolveu false; o fputcsv recebeu esse false no lugar do arquivo e parou tudo. Num servidor de verdade isso acontece porque o PHP roda como outro usuário (por exemplo www-data), que não pode escrever na pasta do site. Como consertar: dê permissão de escrita só na pasta dos dados para esse usuário, de preferência uma pasta fora da pública. Com o php -S isso não acontece, porque ele roda como você.
Faltou o fuso horário: sem date_default_timezone_set nem date.timezone no php.ini, o PHP usa UTC. Veja Datas e Horas.
O código completo
O index.php final (versão CSV, com Post/Redirect/Get):
<?php
date_default_timezone_set('America/Sao_Paulo');
$assuntos = [
'duvida' => 'Dúvida',
'pedido' => 'Pedido',
'elogio' => 'Elogio',
'reclamacao' => 'Reclamação',
];
$dados = [
'nome' => trim($_POST['nome'] ?? ''),
'email' => trim($_POST['email'] ?? ''),
'assunto' => $_POST['assunto'] ?? 'duvida',
'mensagem' => trim($_POST['mensagem'] ?? ''),
];
$erros = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (mb_strlen($dados['nome']) < 2) {
$erros['nome'] = 'Escreva o seu nome.';
}
if (filter_var($dados['email'], FILTER_VALIDATE_EMAIL) === false) {
$erros['email'] = 'Esse e-mail não parece válido.';
}
if (!array_key_exists($dados['assunto'], $assuntos)) {
$erros['assunto'] = 'Escolha um assunto da lista.';
}
$tamanho = mb_strlen($dados['mensagem']);
if ($tamanho < 10 || $tamanho > 1000) {
$erros['mensagem'] = "A mensagem precisa ter entre 10 e 1000 caracteres (tem $tamanho).";
}
if (!$erros) {
$linha = [date('Y-m-d H:i:s'), $dados['nome'], $dados['email'], $dados['assunto'], $dados['mensagem']];
$arquivo = fopen(__DIR__ . '/mensagens.csv', 'a');
fputcsv($arquivo, $linha, ',', '"', '');
fclose($arquivo);
header('Location: index.php?enviado=1', true, 303);
exit;
}
}
function e(string $texto): string
{
return htmlspecialchars($texto);
}
?>
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="utf-8">
<title>Fale com a Ana</title>
</head>
<body>
<h1>Fale com a lojinha da Ana</h1>
<?php if (isset($_GET['enviado'])): ?>
<p class="sucesso">Obrigada! Sua mensagem foi registrada. Responderemos em breve.</p>
<?php endif; ?>
<form method="post">
<p><label>Nome<br><input name="nome" value="<?= e($dados['nome']) ?>"></label>
<?php if (isset($erros['nome'])): ?><br><span class="erro"><?= e($erros['nome']) ?></span><?php endif; ?>
</p>
<p><label>E-mail<br><input name="email" value="<?= e($dados['email']) ?>"></label>
<?php if (isset($erros['email'])): ?><br><span class="erro"><?= e($erros['email']) ?></span><?php endif; ?>
</p>
<p><label>Assunto<br>
<select name="assunto">
<?php foreach ($assuntos as $valor => $texto): ?>
<option value="<?= $valor ?>"<?= $valor === $dados['assunto'] ? ' selected' : '' ?>><?= $texto ?></option>
<?php endforeach; ?>
</select></label>
<?php if (isset($erros['assunto'])): ?><br><span class="erro"><?= e($erros['assunto']) ?></span><?php endif; ?>
</p>
<p><label>Mensagem<br><textarea name="mensagem" rows="5" cols="40"><?= e($dados['mensagem']) ?></textarea></label>
<?php if (isset($erros['mensagem'])): ?><br><span class="erro"><?= e($erros['mensagem']) ?></span><?php endif; ?>
</p>
<p><button>Enviar</button></p>
</form>
</body>
</html>
E o mensagens.php é o da etapa 5, sem mudanças.
Desafios extras
- Campos à prova de array: uma função
campo(string $nome): stringque devolvetrimdo campo se ele for texto e''em qualquer outro caso. Use-a para montar$dados. - Pote de mel (honeypot) contra robôs: um campo escondido com CSS,
<input name="site">. Pessoas não o veem; robôs preenchem tudo. Sesitevier preenchido, redirecione como se tivesse dado certo, mas não salve. - Limite de envios: no máximo uma mensagem por minuto por pessoa, usando uma sessão (veja Sessões).
- Contador no topo: no
mensagens.php, mostre quantas mensagens há de cada assunto (dica:array_count_values(array_column($mensagens, 3))). - Mover os dados: guarde o CSV numa pasta
dados/fora da pasta pública e ajuste o caminho com__DIR__ . '/../dados/mensagens.csv'.
Missão final: as mensagens agora estão em mensagens.json (aba ao lado, gerado pela etapa 6). Mude o mensagens.php para ler o JSON: troque o bloco do fopen/fgetcsv por json_decode(file_get_contents($caminho), true) ?? [], e o foreach para desestruturar pelas chaves: foreach ($mensagens as ['quando' => $quando, 'nome' => $nome, …]).
<?php
$mensagens = [];
$caminho = __DIR__ . '/mensagens.csv';
if (file_exists($caminho)) {
$arquivo = fopen($caminho, 'r');
while (($linha = fgetcsv($arquivo, null, ',', '"', '')) !== false) {
$mensagens[] = $linha;
}
fclose($arquivo);
}
$mensagens = array_reverse($mensagens); // as mais novas primeiro
?>
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="utf-8">
<title>Mensagens recebidas</title>
</head>
<body>
<h1>Mensagens recebidas (<?= count($mensagens) ?>)</h1>
<?php if (!$mensagens): ?>
<p>Nenhuma mensagem ainda.</p>
<?php else: ?>
<table border="1" cellpadding="4">
<tr><th>Quando</th><th>Nome</th><th>E-mail</th><th>Assunto</th><th>Mensagem</th></tr>
<?php foreach ($mensagens as [$quando, $nome, $email, $assunto, $texto]): ?>
<tr>
<td><?= htmlspecialchars($quando) ?></td>
<td><?= htmlspecialchars($nome) ?></td>
<td><?= htmlspecialchars($email) ?></td>
<td><?= htmlspecialchars($assunto) ?></td>
<td><?= nl2br(htmlspecialchars($texto)) ?></td>
</tr>
<?php endforeach; ?>
</table>
<?php endif; ?>
</body>
</html>
[
{
"quando": "2026-10-16 20:51:52",
"nome": "Beto Souza",
"email": "beto@exemplo.com",
"assunto": "pedido",
"mensagem": "Vocês entregam no sábado?"
},
{
"quando": "2026-10-16 20:51:52",
"nome": "Carla",
"email": "carla@exemplo.com",
"assunto": "elogio",
"mensagem": "Adorei a cesta \"grande\"!"
}
]
<?php
$mensagens = [];
$caminho = __DIR__ . '/mensagens.json';
if (file_exists($caminho)) {
$mensagens = json_decode(file_get_contents($caminho), true) ?? [];
}
$mensagens = array_reverse($mensagens); // as mais novas primeiro
?>
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="utf-8">
<title>Mensagens recebidas</title>
</head>
<body>
<h1>Mensagens recebidas (<?= count($mensagens) ?>)</h1>
<?php if (!$mensagens): ?>
<p>Nenhuma mensagem ainda.</p>
<?php else: ?>
<table border="1" cellpadding="4">
<tr><th>Quando</th><th>Nome</th><th>E-mail</th><th>Assunto</th><th>Mensagem</th></tr>
<?php foreach ($mensagens as ['quando' => $quando, 'nome' => $nome, 'email' => $email, 'assunto' => $assunto, 'mensagem' => $texto]): ?>
<tr>
<td><?= htmlspecialchars($quando) ?></td>
<td><?= htmlspecialchars($nome) ?></td>
<td><?= htmlspecialchars($email) ?></td>
<td><?= htmlspecialchars($assunto) ?></td>
<td><?= nl2br(htmlspecialchars($texto)) ?></td>
</tr>
<?php endforeach; ?>
</table>
<?php endif; ?>
</body>
</html>
Exercícios
Complete o redirecionamento do Post/Redirect/Get e a linha que impede o resto do script de rodar:
Complete o modo do fopen que escreve no fim do arquivo sem apagar o que já existe:
Por que validamos o e-mail com filter_var no PHP, se o campo pode ter type="email" no HTML?
Depois de enviar o formulário (sem PRG), a pessoa aperta F5 na página de "Obrigada". O que acontece?
Qual a ordem certa para mostrar uma mensagem com quebras de linha?
Resumo
$dadosguarda os campos (comtrime??);$errosguarda as mensagens com o nome do campo como chave.- Valide no servidor:
mb_strlenpara tamanhos,filter_var(…, FILTER_VALIDATE_EMAIL)para e-mail, lista de valores permitidos para oselect. - Todo valor devolvido para a página passa por
htmlspecialchars(aqui, a funçãoe()): emvalue, dentro dotextareae na listagem. fputcsv($f, $linha, ',', '"', '')escreve CSV corretamente; JSON comjson_encode(…, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)eLOCK_EX.- Post/Redirect/Get: depois de salvar,
header('Location: …', true, 303); exit;. Nada pode ser impresso antes doheader().
Próximo projeto: Login com Sessão, para proteger páginas como o mensagens.php.