Capítulo 1 — Do site à primeira resposta do servidor
Cliente e servidor, HTTP, ambiente PHP e a primeira página dinâmica do Café Aurora.
O pedido chegou ao navegador, mas não chegou ao café
No Web I, o Café Aurora ganhou um site completo. A pessoa pode consultar o cardápio, filtrar produtos e montar uma seleção. Porém, essa seleção permanece no próprio navegador: a proprietária não recebe o pedido e não consegue informar, de um único lugar, se a cozinha ainda está aceitando encomendas.
Imagine uma manhã movimentada. Às 10h, a proprietária precisa interromper novos pedidos por vinte minutos. Alterar um arquivo HTML, publicar novamente e esperar a atualização não é uma boa operação para o dia a dia. O site precisa consultar uma decisão tomada pelo negócio e devolver uma página adequada naquele momento.
É aqui que o Web II começa: o navegador continua exibindo HTML, CSS e JavaScript, mas uma aplicação no servidor passa a receber solicitações, executar regras e preparar respostas. Neste capítulo, ainda não salvaremos pedidos em banco de dados. Primeiro, vamos compreender e construir esse caminho.
O que você vai aprender
Distinguir página estática, página dinâmica, cliente e servidor.
Acompanhar uma solicitação e uma resposta HTTP.
Preparar um ambiente local com PHP e confirmar a instalação.
Organizar o projeto com uma pasta pública.
Executar o servidor de desenvolvimento do PHP.
Usar variáveis, tipos básicos, echo e uma decisão com if.
Combinar PHP e HTML sem perder a estrutura construída no Web I.
Verificar sintaxe e observar a resposta no DevTools.
Aula 1 — O que muda quando o site vira sistema
Entender: duas páginas podem parecer iguais e funcionar de modos diferentes
Quando alguém acessa
Página estática
Página dinâmica
O servidor faz o quê?
Entrega um arquivo que já estava pronto.
Executa código e produz a resposta.
O conteúdo pode depender de uma regra?
Não sem alterar o arquivo ou usar código no navegador.
Sim. A aplicação decide antes de responder.
O navegador recebe PHP?
Não há PHP.
Não. Ele recebe o resultado, normalmente HTML.
Exemplo no Café Aurora
História e endereço do café.
Status atual dos pedidos e, futuramente, pedidos salvos.
Dinâmica não significa apenas ter animações. Um filtro feito com JavaScript no navegador é interativo, mas não dá à proprietária uma visão central dos pedidos. Neste curso, uma página dinâmica no servidor é aquela cuja resposta pode ser construída por uma aplicação conforme dados e regras.
O caminho da primeira página PHP
1. Navegador pede http://localhost:8000/
↓ solicitação HTTP
2. Servidor local encontra public/index.php
↓ executa o código PHP
3. PHP prepara um documento HTML
↓ resposta HTTP
4. Navegador interpreta e mostra o HTML
Cliente e servidor são papéis.
Neste exemplo, o navegador é o cliente porque inicia a solicitação. O programa PHP atende no servidor. Durante o desenvolvimento, os dois rodam no mesmo computador, mas continuam desempenhando papéis diferentes.
HTTP em linguagem simples
URL
Indica qual recurso o cliente deseja acessar.
Requisição
Mensagem enviada pelo cliente ao servidor.
Resposta
Mensagem devolvida com status, cabeçalhos e conteúdo.
Status 200
Indica que a solicitação foi atendida com sucesso.
Experimente antes de programar
Abra o site publicado do Café Aurora ou outra página do projeto.
Abra o DevTools com F12 e selecione Network.
Recarregue a página e selecione a primeira linha do tipo document.
Localize o endereço solicitado, o método GET e o código de status.
Você não precisa memorizar todos os campos. A meta é reconhecer que abrir uma página produz uma solicitação e uma resposta observáveis.
Aula 2 — Preparando o ambiente PHP
No Web I, o navegador conseguia abrir os arquivos diretamente. Agora precisamos de um programa que leia index.php, execute as instruções e entregue apenas o resultado. Usaremos o PHP 8.5 e seu servidor embutido para desenvolvimento local.
O servidor embutido é uma bancada de aprendizagem.
Ele é adequado para desenvolver e testar no próprio computador. Não é um servidor completo de produção e não deve ser exposto à Internet.
Na versão 8.5, escolha o pacote x64 Non Thread Safe em ZIP. Essa opção é adequada ao uso pela linha de comando deste curso.
Extraia o conteúdo para uma pasta simples, por exemplo C:\php.
Adicione C:\php à variável de usuário Path e reinicie o VS Code.
Se o Windows informar que falta um componente de execução, instale o Visual C++ Redistributable 2015–2022 x64 indicado na própria página do PHP.
Em macOS ou Linux, instale uma versão 8.5 suportada pelo gerenciador de pacotes do sistema e faça a mesma conferência abaixo. Em laboratório, siga o procedimento definido pelo responsável pelos computadores.
Confirme no terminal do VS Code
Abra Terminal → Novo Terminal e execute:
php -v
A primeira linha deve informar PHP 8.5. O número depois de 8.5 pode ser diferente, pois correções são publicadas ao longo do tempo.
O comando não foi reconhecido?
Confirme se php.exe está dentro de C:\php, revise o Path e feche completamente o VS Code antes de abri-lo outra vez. Digitar apenas o caminho no ajuste de validação do editor não torna o comando disponível no terminal.
Validação do editor
O VS Code já oferece suporte básico a PHP e usa o executável para verificar sintaxe. Se o PHP estiver instalado, mas o editor não o localizar, abra as configurações em JSON e informe:
"php.validate.executablePath": "C:/php/php.exe"
Checkpoint do ambiente
Antes de avançar, registre a linha da versão apresentada por php -v. Se ela não aparecer, resolva a instalação agora: os próximos passos dependem desse comando.
Aula 3 — Reorganizando o Café Aurora e iniciando o servidor
Faça uma cópia da versão final criada no Web I e nomeie a nova pasta como cafe-aurora-web2. Assim, a versão publicada continua preservada enquanto o sistema evolui.
Crie uma fronteira pública
Dentro da nova pasta, crie public e mova para ela as páginas, imagens, estilos, scripts e dados do site. O arquivo de orientações pode permanecer na raiz. Depois, renomeie index.html para index.php e atualize os links que ainda apontam para index.html.
Ela marca o que o servidor pode entregar diretamente ao navegador. Nos próximos capítulos, configurações e classes poderão ficar fora dessa fronteira. Essa organização reduz confusão agora e prepara uma estrutura mais segura.
Inicie o servidor na raiz do projeto
php -S localhost:8000 -t public
php inicia o interpretador.
-S solicita o servidor de desenvolvimento.
localhost:8000 define o endereço e a porta.
-t public define a pasta pública do projeto.
Mantenha esse terminal aberto e visite http://localhost:8000. Para encerrar o servidor, volte ao terminal e pressione Ctrl + C.
Abrir o arquivo não é executar PHP.
Um endereço iniciado por file:/// abre o arquivo diretamente e ignora o servidor. Durante o Web II, entre pelo endereço http://localhost:8000.
Experimente uma falha controlada
Com o servidor funcionando, confirme que a página abre.
Encerre o processo com Ctrl + C e recarregue o navegador.
Explique por que os arquivos continuam no computador, mas o endereço deixa de responder.
Inicie o servidor novamente.
Aula 4 — A primeira resposta dinâmica do Café Aurora
Agora a proprietária precisa comunicar três informações: o nome do negócio, se os pedidos estão abertos e o tempo estimado de preparo. Em vez de repetir textos pelo HTML, vamos representar esses dados no início de public/index.php.
A aplicação deve preparar uma mensagem para cada situação. Acrescente antes do HTML:
if ($pedidosAbertos) {
$mensagemStatus = "Pedidos para retirada estão abertos.";
$mensagemTempo = "Tempo estimado: $tempoEstimado minutos.";
} else {
$mensagemStatus = "Pedidos para retirada estão encerrados.";
$mensagemTempo = "Consulte novamente durante o horário de atendimento.";
}
?>
O bloco do if é executado quando a condição vale true. O bloco do else cobre a outra situação. As chaves delimitam cada bloco.
Experimente a regra
Salve com $pedidosAbertos = true, recarregue a página e anote a mensagem. Depois troque somente para false. A interface deverá mudar sem você reescrever os parágrafos no HTML.
Mostre os valores dentro do HTML
Substitua o texto fixo do título principal e acrescente a nova seção de atendimento:
A forma <?= ... ?> imprime um valor no ponto exato do documento. Ela é uma forma curta de usar echo. Os valores deste exemplo foram definidos pelo próprio programa; dados recebidos de pessoas exigirão validação e saída segura, assunto dos próximos capítulos.
Código completo para conferência — public/index.php
Aula 5 — Conferindo o que o servidor realmente entregou
Verifique a sintaxe sem abrir o navegador
Em outro terminal, ainda na raiz do projeto, execute:
php -l public/index.php
A resposta esperada informa que nenhum erro de sintaxe foi detectado. Esse teste encontra problemas de escrita da linguagem, como um ponto e vírgula ausente; ele não prova que a regra de negócio está correta.
Observe a resposta no navegador
Com o servidor ativo, acesse http://localhost:8000.
Use Ver código-fonte da página pelo menu do navegador.
Procure $pedidosAbertos e depois procure a mensagem apresentada na tela.
A variável não aparece, mas o texto produzido aparece. Isso comprova uma ideia central: o PHP foi executado no servidor; o navegador recebeu o HTML resultante.
Volte ao Network
No DevTools, selecione a requisição do documento e observe:
Headers: endereço, método GET e status 200;
Response: o HTML devolvido pelo servidor;
Initiator: a navegação que iniciou a solicitação.
Não confunda código-fonte com painel Elements.
O código-fonte mostra a resposta original recebida. O painel Elements mostra o documento depois que o navegador o interpretou e depois de possíveis alterações feitas por JavaScript.
Aplicar: faça uma mudança completa
Altere o tempo estimado para 40 minutos.
Verifique a sintaxe com php -l.
Recarregue a página e confira a interface.
Confirme no código-fonte que o navegador recebeu o novo texto, mas não recebeu a variável PHP.
Verifique sua aprendizagem
Explico por que o Café Aurora precisa de código no servidor.
Diferencio cliente, servidor, requisição e resposta.
Confirmo a instalação com php -v.
Sei por que o projeto possui uma pasta public.
Inicio e encerro o servidor local conscientemente.
Uso variáveis com texto, booleano e número inteiro.
Uso if e else para representar uma regra simples.
Confirmo que o navegador recebe HTML, não o código PHP.
Transfira o aprendizado
Uma pequena clínica precisa mostrar se ainda aceita encaixes no dia e o tempo médio de espera. Sem criar outro site, escreva três variáveis adequadas, indique o tipo de cada uma e monte apenas a decisão if/else que produziria as duas mensagens. Depois explique por que essa informação faz mais sentido no servidor do que fixada no HTML.
O status ainda nasce em uma variável escrita no arquivo. Nos próximos capítulos, o sistema receberá dados por URL e formulário, manterá informações entre solicitações e chegará ao banco de dados. Cada recurso entrará quando uma necessidade real do Café Aurora o justificar.
Web II • 8 horas
Capítulo 2 — A URL conversa com o servidor
Query String, método GET, $_GET, valores padrão e validação por lista permitida.
Um filtro útil que ninguém consegue compartilhar
O cardápio do Café Aurora já possui botões para mostrar bebidas ou comidas. Uma cliente filtra somente as bebidas e quer enviar aquela seleção a uma amiga. Porém, ao copiar o endereço, as duas recebem a página completa: o filtro aconteceu apenas naquele navegador e não passou a fazer parte da URL.
Para uma campanha do café, a proprietária também gostaria de publicar um endereço que já abrisse os alimentos: cardapio.php?categoria=comida. O servidor precisa ler essa informação, verificar se ela é aceitável e devolver o cardápio correspondente.
Neste capítulo, a URL deixará de ser apenas um endereço fixo. Ela passará a transportar um pedido de consulta. Isso prepara o caminho para buscas e formulários sem misturar ainda alteração de dados ou banco de dados.
O que você vai aprender
Reconhecer caminho, Query String, parâmetro, nome e valor.
Entender quando o método GET é adequado.
Ler um parâmetro com a superglobal $_GET.
Definir um valor padrão com o operador ??.
Validar uma categoria usando array e in_array().
Construir um formulário de consulta com method="get".
Renderizar somente os produtos solicitados.
Observar a nova requisição no painel Network.
Aula 1 — Lendo a intenção escrita na URL
Entender: decomponha o endereço
http://localhost:8000/cardapio.php?categoria=bebida
└────── origem ──────┘└── caminho ──┘└── Query String ──┘
nome valor
O sinal ? inicia a Query String.
categoria é o nome do parâmetro.
bebida é o valor enviado.
Quando há mais parâmetros, o caractere & separa cada par.
Endereço
Pedido feito ao servidor
cardapio.php
Mostrar a categoria padrão.
cardapio.php?categoria=bebida
Mostrar bebidas.
cardapio.php?categoria=comida
Mostrar comidas.
cardapio.php?categoria=inventada
Valor inválido: a aplicação deverá recuperar um padrão seguro.
Quando usar GET
GET é adequado para consultar algo sem alterar o estado do sistema. O resultado pode ser recarregado, favoritado e compartilhado. Filtros e buscas são bons exemplos.
A URL não é lugar para segredos.
A Query String pode aparecer no histórico, em registros do servidor e ao copiar o endereço. Nunca envie senha, número de cartão ou outro dado sensível dessa forma. Usar POST depois não eliminará, sozinho, a necessidade de HTTPS e proteção dos dados.
Experimente
Com o servidor local ativo, digite manualmente os quatro endereços da tabela. Por enquanto, a página ainda será igual. Registre o que aparece depois de ? em cada teste e explique por que o navegador já consegue enviar a informação mesmo antes de programarmos sua leitura.
Aula 2 — Recebendo e validando a categoria
Renomeie public/cardapio.html para public/cardapio.php. Depois, atualize em todas as páginas os links que ainda apontam para o nome antigo. O conteúdo visual continua válido; a nova extensão permite que o servidor execute PHP antes de responder.
Leia o valor recebido
No início de cardapio.php, antes do DOCTYPE, escreva:
Array especial com os parâmetros recebidos pela Query String.
["categoria"]
Acessa o valor associado a esse nome.
??
Usa o valor da esquerda quando ele existe; caso contrário, usa o padrão da direita.
"todos"
Evita erro e mantém um comportamento previsível quando não há parâmetro.
Receber não significa confiar
Qualquer pessoa pode alterar a URL. Portanto, não basta verificar se o parâmetro existe. Defina os únicos valores aceitos e recupere o padrão se chegar algo diferente:
! inverte o resultado: o bloco executa quando o valor não está na lista.
O terceiro argumento true exige comparação estrita de valor e tipo.
Validação e escape resolvem problemas diferentes.
A validação decide se o dado pertence ao conjunto aceito pela regra. Quando um texto livre recebido de alguém precisar aparecer no HTML, também será necessário tratá-lo para aquele contexto, por exemplo com htmlspecialchars(). Aqui a categoria só continua depois de pertencer à lista permitida.
Teste os limites
Experimente bebida, BEBIDA, um valor vazio e inventada. Preveja o resultado antes de recarregar. A aplicação aceita somente os valores definidos exatamente.
Aula 3 — Criando uma consulta que qualquer pessoa consegue usar
Digitar parâmetros manualmente ajuda a compreender o mecanismo, mas não é uma interface adequada para clientes. Acrescente antes da grade de produtos:
method="get" coloca os controles bem-sucedidos na Query String.
action="cardapio.php" indica quem receberá a consulta.
O atributo name fornece o nome do parâmetro; value, seu valor.
=== compara valor e tipo sem conversões automáticas.
selected mantém visível a opção usada na resposta atual.
Renderize somente o grupo pedido
Envolva o card do café coado com a condição de bebida:
<?php if ($categoria === "todos" || $categoria === "bebida"): ?>
<article data-categoria="bebida">
<!-- Preserve aqui o card completo do café coado. -->
</article>
<?php endif; ?>
Nos dois cards de alimento, use a mesma estrutura trocando bebida por comida. O operador || significa “ou”: o card aparece quando a consulta pede todos ou sua categoria específica. A forma com dois-pontos e endif facilita reconhecer onde começa e termina uma condição misturada ao HTML.
Código completo para conferência — controle do cardápio
<?php
$categoria = $_GET["categoria"] ?? "todos";
$categoriasPermitidas = ["todos", "bebida", "comida"];
if (!in_array($categoria, $categoriasPermitidas, true)) {
$categoria = "todos";
}
?>
<form method="get" action="cardapio.php">
<label for="categoria">Categoria</label>
<select id="categoria" name="categoria">
<option value="todos"<?php if ($categoria === "todos") { echo " selected"; } ?>>Todos</option>
<option value="bebida"<?php if ($categoria === "bebida") { echo " selected"; } ?>>Bebidas</option>
<option value="comida"<?php if ($categoria === "comida") { echo " selected"; } ?>>Comidas</option>
</select>
<button type="submit">Consultar no servidor</button>
</form>
<div class="grade-produtos" id="lista-produtos">
<?php if ($categoria === "todos" || $categoria === "bebida"): ?>
<article data-categoria="bebida">
<h3>Café coado</h3>
<img src="img/cafe-coado.webp" alt="Xícara de café coado ao lado de uma pequena jarra" width="960" height="640" loading="lazy">
<p>Preparado na hora com grãos selecionados. <strong>R$ 8,00</strong></p>
</article>
<?php endif; ?>
<?php if ($categoria === "todos" || $categoria === "comida"): ?>
<article data-categoria="comida">
<h3>Pão artesanal</h3>
<img src="img/pao-artesanal.webp" alt="Pão artesanal cortado sobre uma mesa de madeira" width="960" height="640" loading="lazy">
<p>Fermentação lenta e produção diária. <strong>R$ 12,00</strong></p>
</article>
<article data-categoria="comida">
<h3>Bolo caseiro de laranja</h3>
<img src="img/bolo-caseiro.webp" alt="Bolo caseiro de laranja com uma fatia servida" width="960" height="640" loading="lazy">
<p>Receita da casa com laranjas frescas. <strong>R$ 9,00 a fatia</strong></p>
</article>
<?php endif; ?>
</div>
Aula 4 — Servidor e navegador trabalhando sem competir
O Café Aurora já possuía um filtro rápido em JavaScript. Agora existem duas camadas úteis: o servidor entrega a categoria solicitada na URL; depois que a página chega, o JavaScript pode permitir novas trocas instantâneas naquele navegador.
Ação
Onde acontece
Produz nova requisição?
Pode ser compartilhada?
Enviar o formulário GET
Servidor PHP
Sim
Sim, pela URL resultante.
Clicar no filtro rápido
JavaScript no navegador
Não
Não, enquanto a URL não mudar.
No checkpoint, o JavaScript preserva a categoria inicial recebida do servidor quando atualiza os produtos pelo JSON. Isso mantém a melhoria do Web I sem apagar o comportamento novo do Web II.
Entregue a categoria inicial ao JavaScript
No elemento body de cardapio.php, registre a categoria que já foi validada:
<body data-categoria-inicial="<?= $categoria ?>">
No início de js/script.js, leia esse atributo e substitua a chamada inicial filtrarProdutos("todos"):
data-categoria-inicial transporta para o navegador um valor já aprovado pelo servidor. A propriedade dataset.categoriaInicial lê esse valor; String() converte o resultado booleano para o texto esperado por aria-pressed.
Veja as duas requisições no Network
Abra Network e limpe a lista.
Escolha Bebidas no formulário e envie.
Localize o documento cardapio.php?categoria=bebida.
Depois, clique em Comidas no filtro rápido e confirme que não surgiu outro documento.
Aplicar: produza um endereço verificável
Gere a consulta de comidas pelo formulário, copie a URL, abra-a em uma janela privativa e confirme que o primeiro HTML já contém apenas os alimentos. Depois teste uma categoria inválida e comprove que o sistema volta a “todos”.
Verifique sua aprendizagem
Identifico a Query String e seus pares de nome e valor.
Escolho GET para consultas que podem ser repetidas e compartilhadas.
Leio um parâmetro com $_GET sem gerar aviso quando ele não existe.
Defino e explico um valor padrão com ??.
Não confio em uma categoria apenas porque ela chegou pela interface criada por mim.
Uso uma lista permitida para limitar os valores aceitos.
Relaciono name e value aos dados produzidos pelo formulário.
Distingo filtro executado no servidor e filtro executado no navegador.
Transfira o aprendizado
Uma oficina de bicicletas quer compartilhar listas por tipo de serviço: revisão, freio ou pneu. Sem criar outro sistema, escreva três URLs possíveis, defina o array de valores permitidos e explique o que a aplicação deverá fazer com ?servico=motor. Não use dados pessoais no endereço.
GET consulta e torna o estado visível na URL. Para enviar o formulário de contato e criar um pedido, precisaremos trabalhar com POST, validar textos livres e produzir mensagens de sucesso ou erro sem duplicar operações.
Web II • 8 horas
Capítulo 3 — O formulário chega ao servidor
Método POST, $_POST, validação no servidor, escape de saída e respostas acessíveis.
A cliente preencheu tudo. O café recebeu alguma coisa?
Uma cliente quer encomendar vinte pães para uma reunião. Ela encontra o formulário do Café Aurora, informa nome, e-mail, assunto e mensagem e pressiona “Enviar”. Até agora, o navegador tenta acessar um endereço que não existe. O formulário parece pronto, mas ninguém do café consegue receber nem conferir os dados.
A validação do próprio HTML ajuda quem está preenchendo, porém alguém pode alterá-la no DevTools ou enviar uma requisição sem usar a nossa página. O servidor precisa tratar cada valor como informação ainda não confiável, aplicar as regras do negócio e devolver uma resposta clara.
Neste capítulo, o PHP receberá e validará a mensagem. Para manter cada aprendizagem visível, ainda não enviaremos e-mail nem gravaremos dados: uma confirmação significará somente que os dados passaram pelas regras desta demonstração.
O que você vai aprender
Distinguir GET e POST conforme a intenção da operação.
Relacionar method, action e name ao envio do formulário.
Detectar uma requisição POST e ler campos com $_POST.
Normalizar textos com trim().
Validar presença, tamanho, e-mail e opções permitidas no servidor.
Escapar valores apresentados no HTML com htmlspecialchars().
Preservar os dados válidos e mostrar erros associados aos campos.
Investigar o envio no painel Network do DevTools.
Aula 1 — Escolhendo POST pela intenção
Entender: consultar não é o mesmo que entregar uma mensagem
Situação
Método
Onde os dados seguem
Exemplo
Consultar sem alterar o sistema
GET
Normalmente na URL
Filtrar o cardápio.
Enviar dados para processamento
POST
No corpo da requisição
Entregar uma mensagem de contato.
POST evita colocar o conteúdo do formulário na URL e expressa que estamos solicitando um processamento. Entretanto, POST não criptografa dados. Em produção, HTTPS continua necessário, e o formulário não deve pedir informações que o negócio não precisa.
Prepare a página que receberá os dados
Renomeie public/contato.html para public/contato.php.
Atualize em todas as páginas os links que ainda apontam para contato.html.
No formulário, troque o destino por action="contato.php" e preserve method="post".
action identifica o recurso que processará os dados. method define como a requisição será feita. Cada controle só ganha um par no envio quando possui name.
Experimente antes de programar
Envie o formulário e observe Network. Localize o documento contato.php, o método POST e a seção Payload ou Carga útil. Use dados fictícios. A página ainda não responderá de forma especial, mas o navegador já terá transportado os pares de nome e valor.
Aula 2 — Recebendo sem confiar
No início de contato.php, descubra se a página foi aberta normalmente ou recebeu um envio:
$_SERVER["REQUEST_METHOD"] informa o método HTTP usado. A comparação estrita produz true apenas para POST. As outras variáveis guardarão o resultado da validação.
Leia e normalize os campos
Como cada controle deve produzir um único texto, crie uma função que também recuse formatos inesperados:
$_POST reúne os pares enviados por este formulário POST.
?? "" fornece texto vazio quando um campo não chegou.
is_string() confirma que chegou um único texto, como o formulário espera.
trim() remove espaços do início e do fim; não corrige nem valida o conteúdo.
O navegador ajuda; o servidor decide.
required, type="email" e limites de tamanho oferecem retorno imediato, mas não são uma barreira de segurança. As mesmas regras relevantes precisam existir no PHP.
Experimento no Elements
Inspecione o campo de mensagem.
Apague temporariamente required e minlength="10" no painel Elements.
Tente enviar uma mensagem vazia.
Recarregue a página e confirme que os atributos voltaram: a alteração no DevTools não modificou o arquivo.
Esse teste demonstra por que o servidor não pode depender somente das restrições do HTML.
Aula 3 — Aplicando regras claras
O café aceita três assuntos e precisa de informações suficientes para responder. Defina as opções no PHP e valide apenas quando houver envio:
$assuntosPermitidos = [
"duvida" => "Dúvida",
"encomenda" => "Encomenda",
"sugestao" => "Sugestão"
];
if ($formularioEnviado) {
if (tamanhoTexto($nome) < 2) {
$erros["nome"] = "Informe um nome com pelo menos 2 caracteres.";
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$erros["email"] = "Informe um endereço de e-mail válido.";
}
if (!array_key_exists($assunto, $assuntosPermitidos)) {
$erros["assunto"] = "Escolha um assunto disponível.";
}
$tamanhoMensagem = tamanhoTexto($mensagem);
if ($tamanhoMensagem < 10 || $tamanhoMensagem > 500) {
$erros["mensagem"] = "Escreva uma mensagem entre 10 e 500 caracteres.";
}
if ($erros === []) {
$mensagemAceita = true;
}
}
O array associativo liga o valor técnico ao rótulo apresentado.
array_key_exists() impede assuntos inventados fora do formulário.
filter_var() verifica o formato do e-mail; não confirma que a caixa postal existe.
O telefone é opcional e ainda não tem uma regra de negócio que justifique rejeitá-lo.
Conte caracteres sem depender de uma única instalação
Antes da validação, crie uma função pequena. Quando a extensão mbstring está disponível, ela conta caracteres multibyte corretamente; o retorno alternativo evita interromper o exercício em uma instalação sem essa extensão.
function tamanhoTexto(string $texto): int
{
if (function_exists("mb_strlen")) {
return mb_strlen($texto);
}
return strlen($texto);
}
Validar não é “limpar até aceitar”.
Uma regra deve dizer claramente se o valor serve para aquela finalidade. Alterar silenciosamente o que a pessoa escreveu pode mudar seu significado. Quando houver erro, preserve o valor seguro para exibição e peça uma correção.
Aula 4 — Respondendo com segurança e clareza
Escape no momento de escrever no HTML
Um nome pode conter caracteres que o navegador interpretaria como marcação. Crie esta função antes do HTML:
Validação responde “este dado atende à regra?”. Escape responde “como mostrar este texto com segurança neste HTML?”. São etapas complementares, não substitutas.
Mostre um resumo que também ajuda a corrigir
<?php if ($mensagemAceita): ?>
<div class="mensagem-sucesso" role="status">
<strong>Mensagem recebida e validada.</strong>
<p>Nesta demonstração, ela ainda não foi armazenada nem enviada.</p>
</div>
<?php elseif ($formularioEnviado): ?>
<div class="resumo-erros" role="alert">
<strong>Revise os campos indicados:</strong>
<ul>
<?php foreach ($erros as $campo => $erro): ?>
<li><a href="#<?= escapar($campo) ?>"><?= escapar($erro) ?></a></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
Aplique o mesmo princípio ao e-mail, telefone e mensagem. No select, use uma comparação estrita para devolver selected à opção recebida. O checkpoint contém a página completa.
Código completo para conferência — processamento PHP
<?php
function tamanhoTexto(string $texto): int
{
if (function_exists("mb_strlen")) {
return mb_strlen($texto);
}
return strlen($texto);
}
function escapar(string $valor): string
{
return htmlspecialchars($valor, ENT_QUOTES | ENT_SUBSTITUTE, "UTF-8");
}
function textoPost(string $campo): string
{
$valor = $_POST[$campo] ?? "";
return is_string($valor) ? trim($valor) : "";
}
$formularioEnviado = ($_SERVER["REQUEST_METHOD"] ?? "GET") === "POST";
$mensagemAceita = false;
$erros = [];
$assuntosPermitidos = [
"duvida" => "Dúvida",
"encomenda" => "Encomenda",
"sugestao" => "Sugestão"
];
$nome = textoPost("nome");
$email = textoPost("email");
$telefone = textoPost("telefone");
$assunto = textoPost("assunto");
$mensagem = textoPost("mensagem");
if ($formularioEnviado) {
if (tamanhoTexto($nome) < 2) {
$erros["nome"] = "Informe um nome com pelo menos 2 caracteres.";
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$erros["email"] = "Informe um endereço de e-mail válido.";
}
if (!array_key_exists($assunto, $assuntosPermitidos)) {
$erros["assunto"] = "Escolha um assunto disponível.";
}
$tamanhoMensagem = tamanhoTexto($mensagem);
if ($tamanhoMensagem < 10 || $tamanhoMensagem > 500) {
$erros["mensagem"] = "Escreva uma mensagem entre 10 e 500 caracteres.";
}
$mensagemAceita = $erros === [];
}
Investigue a resposta no Network
Envie dados válidos e abra a requisição contato.php.
Confirme método POST, status 200 e os campos em Payload.
Repita com um e-mail inválido usando a alteração temporária no Elements.
Compare a resposta HTML e confirme que o PHP rejeitou o dado mesmo sem a proteção do navegador.
Aplicar: teste como desenvolvedor e como cliente
Crie uma tabela com pelo menos cinco casos: envio vazio, nome curto, e-mail inválido, assunto inventado e preenchimento válido. Antes de executar, escreva o resultado esperado. Depois compare com a mensagem apresentada e corrija qualquer regra ambígua.
Verifique sua aprendizagem
Escolho POST para entregar dados a um processamento e sei que ele não substitui HTTPS.
Explico o papel de method, action e name.
Detecto o método da requisição e leio valores de $_POST.
Repito no servidor as regras importantes do formulário.
Distingo normalização, validação e escape de saída.
Uso lista permitida para valores fechados e filter_var() para o formato do e-mail.
Apresento erros claros sem obrigar a pessoa a preencher tudo novamente.
Localizo carga útil e resposta no Network.
Transfira o aprendizado
Uma oficina de bicicletas quer receber pedidos de agendamento com nome, e-mail, tipo de serviço e descrição do problema. Sem construir outro projeto, defina quais campos são obrigatórios, uma lista de serviços permitidos e quatro testes de validação. Explique também qual informação pessoal você decidiria não pedir nessa primeira conversa.
Neste capítulo, “recebida” significa que a requisição chegou ao PHP e passou pela validação. A mensagem ainda desaparece ao terminar a resposta. O próximo passo será manter informações entre requisições e evitar reenvios acidentais antes de introduzir persistência permanente.
Web II • 8 horas
Capítulo 4 — Uma confirmação sem reenvio
Redirecionamento, status 303, padrão POST–Redirect–GET, sessões e mensagens temporárias.
A mensagem foi aceita. Por que o navegador quer enviá-la de novo?
Depois de preencher corretamente o contato do Café Aurora, a cliente recebe a confirmação. Em seguida, atualiza a página para conferir o horário do café. O navegador avisa que precisa reenviar os dados do formulário. Se essa operação já gravasse uma encomenda, o mesmo pedido poderia ser processado novamente.
Isso acontece porque a página visível ainda é a resposta do próprio POST. Atualizar significa repetir a última requisição. A aplicação precisa concluir o processamento e levar a pessoa para uma nova requisição GET, que pode ser recarregada sem repetir o formulário.
Há um detalhe: depois do redirecionamento, o POST terminou. Como a nova requisição saberá que deve mostrar a confirmação? Usaremos uma sessão para transportar somente uma mensagem curta, exibida uma vez e removida.
O que você vai aprender
Reconhecer por que atualizar uma resposta POST pode reenviar dados.
Compreender o padrão POST–Redirect–GET.
Enviar um cabeçalho de redirecionamento com header().
Usar o status 303 para solicitar uma nova navegação GET.
Encerrar o processamento com exit.
Iniciar uma sessão e trabalhar com $_SESSION.
Criar uma mensagem temporária, apresentada uma única vez.
Observar redirecionamento, status e cookie de sessão no DevTools.
Aula 1 — Enxergando o reenvio acidental
Entender: a página atual nasceu de qual requisição?
No fim do capítulo anterior, um envio válido produz diretamente uma resposta HTML. A barra de endereço mostra contato.php, mas o documento foi obtido por POST. O endereço sozinho não revela o método usado.
1. Formulário envia POST para contato.php
↓
2. PHP valida os campos
↓
3. A própria resposta POST mostra a confirmação
↓
4. Atualizar tenta repetir o POST
Experimente o problema antes de corrigi-lo
Abra o checkpoint do Capítulo 3 e inicie o servidor local.
Envie uma mensagem válida usando dados fictícios.
Pressione o botão de atualizar do navegador.
Leia o aviso de reenvio, cancele a operação e explique o risco se o sistema já gravasse pedidos.
O fluxo que queremos construir
1. POST recebe e valida
↓ resposta 303
2. Navegador solicita contato.php com GET
↓ resposta 200
3. GET mostra a confirmação temporária
↓
4. Atualizar repete somente o GET
Esse encadeamento é conhecido como POST–Redirect–GET, ou PRG. Ele reduz reenvios causados por atualização e pelo botão voltar. Não substitui validação, HTTPS nem outras proteções da operação.
Aula 2 — Uma pequena memória entre requisições
HTTP não entrega automaticamente ao próximo acesso as variáveis criadas no POST. Uma sessão permite manter dados associados àquele navegador entre requisições. Em uma configuração comum, o servidor guarda os dados e o navegador recebe um cookie com o identificador da sessão.
Inicie ou retome a sessão
Na primeira linha PHP de contato.php, antes de qualquer HTML, espaço fora do PHP ou texto enviado ao navegador, acrescente:
session_start();
session_start() cria uma sessão ou retoma a sessão identificada na requisição. Ela também pode enviar cabeçalhos HTTP; por isso precisa executar antes da saída da página.
Leia e remova uma mensagem de uso único
Logo depois das funções do arquivo, recupere a mensagem e retire-a da sessão:
A variável local continua com o texto durante a resposta atual; na próxima atualização, a sessão já não o possui.
Temporária não significa pública.
Guarde apenas o necessário. Neste exercício, a sessão transporta uma frase de confirmação controlada pela aplicação, não nome, e-mail, telefone nem mensagem da cliente. O identificador da sessão também não deve ser copiado ou compartilhado.
Aula 3 — Redirecionando depois da validação
No Capítulo 3, o resultado válido apenas alterava $mensagemAceita. Substitua essa atribuição pelo armazenamento temporário e pelo redirecionamento:
if ($erros === []) {
$_SESSION["mensagem_temporaria"] =
"Mensagem recebida e validada. " .
"Nesta demonstração, ela ainda não foi armazenada nem enviada.";
header("Location: contato.php", true, 303);
exit;
}
$_SESSION[...]
Guarda a confirmação para a próxima requisição.
Location
Indica o endereço que o navegador deverá solicitar.
303
Orienta o navegador a buscar o destino com GET.
exit
Impede que o PHP continue gerando a resposta antiga.
Cabeçalho vem antes do conteúdo.
header() não consegue alterar normalmente os cabeçalhos depois que a saída começou. Mantenha sessão, validação e redirecionamento no bloco PHP anterior ao DOCTYPE. Não tente esconder o problema ativando saída em buffer sem compreender sua causa.
Mostre a mensagem trazida pelo GET
No HTML, substitua a antiga condição de sucesso:
<?php if ($mensagemTemporaria !== ""): ?>
<div class="mensagem-sucesso" role="status">
<strong><?= escapar($mensagemTemporaria) ?></strong>
</div>
<?php elseif ($formularioEnviado): ?>
<!-- Preserve aqui o resumo de erros do Capítulo 3. -->
<?php endif; ?>
Um POST inválido não redireciona: a mesma resposta preserva os valores e mostra os erros para correção. Somente o caminho válido produz 303 e uma página GET limpa.
Código completo para conferência — sessão e PRG
<?php
session_start();
function tamanhoTexto(string $texto): int
{
if (function_exists("mb_strlen")) {
return mb_strlen($texto);
}
return strlen($texto);
}
function escapar(string $valor): string
{
return htmlspecialchars($valor, ENT_QUOTES | ENT_SUBSTITUTE, "UTF-8");
}
function textoPost(string $campo): string
{
$valor = $_POST[$campo] ?? "";
return is_string($valor) ? trim($valor) : "";
}
$mensagemTemporaria = $_SESSION["mensagem_temporaria"] ?? "";
unset($_SESSION["mensagem_temporaria"]);
$formularioEnviado = ($_SERVER["REQUEST_METHOD"] ?? "GET") === "POST";
$erros = [];
$assuntosPermitidos = [
"duvida" => "Dúvida",
"encomenda" => "Encomenda",
"sugestao" => "Sugestão"
];
$nome = textoPost("nome");
$email = textoPost("email");
$telefone = textoPost("telefone");
$assunto = textoPost("assunto");
$mensagem = textoPost("mensagem");
if ($formularioEnviado) {
if (tamanhoTexto($nome) < 2) {
$erros["nome"] = "Informe um nome com pelo menos 2 caracteres.";
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$erros["email"] = "Informe um endereço de e-mail válido.";
}
if (!array_key_exists($assunto, $assuntosPermitidos)) {
$erros["assunto"] = "Escolha um assunto disponível.";
}
$tamanhoMensagem = tamanhoTexto($mensagem);
if ($tamanhoMensagem < 10 || $tamanhoMensagem > 500) {
$erros["mensagem"] =
"Escreva uma mensagem entre 10 e 500 caracteres.";
}
if ($erros === []) {
$_SESSION["mensagem_temporaria"] =
"Mensagem recebida e validada. " .
"Nesta demonstração, ela ainda não foi armazenada nem enviada.";
header("Location: contato.php", true, 303);
exit;
}
}
?>
Aula 4 — Provando o novo fluxo no navegador
Acompanhe POST, 303 e GET
Abra Network, ative Preserve log ou Preservar registro e limpe a lista.
Envie uma mensagem válida.
Localize a requisição POST com status 303 e confira o cabeçalho Location: contato.php.
Localize logo depois a requisição GET com status 200.
Atualize a página: deverá surgir outro GET, sem aviso de reenvio e sem repetir a confirmação.
Localize a sessão sem expor seu identificador
No DevTools, abra Application → Cookies ou a área equivalente do seu navegador. Na configuração padrão do PHP, você geralmente encontrará um cookie chamado PHPSESSID. Ele identifica a sessão; a frase de confirmação continua no servidor. Não copie o valor para relatórios ou capturas compartilhadas.
Teste
Resultado esperado
Abrir o contato diretamente
GET 200, sem confirmação.
Enviar dados inválidos
POST 200 com erros e valores preservados.
Enviar dados válidos
POST 303, seguido de GET 200 com confirmação.
Atualizar depois do sucesso
Novo GET 200, sem reenvio e sem nova confirmação.
Aplicar: explique com evidências
Execute os quatro testes da tabela e registre método, status e mensagem visível. Depois explique, em três frases, por que a confirmação aparece uma vez mesmo sendo criada durante outra requisição.
Verifique sua aprendizagem
Reconheço quando a página atual ainda é uma resposta POST.
Explico as três etapas do padrão POST–Redirect–GET.
Uso 303 para tornar explícita a navegação seguinte por GET.
Coloco session_start() e header() antes da saída.
Entendo por que exit deve vir depois do redirecionamento.
Uso $_SESSION para uma mensagem temporária.
Removo a mensagem depois da leitura para que apareça uma única vez.
Comprovo o fluxo pelo Network sem divulgar o identificador da sessão.
Transfira o aprendizado
Uma biblioteca comunitária possui um formulário para reservar um livro. Sem desenvolver outro sistema, desenhe o fluxo POST–Redirect–GET, escreva uma confirmação temporária adequada e explique o que poderia acontecer se uma reserva real fosse processada novamente ao atualizar a resposta POST.
O redirecionamento evita o reenvio comum ao atualizar, mas não grava a mensagem, não impede todos os envios duplicados e não protege sozinho uma operação contra requisições forjadas. Antes de alterar dados reais, o sistema também precisará de proteção CSRF e, depois, de persistência confiável.
Web II • 8 horas
Capítulo 5 — O Café ganha memória permanente
SQLite, PDO, arquivo de banco, tabela, colunas, restrições e inicialização segura.
A confirmação apareceu, mas onde ficou a mensagem?
O formulário do Café Aurora já valida os campos, evita o reenvio ao atualizar e mostra uma confirmação temporária. Porém, no dia seguinte, a proprietária não consegue consultar a encomenda: a sessão serviu para atravessar uma requisição, não para construir o histórico do negócio.
Mensagens reais precisam permanecer organizadas mesmo depois que o navegador fecha ou o servidor reinicia. Elas também precisam de identidade, data de criação e regras que impeçam registros incompletos. Essa responsabilidade pertence a um banco de dados.
Neste capítulo, prepararemos um banco SQLite e a tabela que receberá as mensagens. O formulário ainda não gravará dados. Primeiro, o aluno precisa compreender onde a informação será guardada, como o PHP se conecta e quais regras a estrutura deve proteger.
O que você vai aprender
Distinguir sessão, arquivo de conteúdo e banco de dados.
Entender por que SQLite é adequado para esta etapa.
Reconhecer banco, tabela, linha, coluna, chave primária e restrição.
Verificar e habilitar o driver pdo_sqlite.
Manter o banco e o código de acesso fora da pasta pública.
Criar uma conexão por PDO e compreender seu DSN.
Preparar uma tabela com CREATE TABLE.
Executar uma inicialização repetível e conferir seu resultado.
Aula 1 — Escolhendo a memória certa
Entender: nem toda informação deve morar no mesmo lugar
Recurso
Serve para
No Café Aurora
JSON público
Conteúdo relativamente estável entregue ao navegador.
Produtos e preços do cardápio educacional.
Sessão
Estado temporário associado a uma navegação.
Confirmação mostrada uma única vez.
Banco de dados
Registros persistentes, estruturados e consultáveis.
Histórico futuro das mensagens recebidas.
Persistir significa manter a informação além da requisição atual. Isso não autoriza guardar tudo: o negócio deve coletar apenas o necessário, definir por quanto tempo conservar e proteger o acesso.
Por que SQLite agora
SQLite mantém o banco em um arquivo e não exige instalar um serviço de banco separado. Ele é apropriado para aprendizagem, protótipos e aplicações de menor porte executadas em um único servidor. O PHP acessará esse arquivo pelo driver PDO_SQLITE.
SQLite
É o mecanismo de banco que lê e grava o arquivo.
PDO
É a interface do PHP para trabalhar com diferentes bancos.
PDO_SQLITE
É o driver que conecta a interface PDO ao SQLite.
SQL
É a linguagem usada para definir e consultar a estrutura.
SQLite não é “um MySQL menor”.
São bancos com arquiteturas diferentes. SQLite simplifica uma aplicação em um único servidor e com volume moderado de escrita. MySQL pode ser mais adequado quando vários servidores ou muitos processos precisam escrever simultaneamente. Neste curso, começar com SQLite reduz infraestrutura sem reduzir o cuidado com modelagem e segurança.
Modele a primeira tabela
Coluna
Tipo declarado
Regra
Finalidade
id
INTEGER
Chave primária
Identifica cada mensagem.
nome
TEXT
Obrigatório; mínimo 2
Identifica quem escreveu.
email
TEXT
Obrigatório
Permite responder.
telefone
TEXT
Opcional
Canal alternativo.
assunto
TEXT
Três valores permitidos
Classifica a mensagem.
mensagem
TEXT
Entre 10 e 500
Registra o conteúdo.
criada_em
TEXT
Data padrão
Registra quando chegou.
Em SQLite, o tipo declarado orienta como os valores são tratados, mas sua tipagem é flexível. Por isso também usaremos NOT NULL e CHECK para explicitar regras importantes.
Aula 2 — Preparando o driver e protegendo o arquivo
Confirme os módulos disponíveis
No terminal do VS Code no Windows, execute:
php -m | findstr /I "PDO sqlite"
Procure por PDO e pdo_sqlite. Em macOS ou Linux, use:
php -m | grep -Ei "PDO|sqlite"
Se pdo_sqlite não aparecer
Execute php --ini e localize o arquivo de configuração carregado.
Se o Windows informar que não carregou nenhum, copie C:\php\php.ini-development para C:\php\php.ini.
No php.ini, localize ;extension=pdo_sqlite e remova apenas o ponto e vírgula inicial.
Confirme que a pasta indicada por extension_dir contém as extensões da instalação.
Encerre e reinicie o servidor PHP; depois execute novamente o comando de conferência.
Ative somente o que o projeto precisa.
O PDO fornece a interface, mas cada tipo de banco exige seu driver. O driver SQLite não precisa de endereço, usuário ou senha porque acessará um arquivo local.
Crie uma área privada do projeto
Acrescente as pastas src e dados na raiz, ao lado de public:
A opção -t public do servidor impede que o navegador solicite diretamente os arquivos de dados e src. A aplicação PHP continua conseguindo acessá-los pelo sistema de arquivos.
Um banco de desenvolvimento pode conter dados pessoais. Mantê-lo fora do Git evita uma publicação acidental, mas não substitui permissões, backups e controle de acesso.
Aula 3 — Criando a conexão PDO
Crie src/banco.php. A função abaixo concentra o endereço e as opções da conexão em um único lugar:
DSN é a descrição da origem dos dados. Para SQLite, ele contém o nome do driver e o caminho do arquivo. Os dois valores null ocupam as posições de usuário e senha, que não são usados nesta conexão local.
Defina um comportamento previsível
PDO::ERRMODE_EXCEPTION transforma falhas do banco em exceções que o programa pode tratar.
PDO::FETCH_ASSOC prepara consultas futuras para devolver colunas por seus nomes.
Centralizar a conexão evita repetir o caminho e as opções em cada página.
Experimente o caminho
Antes de executar, explique qual caminho será produzido por dirname(__DIR__) quando o arquivo está em src. Confirme também que o destino não começa por public.
Aula 4 — Criando a tabela de forma repetível
Escreva a estrutura em SQL
Em src/inicializar-banco.php, a instrução principal será:
CREATE TABLE IF NOT EXISTS mensagens (
id INTEGER PRIMARY KEY,
nome TEXT NOT NULL CHECK (length(nome) >= 2),
email TEXT NOT NULL,
telefone TEXT,
assunto TEXT NOT NULL
CHECK (assunto IN ('duvida', 'encomenda', 'sugestao')),
mensagem TEXT NOT NULL
CHECK (length(mensagem) BETWEEN 10 AND 500),
criada_em TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
)
IF NOT EXISTS permite repetir a inicialização sem tentar recriar uma tabela já existente.
INTEGER PRIMARY KEY fornece uma identidade inteira única. Não precisamos de AUTOINCREMENT neste caso.
NOT NULL impede ausência de valor.
CHECK rejeita valores que desrespeitam a expressão.
DEFAULT CURRENT_TIMESTAMP fornece a data e hora em UTC quando o registro for criado; a apresentação no horário local será tratada depois.
Execute SQL e trate uma possível falha
$pdo->exec($sql) executa uma instrução que não precisa devolver linhas. O operador -> acessa um método do objeto. Como conexão, pasta ou SQL podem falhar, try delimita a tentativa e catch trata a exceção.
Código completo para conferência — inicialização do banco
<?php
require_once __DIR__ . "/banco.php";
if (!extension_loaded("pdo_sqlite")) {
fwrite(STDERR, "O driver pdo_sqlite não está ativo." . PHP_EOL);
exit(1);
}
try {
$pdo = conectarBanco();
$sql = "
CREATE TABLE IF NOT EXISTS mensagens (
id INTEGER PRIMARY KEY,
nome TEXT NOT NULL CHECK (length(nome) >= 2),
email TEXT NOT NULL,
telefone TEXT,
assunto TEXT NOT NULL
CHECK (assunto IN ('duvida', 'encomenda', 'sugestao')),
mensagem TEXT NOT NULL
CHECK (length(mensagem) BETWEEN 10 AND 500),
criada_em TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
)
";
$pdo->exec($sql);
$quantidade = (int) $pdo
->query("SELECT COUNT(*) FROM mensagens")
->fetchColumn();
echo "Banco e tabela preparados com sucesso." . PHP_EOL;
echo "Mensagens cadastradas: $quantidade" . PHP_EOL;
} catch (PDOException $erro) {
fwrite(
STDERR,
"Não foi possível preparar o banco: " .
$erro->getMessage() .
PHP_EOL
);
exit(1);
}
O terminal deve informar que o banco e a tabela foram preparados e que existem zero mensagens. O arquivo dados/cafe-aurora.sqlite surgirá nesse momento.
Aplicar: prove que a inicialização é segura para repetição
Execute o inicializador pela primeira vez e localize o novo arquivo.
Execute exatamente o mesmo comando novamente.
Confirme que não aparece erro de tabela existente e que a contagem continua zero.
Tente abrir o arquivo SQLite como texto e observe que esse não é seu formato de edição; feche sem salvar.
Verifique sua aprendizagem
Sei por que sessão e banco de dados não são equivalentes.
Explico os papéis de SQLite, PDO e PDO_SQLITE.
Reconheço tabela, linha, coluna, chave primária e restrição.
Confirmo se o driver está ativo antes de procurar erros no código.
Mantenho banco e conexão fora da pasta pública.
Leio o caminho e o DSN usados pela conexão.
Interpreto NOT NULL, CHECK e DEFAULT.
Inicializo a estrutura mais de uma vez sem apagar dados.
Transfira o aprendizado
Uma clínica veterinária quer registrar pedidos de retorno com tutor, contato, nome do animal, motivo e data de criação. Sem construir outro sistema, proponha as colunas, escolha a chave primária e indique quais valores devem ser obrigatórios. Justifique também por que o arquivo do banco não deve ficar na pasta pública.
Banco preparado não significa formulário conectado
A tabela existe, mas ainda contém zero registros. No próximo capítulo, o POST validado será protegido contra CSRF e usará um comando preparado para inserir a mensagem. Essa separação evita apresentar conexão, modelagem, segurança e gravação como uma única etapa obscura.
Web II • 8 horas
Capítulo 6 — A mensagem vira registro
CSRF, token de sessão, comando preparado, INSERT, tratamento de falhas e fluxo PRG completo.
Agora o botão Enviar precisa cumprir sua promessa
O banco do Café Aurora está pronto, mas a proprietária continua sem histórico: o formulário valida os campos e confirma o envio, porém nenhum registro chega à tabela. Ligar as duas partes parece apenas executar um INSERT, mas esse atalho criaria riscos.
Dados vindos do navegador não devem ser encaixados diretamente em SQL. Além disso, uma página externa poderia tentar provocar um envio usando o navegador de outra pessoa. Antes de gravar, o sistema precisa distinguir o comando dos valores e confirmar que o formulário veio da sessão que o exibiu.
Neste capítulo, cada POST válido poderá criar um registro no SQLite, sem a duplicação comum causada por atualizar a página de confirmação. Faremos isso preservando a validação, o escape de saída e o padrão POST–Redirect–GET já construídos.
O que você vai aprender
Reconhecer o limite de confiança entre navegador e servidor.
Entender, gerar, enviar e verificar um token CSRF.
Distinguir instrução SQL, marcador e valor.
Preparar e executar um INSERT com PDO.
Representar o telefone opcional como null.
Tratar uma falha do banco sem expor detalhes técnicos à pessoa usuária.
Manter o PRG depois da gravação.
Comprovar que a tabela recebeu apenas os registros esperados.
Aula 1 — Proteger antes de gravar
Entender: o navegador está fora da área de confiança
Os atributos HTML ajudam quem preenche o formulário, mas podem ser alterados ou ignorados. Por isso, a validação em PHP continua obrigatória. Agora acrescentaremos duas proteções com responsabilidades diferentes:
Proteção
Evita
Não substitui
Comando preparado
Que valores sejam interpretados como parte da instrução SQL.
Validação, autorização e regras do banco.
Token CSRF
Que outra origem provoque uma alteração sem apresentar o token da sessão.
Controle de robôs, limite de envios e autenticação.
Escape na saída
Que texto exibido seja interpretado como marcação HTML.
Proteção do SQL e validação de entrada.
O fluxo seguro da mensagem
1. GET exibe o formulário e o token da sessão
↓
2. POST devolve campos e token ao servidor
↓
3. PHP confere token e valida os campos
↓
4. PDO prepara o SQL e envia os valores separados
↓
5. SQLite grava uma linha
↓
6. PHP redireciona para um novo GET
O token CSRF será secreto, imprevisível e associado à sessão. Ele viajará em um campo oculto do formulário, nunca na URL. O servidor aceitará a alteração somente quando o valor recebido corresponder ao guardado na sessão.
Experimente antes de programar
Desenhe o fluxo em seis passos e marque onde cada decisão acontece. Se você colocou a validação apenas no navegador ou o redirecionamento antes da gravação, corrija o desenho antes de seguir.
Aula 2 — Criando e conferindo o token CSRF
Gere um token para a sessão
Depois de session_start(), crie o token somente se a sessão ainda não possuir um valor válido:
hidden não torna o valor secreto para a pessoa que usa o navegador. Ele apenas não cria um controle visível. O segredo importante é outro: uma página de origem diferente não deve conseguir ler o token gerado para esta sessão.
Compare no servidor
No início do processamento do POST, obtenha o valor como texto e faça uma comparação apropriada:
$tokenRecebido = $_POST["csrf_token"] ?? "";
$tokenValido = is_string($tokenRecebido) &&
hash_equals($csrfToken, $tokenRecebido);
if (!$tokenValido) {
http_response_code(403);
$erroGeral =
"Não foi possível confirmar este envio. " .
"Atualize a página e tente novamente.";
}
hash_equals() compara as duas sequências de forma resistente a ataques de temporização. O valor conhecido da sessão aparece primeiro; o recebido, depois. Token ausente ou diferente impede a gravação, e o status 403 informa que o servidor recusou a operação.
Por que usar CSRF em um formulário público?
A proteção dificulta envios forjados por outra página usando a sessão do navegador. Como não há login, ela não impede um robô de visitar o formulário, obter seu próprio token e enviar mensagens. Essa limitação será tratada como um problema diferente, não escondida sob o nome “segurança”.
Aula 3 — Inserindo valores sem montar SQL com texto
Separe a instrução dos dados
Inclua a conexão preparada no capítulo anterior:
require_once dirname(__DIR__) . "/src/banco.php";
Depois que token e campos forem aprovados, prepare o comando:
As chaves do array correspondem aos marcadores da instrução.
Um telefone vazio vira null, isto é, ausência de valor no banco.
criada_em não aparece porque a tabela já possui um valor padrão.
id também não aparece: o SQLite fornece a próxima identidade inteira.
Não concatene dados na instrução.
Montar SQL com $nome, $email ou qualquer texto recebido mistura dado e comando. Marcadores servem para valores. Nomes de tabela, colunas e palavras da linguagem SQL não devem vir livremente do formulário.
Trate a falha e preserve o PRG
try {
// conectar, preparar e executar...
$_SESSION["mensagem_temporaria"] =
"Mensagem registrada com sucesso.";
header("Location: contato.php", true, 303);
exit;
} catch (PDOException $erro) {
error_log($erro->getMessage());
$erroGeral =
"Não foi possível registrar a mensagem agora. " .
"Tente novamente.";
}
O detalhe técnico vai para o registro de erros do servidor; a página mostra uma orientação segura. O redirecionamento acontece somente depois da execução bem-sucedida. Se o banco falhar, não devemos anunciar sucesso.
Aula 4 — Integrando e testando a gravação
Ajuste a mensagem da interface
Como os dados agora são persistidos, substitua a orientação antiga por uma descrição verdadeira:
<p id="orientacao-formulario">
Os campos identificados como obrigatórios devem ser
preenchidos. A mensagem será armazenada para atendimento.
</p>
Código completo para conferência — processamento do formulário
<?php
session_start();
require_once dirname(__DIR__) . "/src/banco.php";
function tamanhoTexto(string $texto): int
{
if (function_exists("mb_strlen")) {
return mb_strlen($texto);
}
return strlen($texto);
}
function escapar(string $valor): string
{
return htmlspecialchars(
$valor,
ENT_QUOTES | ENT_SUBSTITUTE,
"UTF-8"
);
}
function textoPost(string $campo): string
{
$valor = $_POST[$campo] ?? "";
return is_string($valor) ? trim($valor) : "";
}
if (!isset($_SESSION["csrf_token"]) ||
!is_string($_SESSION["csrf_token"])) {
$_SESSION["csrf_token"] = bin2hex(random_bytes(32));
}
$csrfToken = $_SESSION["csrf_token"];
$mensagemTemporaria =
$_SESSION["mensagem_temporaria"] ?? "";
unset($_SESSION["mensagem_temporaria"]);
$formularioEnviado =
($_SERVER["REQUEST_METHOD"] ?? "GET") === "POST";
$erros = [];
$erroGeral = "";
$assuntosPermitidos = [
"duvida" => "Dúvida",
"encomenda" => "Encomenda",
"sugestao" => "Sugestão"
];
$nome = textoPost("nome");
$email = textoPost("email");
$telefone = textoPost("telefone");
$assunto = textoPost("assunto");
$mensagem = textoPost("mensagem");
if ($formularioEnviado) {
$tokenRecebido = $_POST["csrf_token"] ?? "";
$tokenValido = is_string($tokenRecebido) &&
hash_equals($csrfToken, $tokenRecebido);
if (!$tokenValido) {
http_response_code(403);
$erroGeral =
"Não foi possível confirmar este envio. " .
"Atualize a página e tente novamente.";
}
if (tamanhoTexto($nome) < 2) {
$erros["nome"] =
"Informe um nome com pelo menos 2 caracteres.";
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$erros["email"] =
"Informe um endereço de e-mail válido.";
}
if (!array_key_exists($assunto, $assuntosPermitidos)) {
$erros["assunto"] =
"Escolha um assunto disponível.";
}
$tamanhoMensagem = tamanhoTexto($mensagem);
if ($tamanhoMensagem < 10 || $tamanhoMensagem > 500) {
$erros["mensagem"] =
"Escreva uma mensagem entre 10 e 500 caracteres.";
}
if ($erros === [] && $erroGeral === "") {
try {
$pdo = conectarBanco();
$comando = $pdo->prepare(
"INSERT INTO mensagens
(nome, email, telefone, assunto, mensagem)
VALUES
(:nome, :email, :telefone, :assunto, :mensagem)"
);
$comando->execute([
"nome" => $nome,
"email" => $email,
"telefone" =>
$telefone !== "" ? $telefone : null,
"assunto" => $assunto,
"mensagem" => $mensagem
]);
$_SESSION["mensagem_temporaria"] =
"Mensagem registrada com sucesso.";
header("Location: contato.php", true, 303);
exit;
} catch (PDOException $erro) {
error_log($erro->getMessage());
$erroGeral =
"Não foi possível registrar a mensagem agora. " .
"Tente novamente.";
}
}
}
?>
O restante do HTML permanece. Além da orientação e do campo oculto, ajuste apenas o resumo para também mostrar $erroGeral quando existir. O checkpoint contém a página completa para conferência.
Programe e teste em ordem
Execute php src/inicializar-banco.php antes de iniciar o servidor.
Confira a sintaxe com php -l public/contato.php.
Inicie php -S localhost:8000 -t public.
Envie uma mensagem válida e confirme o redirecionamento seguido da mensagem de sucesso.
Execute novamente php src/inicializar-banco.php; a contagem deve aumentar para 1.
Atualize a página de confirmação e confira que a contagem continua 1.
Use o DevTools para provar as proteções
No painel Elements, localize o campo csrf_token, remova-o temporariamente e envie o formulário. O servidor deve recusar a gravação.
Recarregue a página: a alteração no painel desaparece porque o arquivo não foi modificado.
Envie novamente usando o nome Ana O'Connor. O apóstrofo deve ser tratado como dado, e a tabela deve continuar existindo.
Confira a contagem depois de cada teste e explique por que apenas um deles cria registro.
Verifique sua aprendizagem
Explico por que a validação HTML não protege sozinha o servidor.
Diferencio CSRF, injeção de SQL e interpretação de HTML.
Gero o token com uma fonte aleatória apropriada.
Envio o token em campo oculto e o comparo no servidor.
Separo instrução SQL e valores por marcadores.
Gravo telefone vazio como ausência de valor.
Não mostro mensagens internas do banco à pessoa usuária.
Comprovo que atualizar a confirmação não duplica o registro.
Transfira o aprendizado
A clínica veterinária do desafio anterior decidiu registrar pedidos de retorno. Sem criar outro projeto completo, escreva o INSERT preparado com marcadores para tutor, contato, animal e motivo. Indique onde o token CSRF será criado, enviado e conferido e explique por que nenhuma dessas etapas substitui a validação dos campos.
Gravar com segurança não encerra a responsabilidade
O formulário agora protege o SQL e verifica o token da sessão, mas ainda não possui controle de abuso, idempotência contra envios repetidos, autenticação, painel de atendimento, política de retenção, backup ou publicação em servidor PHP. No próximo capítulo, a proprietária precisará consultar os registros sem expor dados pessoais ao público.
Web II • 8 horas
Capítulo 7 — As mensagens chegam à proprietária
Autenticação, hash de senha, sessão protegida, SELECT, filtro parametrizado, ordenação, limite e saída segura.
Guardar mensagens só tem valor quando a pessoa certa consegue lê-las
O formulário do Café Aurora já registra dúvidas, encomendas e sugestões. Entretanto, abrir o banco manualmente não faz parte da rotina de uma proprietária, e criar uma página pública com nomes, e-mails, telefones e mensagens seria uma falha grave de privacidade.
A solução será uma pequena área administrativa. Antes de consultar o banco, o sistema exigirá uma credencial, associará o acesso aprovado à sessão e impedirá que a resposta com dados pessoais seja armazenada pelo navegador. Somente depois disso executará o SELECT.
Você construirá a operação de leitura do CRUD sem tratar segurança como um botão mágico. A autenticação desta etapa é uma base didática responsável; ao final, seus limites também ficarão explícitos.
O que você vai aprender
Distinguir autenticação, autorização e proteção da rota.
Gerar um hash de senha e verificar a credencial sem guardar a senha original.
Renovar a identificação da sessão depois da entrada.
Impedir o acesso direto ao painel sem uma sessão autenticada.
Consultar registros com SELECT, ORDER BY e LIMIT.
Aplicar um filtro permitido com consulta preparada.
Escapar cada dado antes de mostrá-lo no HTML.
Encerrar a sessão por uma requisição protegida.
Aula 1 — Definindo quem pode entrar
Entender: três perguntas diferentes
Pergunta
Responsabilidade
No Café Aurora
Quem é você?
Autenticação
Confere usuário e senha.
O que você pode fazer?
Autorização
Permite que a pessoa autenticada consulte o painel.
Esta página está protegida?
Controle de acesso
Interrompe a rota antes da consulta quando não há sessão válida.
Ocultar o link, usar uma pasta chamada admin ou acrescentar noindex não responde a essas perguntas. Qualquer endereço conhecido ainda pode ser solicitado. A verificação precisa acontecer no PHP antes que os dados sejam lidos ou enviados.
Não guarde a senha original
Crie src/criar-administrador.php para receber a credencial no terminal. Depois de validar usuário e tamanho mínimo da senha, transforme-a:
password_hash() aplica um algoritmo próprio para senhas e inclui as informações necessárias para a verificação futura. O arquivo gerado ficará em dados/administrador.php, fora da raiz pública e ignorado pelo Git.
Hash não é criptografia reversível.
Na entrada, o programa não “descriptografa” a senha. Ele usa password_verify() para verificar se o texto informado corresponde ao hash armazenado. Mesmo assim, o arquivo deve continuar protegido: quem obtém o hash pode tentar adivinhar senhas fora do sistema.
Crie sua credencial local
php src/criar-administrador.php
Escolha uma senha exclusiva para o exercício, com pelo menos 12 caracteres. Não reutilize a senha de e-mail, rede social ou qualquer serviço real.
Aula 2 — Transformando a entrada em uma sessão protegida
Configure o cookie antes de iniciar a sessão
Em src/autenticacao.php, concentre as funções de acesso. A sessão deve começar com parâmetros apropriados:
session_regenerate_id(true) troca a identificação da sessão depois da autenticação e remove a anterior. A mensagem de erro deve permanecer genérica: “Usuário ou senha inválidos” não revela qual parte estava correta.
Proteja a rota antes de consultar
No início de public/admin/painel.php, inicie a sessão e interrompa quem ainda não entrou:
O cabeçalho no-store orienta navegadores e intermediários a não armazenar a resposta. Ele não substitui autenticação. Do mesmo modo, noindex orienta buscadores, mas não controla acesso.
Teste a porta, não apenas a placa
Abra diretamente /admin/painel.php sem entrar.
Confirme o redirecionamento para entrar.php.
Entre com uma credencial incorreta e observe a resposta genérica.
Entre corretamente e confira, no DevTools, a nova solicitação e o cabeçalho Cache-Control.
Aula 3 — Lendo os registros com critérios claros
Consulte somente o necessário
$consulta = $pdo->query(
"SELECT id, nome, email, telefone, assunto, mensagem, criada_em
FROM mensagens
ORDER BY criada_em DESC, id DESC
LIMIT 50"
);
$mensagens = $consulta->fetchAll();
SELECT
Escolhe explicitamente as colunas que a página usará.
ORDER BY
Coloca primeiro os registros mais recentes e usa o ID como desempate.
LIMIT 50
Impede que uma página cresça sem controle nesta primeira versão.
fetchAll()
Obtém todas as linhas devolvidas por esta consulta limitada.
Valide o filtro antes de construir a consulta
O filtro virá da URL, portanto continua sendo dado externo. Aproveite a mesma lista fechada usada no formulário:
Quando houver assunto, use um marcador para o valor:
$consulta = $pdo->prepare(
"SELECT id, nome, email, telefone, assunto, mensagem, criada_em
FROM mensagens
WHERE assunto = :assunto
ORDER BY criada_em DESC, id DESC
LIMIT 50"
);
$consulta->execute(["assunto" => $assunto]);
Por que existem duas consultas?
Sem filtro, não precisamos inventar uma condição artificial. Com filtro, o valor é enviado separadamente. Em ambos os casos, palavras SQL, colunas, ordenação e limite permanecem definidos pelo programa, não pela URL.
Aula 4 — Mostrando dados sem transformá-los em código
Aplique a função a nome, e-mail, telefone, assunto, mensagem e valores usados em atributos HTML. Para preservar quebras de linha da mensagem, escape primeiro e somente depois use nl2br():
Inverter a ordem faria htmlspecialchars() neutralizar também os elementos <br> criados pelo programa.
Dê estrutura à tabela
Use caption, cabeçalhos com scope="col" e uma região com rolagem horizontal em telas estreitas. A responsividade não deve ocultar colunas nem misturar o contato de uma pessoa com a mensagem de outra.
Encerre a sessão como uma alteração de estado
O botão Sair deve enviar POST com o token CSRF. Depois da conferência, limpe os dados, remova o cookie e destrua a sessão. Um link GET não é adequado para uma ação que muda o estado de autenticação.
Faça um teste que comprova a proteção de saída
No formulário público, envie uma mensagem contendo <strong>teste</strong>.
Abra o painel e confirme que as marcas aparecem como texto, não como formatação.
Filtre cada assunto e confirme que uma opção inventada na URL não altera a instrução SQL.
Saia, use o botão Voltar do navegador e confirme que o painel exige autenticação novamente.
Verifique sua aprendizagem
Explico por que esconder um endereço não protege seus dados.
Não guardo nem reutilizo senhas reais no exercício.
Diferencio hash de senha e criptografia reversível.
Renovo a identificação da sessão depois da autenticação.
Protejo a rota antes de executar o SELECT.
Uso ordenação e limite explícitos na leitura.
Mantenho valores externos fora da estrutura da instrução SQL.
Escapo cada dado no momento de gerar o HTML.
Transfira o aprendizado
A clínica veterinária precisa consultar pedidos de retorno por tipo de animal. Desenhe a tela protegida, defina as colunas que realmente precisam ser exibidas e escreva uma consulta com filtro parametrizado, ordenação e limite. Depois, identifique quais valores precisam ser escapados antes de aparecer no HTML.
Uma base didática não é um sistema de identidade completo
O painel já exige credencial, protege a rota e evita exibir dados como HTML, mas ainda não possui limite de tentativas, recuperação de senha, HTTPS obrigatório, perfis de autorização, auditoria, expiração por inatividade ou política de retenção. O próximo capítulo continuará o CRUD e permitirá registrar o andamento de cada atendimento.
Web II • 8 horas
Capítulo 8 — Cada mensagem ganha andamento
Evolução da tabela, estados permitidos, formulário administrativo, UPDATE parametrizado, WHERE, linhas afetadas e PRG.
Ler não basta quando o atendimento precisa continuar
A proprietária do Café Aurora já consegue entrar no painel e consultar as mensagens. Depois de alguns dias, surge outro problema: ela não sabe quais contatos acabaram de chegar, quais estão sendo tratados e quais já receberam resposta.
Apagar uma mensagem respondida eliminaria o histórico. Deixar tudo misturado faria pedidos importantes se perderem. O sistema precisa representar o andamento do trabalho e permitir que uma única mensagem mude de estado.
Neste capítulo, você construirá o Update do CRUD. Antes de alterar dados, evoluirá a estrutura do banco sem apagar registros existentes. Depois, cada atualização passará por autenticação, token CSRF, validação, comando preparado e uma cláusula WHERE específica.
O que você vai aprender
Distinguir evolução de estrutura e alteração de registros.
Adicionar colunas a uma tabela existente sem recriá-la.
Definir estados permitidos no PHP e no banco.
Enviar ID, estado e token por um formulário administrativo.
Validar campos ocultos como qualquer outro dado externo.
Executar UPDATE parametrizado com WHERE.
Verificar quantas linhas foram afetadas.
Aplicar POST–Redirect–GET e preservar o filtro do painel.
Aula 1 — Preparando o banco para acompanhar o trabalho
Entender: registro e estrutura não são a mesma coisa
Mudança
Exemplo
Comando principal
Estrutura da tabela
Acrescentar as colunas status e atualizada_em.
ALTER TABLE
Conteúdo de um registro
Mudar a mensagem 12 de “nova” para “em atendimento”.
UPDATE
O banco de quem começou no Capítulo 5 já possui a tabela mensagens. Alterar apenas o CREATE TABLE IF NOT EXISTS não modifica essa tabela antiga. Por isso, o inicializador precisa reconhecer quais colunas já existem.
PRAGMA table_info devolve informações sobre as colunas. array_column() extrai somente os nomes, permitindo decidir se cada pequena migração ainda é necessária.
Adicione somente o que falta
if (!in_array("status", $nomesColunas, true)) {
$pdo->exec(
"ALTER TABLE mensagens
ADD COLUMN status TEXT NOT NULL DEFAULT 'nova'
CHECK (status IN ('nova', 'em_atendimento', 'respondida'))"
);
}
if (!in_array("atualizada_em", $nomesColunas, true)) {
$pdo->exec(
"ALTER TABLE mensagens
ADD COLUMN atualizada_em TEXT"
);
}
DEFAULT 'nova'
Dá um estado coerente aos registros antigos e aos novos.
CHECK
Impede que o banco aceite um estado fora da lista.
atualizada_em
Começa vazia e recebe a data quando o andamento muda.
Esta é uma migração didática e idempotente.
Execute as mudanças dentro de uma transação: se uma delas falhar, faça rollBack(); se todas funcionarem, faça commit(). Repetir o inicializador não deve duplicar colunas nem apagar mensagens. Projetos maiores registram versões de esquema e usam ferramentas próprias de migração, mas a ideia central permanece: transformar uma estrutura conhecida em outra de maneira controlada.
Aula 2 — Enviando a intenção de mudar uma mensagem
Inclua o estado na leitura
O painel precisa receber as novas colunas para exibir a situação atual:
SELECT id, nome, email, telefone, assunto, mensagem,
criada_em, status, atualizada_em
FROM mensagens
ORDER BY criada_em DESC, id DESC
LIMIT 50
O ID identifica a linha que deverá mudar. O token confirma que a solicitação veio da sessão que exibiu o formulário. O estado informa o novo valor. Os três campos podem ser modificados fora da interface, portanto todos precisam ser verificados no servidor.
hidden significa invisível, não confiável.
O navegador envia campos ocultos como envia qualquer outro campo. Uma pessoa pode alterá-los pelo DevTools ou construir outra requisição. O servidor não deve confiar no ID apenas porque a tela não permite digitá-lo.
Preserve o contexto de trabalho
Se a proprietária estiver vendo apenas encomendas, a atualização não deve devolvê-la à lista completa sem necessidade. Envie também assunto_retorno, valide-o pela lista fechada e use-o somente para reconstruir o redirecionamento.
Aula 3 — Atualizando exatamente uma linha
Comece protegendo a rota
Crie public/admin/atualizar-status.php. Antes de ler os campos, exija autenticação, aceite somente POST e confira o token:
iniciarSessaoSegura();
exigirAutenticacao();
if (($_SERVER["REQUEST_METHOD"] ?? "GET") !== "POST") {
header("Allow: POST");
http_response_code(405);
exit("Método não permitido.");
}
if (!tokenCsrfValido($_POST["csrf_token"] ?? "")) {
http_response_code(403);
exit("Não foi possível confirmar a alteração.");
}
Um inteiro positivo ainda pode apontar para uma linha inexistente; essa diferença será descoberta depois da execução. Já um estado fora da lista deve ser rejeitado antes de chegar ao banco.
Separe comando e valores
$comando = $pdo->prepare(
"UPDATE mensagens
SET status = :status,
atualizada_em = CURRENT_TIMESTAMP
WHERE id = :id"
);
$comando->execute([
"status" => $status,
"id" => $id
]);
Sem WHERE, todas as mensagens mudariam.
O marcador protege os valores contra interpretação como SQL, mas não corrige uma instrução logicamente perigosa. Segurança também depende de escrever o comando certo. Antes de executar um UPDATE ou DELETE, localize e explique sua condição.
Confira o efeito real
if ($comando->rowCount() === 1) {
$_SESSION["aviso_admin"] = [
"tipo" => "sucesso",
"texto" => "Andamento atualizado com sucesso."
];
} else {
$_SESSION["aviso_admin"] = [
"tipo" => "erro",
"texto" => "A mensagem informada não foi encontrada."
];
}
Como id é a chave primária, a condição pode localizar no máximo uma linha. A aplicação espera modificar exatamente uma. Zero indica que o ID não encontrou um registro; qualquer resultado diferente do esperado não deve ser tratado como sucesso.
Aula 4 — Confirmando sem repetir a alteração
Use uma mensagem temporária e redirecione
Depois do processamento, preserve o filtro permitido e transforme o POST em um novo GET:
O painel lê $_SESSION["aviso_admin"], remove a mensagem da sessão e a mostra uma única vez. Atualizar a página repete apenas o GET do painel, não o UPDATE.
Teste o caminho feliz e as recusas
Execute novamente php src/inicializar-banco.php e confirme que os registros continuam no banco.
Mude uma mensagem de “Nova” para “Em atendimento” e depois para “Respondida”.
Recarregue o painel e confirme que o estado permanece.
Altere o ID para zero no DevTools: nenhuma outra linha deve mudar.
Crie uma opção de estado inventada: o PHP deve recusá-la.
Remova o token: a resposta deve ser 403.
Abra o endereço de atualização por GET: a resposta deve ser 405.
Filtre por encomenda, atualize uma linha e confirme que o filtro permanece ativo.
Verifique sua aprendizagem
Explico por que alterar o CREATE TABLE não atualiza sozinho uma tabela existente.
Faço uma pequena migração sem apagar os registros.
Valido campos ocultos no servidor.
Recuso método, token, ID e estado inadequados.
Uso marcadores tanto para o novo valor quanto para o identificador.
Localizo e explico a cláusula WHERE.
Confiro a quantidade de linhas afetadas.
Evito repetir o UPDATE ao atualizar a página.
Transfira o aprendizado
A clínica veterinária quer acompanhar pedidos de retorno como “aguardando”, “contato realizado” e “consulta marcada”. Defina a nova coluna, o valor padrão e a restrição do banco. Depois escreva um UPDATE preparado que altere somente o pedido identificado e explique como você comprovaria que nenhuma outra linha mudou.
Atualizar o estado não define sozinho a política do histórico
O sistema agora cria, lê e atualiza mensagens, mas ainda não decidiu por quanto tempo deve guardar dados pessoais nem como remover um registro sem exclusão acidental. O próximo capítulo concluirá o CRUD com exclusão controlada e uma política simples de retenção.
Web II • 8 horas
Capítulo 9 — Excluir exige critério
Ciclo de vida dos dados, retenção, confirmação explícita, DELETE parametrizado, WHERE defensivo, linhas afetadas e PRG.
Guardar tudo para sempre também cria um problema
O painel do Café Aurora já separa mensagens novas, em atendimento e respondidas. Meses depois, a proprietária percebe que contatos antigos continuam guardando nomes, e-mails, telefones e textos que já não ajudam no atendimento.
Um clique apressado pode destruir um registro ainda necessário. Mas conservar dados pessoais sem finalidade também aumenta exposição e desorganiza o trabalho. A decisão correta não é “apagar tudo” nem “guardar tudo”: é definir por que cada dado existe, quando ele deixa de ser necessário e quem pode eliminá-lo.
Neste capítulo, você concluirá o CRUD com o Delete. O sistema permitirá excluir somente uma mensagem respondida, mediante autenticação, POST, token CSRF, confirmação explícita e uma condição que protege até mesmo contra alteração indevida da interface.
O que você vai aprender
Relacionar coleta, uso, retenção e descarte no ciclo de vida dos dados.
Distinguir exclusão, arquivamento, anonimização e cópia de segurança.
Definir uma regra de negócio antes de oferecer o botão de exclusão.
Exigir confirmação sem confiar apenas na interface.
Proteger a rota com autenticação, POST e token CSRF.
Executar DELETE parametrizado com WHERE defensivo.
Conferir a quantidade de linhas afetadas e aplicar POST–Redirect–GET.
Testar tanto o sucesso quanto as tentativas que devem ser recusadas.
Aula 1 — Decidindo antes de apagar
Entender: o dado percorre um ciclo
1. Coletar somente o necessário
↓
2. Usar para a finalidade informada
↓
3. Manter enquanto houver necessidade
↓
4. Revisar e descartar com segurança
A mensagem existe para permitir contato e atendimento. Quando esse trabalho termina, o negócio deve avaliar se ainda há uma finalidade legítima ou alguma obrigação que justifique a conservação. O exemplo didático não inventará um prazo universal: negócios diferentes podem ter finalidades e obrigações diferentes.
“Noventa dias” não é uma resposta automática da LGPD.
A lei trabalha com finalidade, necessidade e hipóteses de conservação, não com um único prazo para qualquer mensagem. Em um sistema real, a política deve ser documentada com orientação jurídica e administrativa adequada ao negócio. Aqui, o critério técnico será simples: somente mensagens respondidas e sem necessidade operacional poderão ser removidas individualmente.
Escolha a ação correta
Ação
O que acontece
Quando pode fazer sentido
Excluir
Remove o registro da base ativa.
Quando a finalidade terminou e não existe motivo para conservar.
Arquivar
Retira da rotina, mas mantém os dados identificáveis.
Quando ainda existe uma necessidade real de consulta.
Anonimizar
Busca impedir a associação do dado a uma pessoa.
Quando a análise pode continuar sem identificação, usando método adequado.
Fazer backup
Cria uma cópia para recuperação.
Para continuidade e restauração, com acesso e prazo próprios.
Backup não é botão “Desfazer” do painel. Uma exclusão na base ativa pode continuar presente em cópias temporárias de segurança até o encerramento do ciclo dessas cópias. Por isso, retenção e backup precisam de regras coerentes.
Aula 2 — Tornando a intenção inequívoca
Mostre a ação somente no momento adequado
No painel, mantenha a atualização para todos os estados, mas ofereça a exclusão apenas quando a mensagem estiver respondida. Isso reduz enganos sem substituir a validação do servidor.
Explica qual registro será eliminado e evita um “OK” ambíguo.
Ação separada
Atualizar e excluir usam formulários e rotas diferentes.
Estilo de perigo
A cor e o texto distinguem a ação destrutiva, sem depender somente da cor.
required melhora a experiência, mas não protege o servidor.
Uma requisição pode ser construída sem usar o formulário. O PHP ainda precisa conferir o valor exato de confirmacao, além do ID, da sessão e do token.
Aula 3 — Excluindo uma única mensagem respondida
Proteja a rota antes de tocar no banco
Crie public/admin/excluir-mensagem.php e repita as barreiras das outras ações administrativas:
iniciarSessaoSegura();
exigirAutenticacao();
if (($_SERVER["REQUEST_METHOD"] ?? "GET") !== "POST") {
header("Allow: POST");
http_response_code(405);
exit("Método não permitido.");
}
if (!tokenCsrfValido($_POST["csrf_token"] ?? "")) {
http_response_code(403);
exit("Não foi possível confirmar a exclusão.");
}
Valide o ID e a confirmação
$id = filter_var(
$_POST["id"] ?? "",
FILTER_VALIDATE_INT,
["options" => ["min_range" => 1]]
);
$confirmacao = $_POST["confirmacao"] ?? "";
$confirmacao = is_string($confirmacao) ? $confirmacao : "";
if ($id === false || $confirmacao !== "excluir") {
// Registre um aviso de erro e não execute DELETE.
}
Coloque a regra também no WHERE
$comando = $pdo->prepare(
"DELETE FROM mensagens
WHERE id = :id
AND status = 'respondida'"
);
$comando->execute(["id" => $id]);
O ID limita a operação a uma linha. A condição do estado impede a exclusão de uma mensagem nova ou em atendimento, mesmo que alguém altere o HTML. O marcador mantém o valor externo separado da estrutura SQL.
Sem WHERE, o DELETE remove todas as linhas.
Faça uma pausa obrigatória antes de executar: leia a instrução, aponte a condição e diga quantos registros ela pode atingir. Ferramentas e permissões ajudam, mas não compensam uma lógica destrutiva escrita incorretamente.
Trate sucesso como exatamente uma linha removida
if ($comando->rowCount() === 1) {
$_SESSION["aviso_admin"] = [
"tipo" => "sucesso",
"texto" => "Mensagem excluída com sucesso."
];
} else {
$_SESSION["aviso_admin"] = [
"tipo" => "erro",
"texto" => "A mensagem não existe ou ainda não pode ser excluída."
];
}
Zero linhas não deve virar sucesso: o ID pode não existir ou o registro pode não estar respondido. A mensagem de retorno não revela detalhes desnecessários, mas orienta a pessoa autenticada.
Uma única instrução DELETE já é atômica no SQLite.
Não acrescente uma transação apenas para parecer mais seguro. Use beginTransaction() quando várias operações relacionadas precisarem ser confirmadas ou desfeitas como uma unidade, por exemplo registrar uma auditoria e excluir o dado. Neste exercício, a condição completa está na única instrução executada.
Aula 4 — Comprovando que a exclusão está sob controle
Redirecione e preserve o filtro
Depois da tentativa, reconstrua somente um destino permitido e responda com 303. Atualizar o painel repetirá o GET, não a exclusão:
Cadastre três mensagens e deixe cada uma em um estado diferente.
Confirme que somente a respondida mostra o formulário de exclusão.
Tente enviar sem marcar a confirmação: o navegador deve impedir.
Remova a confirmação no DevTools e envie: o servidor deve recusar.
Altere o ID para o registro em atendimento: nenhuma linha deve ser removida.
Remova o token: a resposta deve ser 403.
Abra a rota por GET: a resposta deve ser 405.
Exclua a mensagem respondida e confirme que exatamente ela desapareceu.
Atualize a página: nenhuma exclusão adicional deve ocorrer.
Repita o teste com um filtro ativo e confirme que ele permanece.
Registre uma política simples antes de automatizar
O Café Aurora deve documentar: finalidade das mensagens, responsáveis, critérios de encerramento, condições de conservação, forma de descarte e tratamento das cópias de segurança. Uma rotina automática só deve existir depois que essas decisões estiverem corretas. Automatizar uma regra errada apenas produz erros mais rapidamente.
Verifique sua aprendizagem
Explico por que guardar dados indefinidamente também gera risco.
Não invento um prazo universal de retenção.
Diferencio exclusão, arquivo, anonimização e backup.
Exijo confirmação na interface e novamente no servidor.
Recuso GET, sessão ausente, token inválido e ID inadequado.
Uso WHERE com ID e estado permitido.
Confiro se exatamente uma linha foi removida.
Evito repetir o DELETE ao atualizar a página.
Transfira o aprendizado
A clínica veterinária terminou um pedido de retorno, mas o histórico pode conter informação necessária ao prontuário. Antes de programar, separe o que é simples contato do que integra outro registro sujeito a regras próprias. Depois, desenhe uma exclusão individual que só aceite pedidos encerrados, exija confirmação no servidor e comprove quantas linhas foram removidas.
O Café Aurora agora cria, lê, atualiza e exclui registros com controles básicos. Ainda dependemos principalmente de testes manuais e repetitivos. O próximo capítulo transformará comportamentos importantes em testes que possam ser executados novamente a cada mudança.
Web II • 8 horas
Capítulo 10 — O teste pode ser repetido
Risco de regressão, código testável, Arrange–Act–Assert, SQLite em memória, executor de testes, saída do processo e limites da cobertura.
A regra continua segura depois da próxima mudança?
O Café Aurora já concluiu o CRUD. Para conferir a exclusão, o aluno cadastra mensagens, muda estados, tenta IDs inadequados e observa o resultado. O procedimento funciona, mas precisa ser repetido manualmente sempre que o código muda.
Agora imagine uma manutenção simples: outro programador reorganiza a rota e, sem perceber, remove a condição status = 'respondida'. A página ainda abre, o botão ainda funciona e uma mensagem respondida ainda pode ser excluída. Apenas o caso proibido revelaria a regressão.
Neste capítulo, você separará a operação de banco em uma função reutilizável e criará testes que preparam um banco temporário, executam a mesma função da aplicação e verificam automaticamente o resultado. O banco real e os dados do painel não serão tocados.
O que você vai aprender
Explicar regressão e o valor de uma verificação repetível.
Distinguir teste manual, unitário, de integração e ponta a ponta.
Organizar um teste em preparar, executar e verificar.
Extrair acesso ao banco para uma função que possa ser chamada fora da rota.
Criar um banco SQLite exclusivo em memória.
Construir um pequeno executor de testes sem dependências externas.
Usar mensagens de falha e código de saída para indicar o resultado.
Reconhecer o que a suíte protege e o que ainda exige outros testes.
Aula 1 — Transformando uma conferência em especificação executável
Entender: clicar uma vez não prova o futuro
Um teste manual responde se um comportamento funcionou naquele momento e naquele caminho percorrido. Um teste automatizado registra entradas, ação e resultado esperado em código. Ele pode ser repetido depois de uma correção, refatoração ou nova funcionalidade.
Tipo
Foco
Exemplo no Café Aurora
Manual
Uso real, apresentação e percepção humana.
Confirmar que o formulário de exclusão está compreensível no celular.
Unitário
Uma unidade pequena, isolada de banco, rede e interface.
Validar uma função pura que aceite apenas estados previstos.
Integração
Colaboração entre partes reais.
Executar a função de repositório contra um SQLite em memória.
Ponta a ponta
Fluxo completo pelo sistema.
Entrar, marcar a confirmação, enviar o POST e observar o painel.
O teste deste capítulo é de integração: usa PDO, o driver SQLite, uma tabela real e a instrução SQL de produção. Chamar qualquer teste de “unitário” apenas porque ele é automatizado esconderia dependências importantes.
Escreva o comportamento antes do mecanismo
Deve permitir
Uma mensagem respondida identificada é removida.
Deve impedir
Uma mensagem nova ou em atendimento permanece.
Deve limitar
Excluir um ID não modifica outro registro.
Essas frases são regras observáveis. Detalhes como nome de variável ou quantidade de linhas da função podem mudar sem quebrar o contrato. Um bom teste protege comportamento relevante, não a aparência interna do código.
Aula 2 — Fazendo a aplicação e o teste chamarem o mesmo código
Extraia a operação da rota
Crie src/repositorio-mensagens.php. A função recebe a conexão pronta e o ID, executa a exclusão defensiva e devolve se exatamente uma linha foi removida:
<?php
function excluirMensagemRespondida(PDO $pdo, int $id): bool
{
$comando = $pdo->prepare(
"DELETE FROM mensagens
WHERE id = :id
AND status = 'respondida'"
);
$comando->execute(["id" => $id]);
return $comando->rowCount() === 1;
}
A função não abre a conexão por conta própria. Quem chama decide se entregará o banco do sistema ou um banco exclusivo de teste. Essa passagem explícita da dependência torna o código mais flexível e reduz a chance de o teste tocar dados reais.
Use a função na rota existente
require_once dirname(__DIR__, 2) .
"/src/repositorio-mensagens.php";
$pdo = conectarBanco();
$excluiu = excluirMensagemRespondida($pdo, $id);
if ($excluiu) {
// Registre o aviso de sucesso.
} else {
// A mensagem não existe ou não está respondida.
}
Não copie o SQL para dentro do teste.
Se o teste executar uma versão própria da instrução, ele poderá continuar verde enquanto a rota usa outra versão defeituosa. Aplicação e teste devem chamar a mesma função de produção.
Refatorar preserva o comportamento.
A rota continua validando autenticação, método, CSRF, ID, confirmação e filtro. Somente a responsabilidade de conversar com o banco foi deslocada para um arquivo apropriado. Antes e depois da mudança, a regra externa deve permanecer a mesma.
Aula 3 — Preparando um laboratório descartável
Crie um banco novo para cada teste
Em tests/exclusao-mensagem-test.php, abra sqlite::memory:. O conteúdo existirá somente naquela conexão e desaparecerá quando o processo terminar:
function criarBancoTeste(): PDO
{
$pdo = new PDO("sqlite::memory:", null, null, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
]);
$pdo->exec(
"CREATE TABLE mensagens (
id INTEGER PRIMARY KEY,
status TEXT NOT NULL
)"
);
$pdo->exec(
"INSERT INTO mensagens (id, status) VALUES
(1, 'nova'),
(2, 'em_atendimento'),
(3, 'respondida'),
(4, 'respondida')"
);
return $pdo;
}
Cada caso chama criarBancoTeste(). Assim, um teste não depende da ordem nem das alterações deixadas pelo anterior. O conjunto inicial conhecido é chamado de fixture.
Use preparar, executar e verificar
$pdo = criarBancoTeste(); // Arrange
$excluiu = excluirMensagemRespondida( // Act
$pdo,
3
);
verificar($excluiu, // Assert
"A mensagem respondida deveria ser excluída."
);
$quantidade = (int) $pdo
->query("SELECT COUNT(*) FROM mensagens WHERE id = 3")
->fetchColumn();
verificar($quantidade === 0,
"O registro 3 ainda está no banco."
);
A organização Arrange–Act–Assert deixa clara a preparação, a ação principal e a comprovação. Comentários podem ajudar no início, mas a estrutura e os nomes devem tornar cada etapa reconhecível.
verificar() interrompe o caso quando uma expectativa não é atendida. testar() captura a falha, mostra o comportamento afetado e permite que os próximos casos também sejam executados.
Aula 4 — Executando, provocando falha e interpretando o resultado
Rode toda a suíte com um comando
php tests/exclusao-mensagem-test.php
A saída esperada apresenta quatro casos aprovados e termina com um resumo. O arquivo retorna código 0 quando tudo passa e 1 quando existe falha. Editores, scripts e futuras ferramentas de integração contínua usam esse código para decidir se a execução foi bem-sucedida.
[OK] exclui uma mensagem respondida
[OK] não exclui uma mensagem nova
[OK] não exclui uma mensagem em atendimento
[OK] exclui somente o ID informado
4 teste(s), 0 falha(s).
Comprove que o teste detecta o defeito
Execute a suíte e confirme que todos os casos passam.
Temporariamente, retire AND status = 'respondida' da função.
Execute novamente: os casos proibidos devem falhar.
Restaure a condição e confirme que a suíte volta a passar.
Um teste que nunca foi visto falhar pode estar verificando a coisa errada. Essa alteração controlada demonstra que a suíte realmente vigia a regra. Não publique nem mantenha o defeito provocado.
Confira o código de saída
Terminal
Comando após os testes
PowerShell
$LASTEXITCODE
Prompt de Comando
echo %ERRORLEVEL%
Linux ou macOS
echo $?
Quatro testes não significam sistema totalmente testado.
A suíte cobre a regra SQL de exclusão e seu efeito no banco. Ela não abre o navegador, não autentica, não envia o formulário e não avalia acessibilidade ou aparência. Os testes manuais do Capítulo 9 continuam úteis até que outros níveis sejam construídos.
Entenda por que começamos sem framework
O pequeno executor torna visíveis caso, expectativa, falha, resumo e código de saída. Em projetos reais, não convém ampliar indefinidamente uma ferramenta caseira. O PHPUnit oferece organização, asserções, seleção de testes, relatórios e integração com outras ferramentas. O próximo passo será migrar esta suíte para uma ferramenta de mercado sem perder o entendimento construído aqui.
Verifique sua aprendizagem
Explico o que é regressão com um exemplo do sistema.
Não confundo teste automatizado com teste unitário.
Separo preparação, execução e verificação.
Faço teste e aplicação chamarem o mesmo código de produção.
Uso um banco em memória que não toca os dados reais.
Crio um estado inicial independente para cada caso.
Leio mensagem, resumo e código de saída.
Reconheço comportamentos ainda não cobertos pela suíte.
Transfira o aprendizado
A clínica veterinária permite encerrar apenas pedidos com estado “consulta realizada”. Extraia a operação para uma função que receba PDO e ID. Depois crie um banco SQLite em memória e automatize três provas: o estado permitido é removido, o estado “aguardando” permanece e um ID inexistente não produz sucesso.
Um executor didático não substitui uma ferramenta de testes
A suíte já detecta regressões importantes sem tocar o banco real, mas ainda não oferece recursos profissionais de organização, seleção e relatório. O próximo capítulo levará os mesmos comportamentos ao PHPUnit e preparará uma rotina padronizada de testes.
Web II • 8 horas
Capítulo 11 — A suíte entra no padrão
Composer, dependências de desenvolvimento, autoload, PHPUnit 13, configuração da suíte, asserções, TestDox e execução seletiva.
Como outra pessoa instala e executa os testes?
O executor criado no capítulo anterior cumpriu uma função importante: tornou visíveis os casos, as verificações, as falhas e o código de saída. Mas o Café Aurora agora precisa ser retomado por outra pessoa, em outro computador, sem depender de arquivos copiados à mão ou comandos lembrados de memória.
Neste capítulo, os mesmos quatro comportamentos serão migrados para o PHPUnit. O Composer registrará as dependências do projeto, criará o carregamento automático e oferecerá um único comando para a suíte. O banco continuará sendo um SQLite em memória, novo para cada teste; a mudança está na infraestrutura, não na regra protegida.
O que você vai aprender
Explicar a diferença entre gerenciador de dependências, framework de testes e executor.
Distinguir composer.json, composer.lock e vendor/.
Declarar requisitos de execução e dependências exclusivas de desenvolvimento.
Carregar o código da aplicação pelo autoload do Composer.
Migrar os quatro casos para uma classe TestCase.
Trocar verificações caseiras por asserções expressivas do PHPUnit.
Configurar e executar a suíte com um comando estável.
Selecionar um caso, provocar uma falha e interpretar o relatório.
Aula 1 — Tornando as dependências explícitas
Entender: cada arquivo responde a uma pergunta
Item
O que registra
Vai para o repositório?
composer.json
Requisitos, faixas de versão, autoload e comandos do projeto.
Sim.
composer.lock
Versões exatas resolvidas pelo Composer.
Sim, neste projeto de aplicação.
vendor/
Pacotes baixados e autoload gerado para aquela instalação.
Não; pode ser reconstruído.
O arquivo JSON declara a intenção; o arquivo de trava registra a resolução; a pasta vendor/ é o resultado instalado. Confundir esses papéis produz instalações enormes ou diferentes entre computadores.
Este módulo usa PHP 8.5. O PHPUnit 13 exige PHP 8.4 ou superior, portanto as versões são compatíveis. No Linux ou macOS, substitua findstr por grep -Ei.
Instale o Composer pela fonte oficial.
O procedimento de instalação pode mudar e inclui verificações de integridade. Consulte a página oficial indicada nas referências, conclua a instalação e só então confirme composer --version.
require descreve o que a aplicação precisa para funcionar. require-dev contém ferramentas usadas para desenvolver e verificar o projeto. O PHPUnit não é necessário para servir as páginas ao visitante, por isso pertence ao segundo grupo.
As entradas ext-pdo e ext-pdo_sqlite não baixam extensões do PHP. Elas fazem o Composer interromper a instalação com uma explicação quando o ambiente não possui capacidades exigidas pelo sistema.
Experimente a primeira instalação
composer validate
composer install
Como o checkpoint ainda não possui composer.lock, a primeira instalação resolve versões compatíveis, grava a trava e cria vendor/. Depois disso, mantenha o lock no controle de versão e use composer install para repetir aquelas versões. Use composer update somente quando a equipe decidir atualizar dependências e revisar a nova trava.
Não envie vendor/ ao repositório.
Adicione /vendor/ e /.phpunit.cache/ ao .gitignore. Não ignore composer.lock: ele é parte da reprodução desta aplicação.
Aula 3 — Migrando os casos para PHPUnit
Programar: configure a descoberta da suíte
Crie phpunit.xml na raiz. O bootstrap carrega o arquivo gerado pelo Composer; a suíte procura classes de teste na pasta tests:
<?php
declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class ExclusaoMensagemTest extends TestCase
{
public function testExcluiUmaMensagemRespondida(): void
{
$pdo = $this->criarBancoTeste();
$excluiu = excluirMensagemRespondida($pdo, 3);
self::assertTrue($excluiu);
self::assertSame(0, $this->quantidadeMensagem($pdo, 3));
}
public function testNaoExcluiUmaMensagemNova(): void
{
$pdo = $this->criarBancoTeste();
$excluiu = excluirMensagemRespondida($pdo, 1);
self::assertFalse($excluiu);
self::assertSame(1, $this->quantidadeMensagem($pdo, 1));
}
}
A classe herda infraestrutura de TestCase. Métodos públicos iniciados por test são descobertos como casos. assertTrue(), assertFalse() e assertSame() registram expectativas e produzem relatórios sem as funções verificar() e testar().
A classificação do teste não mudou.
Usar PHPUnit não transforma o caso em teste unitário. Como PDO, driver SQLite e SQL real colaboram, ele continua sendo um teste de integração.
Aula 4 — Executando uma rotina profissional
Aplicar: rode toda a suíte
composer test
O script chama a cópia do PHPUnit instalada no próprio projeto e ativa o relatório TestDox. Assim, todos usam a dependência declarada pelo projeto, sem exigir uma instalação global do framework.
Exclusao Mensagem
✔ Exclui uma mensagem respondida
✔ Não exclui uma mensagem nova
✔ Não exclui uma mensagem em atendimento
✔ Exclui somente o id informado
OK (4 tests, 8 assertions)
No Linux ou macOS, use vendor/bin/phpunit. O filtro acelera a investigação local, mas não substitui a execução da suíte completa antes de concluir a mudança.
Pratique vermelho, verde e refatoração
Rode composer test e confirme os quatro casos verdes.
Retire temporariamente a condição status = 'respondida' do repositório.
Rode o caso filtrado e leia valores esperado e obtido na falha.
Restaure a regra e execute novamente toda a suíte.
Melhore nomes ou remova repetição sem mudar o comportamento; mantenha a suíte verde.
Ferramenta profissional não elimina teste manual.
A suíte ainda não envia HTTP, valida sessão ou CSRF, abre o navegador, mede acessibilidade nem avalia a experiência da confirmação. Ela protege a integração entre a função de exclusão e o banco. O relatório deve ser lido dentro desse limite.
Verifique sua aprendizagem
Explico o papel de Composer e PHPUnit sem tratá-los como a mesma ferramenta.
Versiono composer.json e composer.lock, mas não vendor/.
Separo dependências da aplicação e de desenvolvimento.
Carrego o código de produção pelo autoload.
Reconheço cada caso, ação e asserção na classe de teste.
Executo a suíte inteira com composer test.
Uso filtro apenas para investigação e volto à suíte completa.
Descrevo com precisão o que os quatro testes não cobrem.
Transfira o aprendizado
Na clínica veterinária, migre os três casos do encerramento de pedidos para PHPUnit. Declare a versão de PHP, PDO, SQLite e PHPUnit no Composer; configure o autoload; use uma fixture independente para cada caso; crie um script composer test; provoque uma falha controlada e explique o relatório antes de restaurar a regra.
A suíte está padronizada; o produto ainda precisa ser encerrado
O projeto agora instala sua ferramenta de testes de modo repetível e protege uma regra crítica com uma suíte reconhecida pelo ecossistema PHP. O capítulo final reunirá critérios de entrega, testes do fluxo completo, configuração segura, operação, documentação e uma revisão consciente do que está pronto — e do que não está.
Web II • 8 horas
Capítulo 12 — Pronto para entregar?
Critérios de aceite, ambiente de execução, configuração externa, erros e logs, backup, restauração, implantação e passagem do sistema.
Amanhã o sistema será usado sem você ao lado
O Café Aurora começará a atender às 7 horas. No computador do aluno, o formulário registra mensagens, o painel exige login e a suíte está verde. Porém, no servidor contratado podem faltar o driver SQLite, permissão para gravar, uma pasta persistente ou a configuração correta da raiz pública.
“Funcionou aqui” comprova apenas um ambiente. Entregar exige tornar requisitos visíveis, ensaiar o caminho completo, proteger dados, preparar recuperação e deixar instruções que outra pessoa consiga seguir. Neste capítulo final, você transformará o projeto construído em uma entrega verificável — sem afirmar que um sistema pequeno está pronto para qualquer escala.
O que você vai aprender
Distinguir código concluído, versão candidata e sistema entregue.
Explicar por que o GitHub Pages publica o curso, mas não executa o projeto PHP.
Verificar versão, extensões e permissão de gravação antes da implantação.
Separar o caminho do banco do código por variável de ambiente.
Diferenciar apresentação de erros em desenvolvimento e registro em produção.
Criar um backup consistente e planejar um ensaio de restauração.
Executar um teste de aceitação do fluxo público e administrativo.
Registrar evidências, responsáveis, limites e próximos passos.
Aula 1 — Definindo o que significa entregar
Entender: endereço publicado não garante sistema executado
Parte
O que o GitHub Pages faz
O que a aplicação exige
Curso Mundo bit Byte
Entrega HTML, CSS, JavaScript e os ZIPs ao navegador.
Continua publicado como conteúdo estático.
Café Aurora do Web II
Armazena e distribui o código, mas não executa PHP.
Servidor com PHP 8.5, PDO_SQLITE e armazenamento persistente.
O projeto baixado precisa de processamento no servidor. A hospedagem escolhida deve permitir definir a pasta public/ como raiz acessível, gravar o SQLite fora dela, manter os dados entre implantações e oferecer HTTPS. Se um desses requisitos não puder ser comprovado, o ambiente ainda não foi aceito.
Transforme expectativa em critério observável
Funciona
As jornadas essenciais produzem o resultado esperado.
Protege
Código interno, banco, credencial e erros não são expostos.
Recupera
Há backup e a restauração foi ensaiada em uma cópia.
Transfere
Outra pessoa consegue instalar, verificar e operar.
Uma frase como “está tudo certo” não é evidência. Um critério de aceite contém ação, resultado esperado e registro da conferência. A versão que passará por esses critérios é a candidata à entrega.
Aula 2 — Verificando o ambiente antes do usuário
Experimentar: execute uma inspeção repetível
O checkpoint acrescenta src/verificar-ambiente.php. Ele confere PHP 8.5, PDO, o driver SQLite e a pasta que receberá o banco:
O comando executa primeiro a inspeção do ambiente e depois a suíte PHPUnit. Ele retorna código diferente de zero diante de uma falha, o que permite interromper uma entrega inadequada. Ainda assim, uma verificação técnica não substitui a navegação real.
Não use o servidor de desenvolvimento em produção
php -S localhost:8000 -t public continua útil no laboratório. O próprio Manual do PHP alerta que o servidor embutido não é um servidor completo e não deve ser usado em rede pública. O ambiente final precisa de um servidor Web mantido para produção.
Erro útil ao programador pode vazar informação ao visitante.
Em desenvolvimento, exibir erros acelera a correção. Em produção, mantenha display_errors desativado e log_errors ativo na configuração do PHP. O usuário recebe uma resposta segura; a equipe consulta o registro do servidor. O local do log depende da hospedagem e deve ser documentado.
Sem configuração, o laboratório continua usando dados/cafe-aurora.sqlite. No servidor, CAFE_AURORA_DB_PATH pode apontar para o armazenamento persistente definido pela hospedagem. O valor não precisa ser gravado no repositório.
Crie uma cópia consistente do SQLite
$pdo = conectarBanco();
$destinoSql = $pdo->quote($destino);
if ($destinoSql === false) {
throw new RuntimeException(
"O destino do backup não pôde ser preparado."
);
}
$pdo->exec("VACUUM INTO $destinoSql");
composer backup
VACUUM INTO produz uma cópia consistente do banco em outro arquivo. O script confirma origem, destino e tamanho antes de declarar sucesso. Por padrão, usa backups/; no servidor, CAFE_AURORA_BACKUP_DIR pode indicar outra pasta protegida e persistente. Esses arquivos são ignorados pelo Git porque podem conter dados pessoais.
Backup criado não é recuperação comprovada.
Guarde uma cópia protegida fora do servidor e ensaie a restauração em outro diretório ou em uma cópia do projeto. Preserve sempre o banco atual; nunca teste apagando a única base válida. Depois da restauração, repita as jornadas essenciais.
Aula 4 — Aplicando o ensaio de entrega
Aplicar: percorra o sistema como visitante e proprietária
Jornada
Ação principal
Resultado esperado
Páginas públicas
Abrir início, cardápio, sobre e contato.
Conteúdo disponível sem erro e navegação coerente.
Contato válido
Enviar uma mensagem e atualizar a página.
Um registro e nenhuma repetição pelo recarregamento.
Validação
Enviar campos ausentes ou inadequados.
Recusa compreensível, sem gravar dados inválidos.
Autenticação
Tentar senha errada e depois a correta.
Recusa inicial e acesso protegido ao painel.
Andamento
Filtrar e mudar o estado de uma mensagem.
Alteração única, filtro preservado e PRG funcionando.
Exclusão
Tentar excluir estados proibido e permitido.
Somente o registro respondido e confirmado é removido.
Saída
Encerrar a sessão e voltar ao painel.
Novo login exigido.
Acesso
Usar teclado, ampliação e tela estreita.
Fluxo essencial legível e operável.
Separe preparação, implantação e aceitação
Em desenvolvimento, execute composer install e composer check.
Crie um backup e registre qual versão está sendo entregue.
No servidor, defina a raiz public/, o caminho persistente do banco, HTTPS, erros e logs.
Instale com composer install --no-dev --optimize-autoloader.
Pelo terminal do servidor, verifique o ambiente, inicialize o banco e crie uma credencial exclusiva.
No endereço final, execute o teste de aceitação e registre problemas encontrados.
Entregue instruções, responsáveis, frequência de backup e limites conhecidos.
A rota de exclusão também passa a carregar o repositório por vendor/autoload.php. Assim, a instalação declarada pelo Composer deixa de ser apenas uma convenção dos testes e integra a execução da aplicação.
Não corrija silenciosamente durante a conferência.
Se um critério falhar, interrompa a entrega, registre a evidência, corrija a causa e repita a suíte e as jornadas afetadas. Marcar um checklist sem executar a ação apenas esconde risco.
Projeto final — entrega do Web II
Entregue:
código organizado com apenas public/ exposto pelo servidor;
composer.json e composer.lock versionados, sem vendor/;
composer check aprovado antes da implantação;
caminho persistente do banco configurado fora da raiz pública;
erros ocultos do visitante e disponíveis no log da equipe;
backup recente e restauração ensaiada em uma cópia;
teste de aceitação concluído no endereço final;
checklist com evidências, responsáveis e limites conhecidos.
A entrega não precisa prometer escala ilimitada nem cobertura perfeita. Ela precisa demonstrar funcionamento, proteção, recuperação e transferência compatíveis com o escopo construído.
Verifique sua aprendizagem
Distingo publicação de arquivos e execução de PHP no servidor.
Transformo requisitos do ambiente em verificações observáveis.
Mantenho dados, credencial, backups e código interno fora da raiz pública.
Configuro o caminho do banco sem alterar o código.
Separo erro mostrado em desenvolvimento de erro registrado em produção.
Não confundo arquivo de backup com restauração comprovada.
Executo testes automatizados e jornadas manuais antes da entrega.
Consigo explicar o que está pronto e quais limites permanecem.
Transfira o aprendizado
Uma clínica veterinária receberá amanhã seu sistema de pedidos de retorno. Escreva seis critérios indispensáveis: ambiente, raiz pública, registro válido, acesso administrativo, backup com restauração e passagem para outra pessoa. Para cada critério, indique ação, resultado esperado e evidência. Não construa outro sistema.
Você partiu de um site estático e construiu um sistema no servidor: recebeu e validou formulários, evitou reenvio, persistiu com SQLite, criou painel autenticado, protegeu alterações, completou o CRUD, automatizou uma regra com PHPUnit e preparou entrega e recuperação. O Web III poderá avançar para arquitetura, APIs, serviços e frameworks sem apagar os fundamentos compreendidos aqui.