PHP Projeto Tarefas com SQLite
A Ana vive esquecendo coisas. Neste projeto você faz para ela uma lista de tarefas que fica guardada num banco de dados: ela adiciona, marca como feita, corrige e apaga. Essas quatro operações têm um nome famoso, CRUD (Create, Read, Update, Delete: criar, ler, atualizar, apagar), e estão por trás de quase todo sistema: loja, blog, agenda, rede social. Quem faz um CRUD bem feito sabe fazer os outros.
- Criar o banco SQLite e a tabela a partir do PHP, com PDO.
- R: ler as linhas com
query+fetchAlle mostrar numa lista. - C: inserir com
prepare+executee voltar para a lista (Post/Redirect/Get). - U: marcar como feita e editar o título com
UPDATE … WHERE id = ?. - D: excluir com
DELETE … WHERE id = ?. - Ver uma injeção de SQL apagar a tabela inteira, e o prepared statement impedir o mesmo ataque.
Antes de começar
| O que usamos | Aula |
|---|---|
Tabelas, linhas, SELECT, INSERT, UPDATE, DELETE | Banco de Dados: o Básico |
new PDO('sqlite:…'), query, fetchAll | Conectar com PDO |
| O CRUD em PDO | CRUD com PDO |
prepare + execute, o perigo da concatenação | Prepared Statements e SQL Injection |
Post/Redirect/Get, htmlspecialchars | Projeto Formulário de Contato |
O projeto tem dois arquivos: banco.php (a conexão) e index.php (a página, que faz tudo). Uma página só, com um campo escondido acao em cada formulário dizendo o que fazer, mantém o projeto pequeno e fácil de seguir.
$ mkdir tarefas $ cd tarefas $ php -S localhost:8000 [Fri Oct 16 21:04:16 2026] PHP 8.5.8 Development Server (http://localhost:8000) started
O sqlite3 que aparece em alguns comandos é o programa de terminal do SQLite, para espiar o banco por fora do PHP (no Debian/Ubuntu: sudo apt install sqlite3). Ele é opcional: tudo funciona sem ele.
As etapas
-
Etapa 1 · O banco e a tabela
O
banco.phpabre o arquivo do banco, cria a tabelatarefasse ela não existir e, só na primeira vez, põe três tarefas de exemplo para a lista não começar vazia.<?php // Conexão com o banco SQLite e criação da tabela date_default_timezone_set('America/Sao_Paulo'); $arquivo = __DIR__ . '/tarefas.sqlite'; $primeiraVez = !file_exists($arquivo); $pdo = new PDO('sqlite:' . $arquivo); $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); $pdo->exec('CREATE TABLE IF NOT EXISTS tarefas ( id INTEGER PRIMARY KEY AUTOINCREMENT, titulo TEXT NOT NULL, feita INTEGER NOT NULL DEFAULT 0, criada_em TEXT NOT NULL )'); // Só para testar: quando o banco acaba de ser criado, três tarefas de exemplo. if ($primeiraVez) { $exemplo = $pdo->prepare('INSERT INTO tarefas (titulo, feita, criada_em) VALUES (?, ?, ?)'); $exemplo->execute(['Comprar pão', 0, date('Y-m-d H:i:s')]); $exemplo->execute(['Pagar a conta de luz', 0, date('Y-m-d H:i:s')]); $exemplo->execute(['Estudar PHP', 1, date('Y-m-d H:i:s')]); }Trecho O que faz $primeiraVez = !file_exists($arquivo)Antes de conectar, olha se o arquivo do banco já existe. O new PDOcria o arquivo, então a pergunta tem que vir antes.new PDO('sqlite:' . $arquivo)Conecta ao banco. Desde o PHP 8, o PDO já avisa os erros com exceções ( PDOException), o que é o que queremos.PDO::FETCH_ASSOCCada linha lida vira um array associativo: $t['titulo'].id INTEGER PRIMARY KEY AUTOINCREMENTUm número único para cada tarefa, que o banco escolhe sozinho (1, 2, 3…) e nunca reutiliza. feita INTEGER NOT NULL DEFAULT 0O SQLite não tem tipo "booleano": usamos 0(pendente) e1(feita). Se ninguém disser nada, é0.criada_em TEXTA data como texto 2026-10-16 21:04:16: nesse formato, ordenar o texto é o mesmo que ordenar a data.$exemplo->execute([…])três vezesUm preparepode ser executado várias vezes com valores diferentes.Para ver o banco funcionando antes de existir uma página, um script de teste no terminal:
<?php require __DIR__ . '/banco.php'; $tarefas = $pdo->query('SELECT id, titulo, feita FROM tarefas')->fetchAll(); foreach ($tarefas as $t) { echo $t['id'], '. ', $t['titulo'], $t['feita'] ? ' (feita)' : '', "\n"; }$ php teste.php 1. Comprar pão 2. Pagar a conta de luz 3. Estudar PHP (feita) $ php teste.php 1. Comprar pão 2. Pagar a conta de luz 3. Estudar PHP (feita) « as de exemplo não se repetem: o banco já existia $ sqlite3 tarefas.sqlite .schema CREATE TABLE tarefas ( id INTEGER PRIMARY KEY AUTOINCREMENT, titulo TEXT NOT NULL, feita INTEGER NOT NULL DEFAULT 0, criada_em TEXT NOT NULL ); CREATE TABLE sqlite_sequence(name,seq);A tabela
sqlite_sequencefoi criada pelo próprio SQLite: é onde ele anota o últimoidusado, por causa doAUTOINCREMENT.Missão: no fim do
teste.php, mostre quantas tarefas existem:$pdo->query('SELECT COUNT(*) FROM tarefas')->fetchColumn()devolve o número direto (fetchColumn= "pegue a primeira coluna"). A saída deve terminar comTotal: 3 tarefa(s).<?php require __DIR__ . '/banco.php'; $tarefas = $pdo->query('SELECT id, titulo, feita FROM tarefas')->fetchAll(); foreach ($tarefas as $t) { echo $t['id'], '. ', $t['titulo'], $t['feita'] ? ' (feita)' : '', "\n"; }<?php // Conexão com o banco SQLite e criação da tabela date_default_timezone_set('America/Sao_Paulo'); $arquivo = __DIR__ . '/tarefas.sqlite'; $primeiraVez = !file_exists($arquivo); $pdo = new PDO('sqlite:' . $arquivo); $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); $pdo->exec('CREATE TABLE IF NOT EXISTS tarefas ( id INTEGER PRIMARY KEY AUTOINCREMENT, titulo TEXT NOT NULL, feita INTEGER NOT NULL DEFAULT 0, criada_em TEXT NOT NULL )'); // Só para testar: quando o banco acaba de ser criado, três tarefas de exemplo. if ($primeiraVez) { $exemplo = $pdo->prepare('INSERT INTO tarefas (titulo, feita, criada_em) VALUES (?, ?, ?)'); $exemplo->execute(['Comprar pão', 0, date('Y-m-d H:i:s')]); $exemplo->execute(['Pagar a conta de luz', 0, date('Y-m-d H:i:s')]); $exemplo->execute(['Estudar PHP', 1, date('Y-m-d H:i:s')]); }<?php require __DIR__ . '/banco.php'; $tarefas = $pdo->query('SELECT id, titulo, feita FROM tarefas')->fetchAll(); foreach ($tarefas as $t) { echo $t['id'], '. ', $t['titulo'], $t['feita'] ? ' (feita)' : '', "\n"; } $total = $pdo->query('SELECT COUNT(*) FROM tarefas')->fetchColumn(); echo "Total: $total tarefa(s)\n"; -
Etapa 2 · R de Read: mostrar a lista
A primeira versão do
index.phpsó lê e mostra:<?php require __DIR__ . '/banco.php'; function e(string $texto): string { return htmlspecialchars($texto); } $tarefas = $pdo->query('SELECT id, titulo, feita FROM tarefas ORDER BY feita, id')->fetchAll(); $pendentes = count(array_filter($tarefas, fn($t) => $t['feita'] == 0)); ?> <!DOCTYPE html> <html lang="pt-BR"> <head> <meta charset="utf-8"> <title>Tarefas</title> </head> <body> <h1>Tarefas da Ana</h1> <p><?= $pendentes ?> pendente(s) de <?= count($tarefas) ?> tarefa(s).</p> <ul> <?php foreach ($tarefas as $t): ?> <li> <span class="tarefa"><?= $t['feita'] ? '<s>' . e($t['titulo']) . '</s>' : e($t['titulo']) ?></span> </li> <?php endforeach; ?> </ul> </body> </html>- $pdo->query('SELECT … ORDER BY feita, id')
- Roda um SQL fixo (sem nada vindo do usuário, por isso pode ser
querydireto).ORDER BY feita, id: primeiro as pendentes (0), depois as feitas (1); dentro de cada grupo, na ordem de criação. - ->fetchAll()
- Busca todas as linhas de uma vez, num array de arrays.
- array_filter($tarefas, fn($t) => $t['feita'] == 0)
- Separa as pendentes, para contar (veja Arrow Functions). No PHP 8.1+, o SQLite devolve
feitacomo número (int(0)ouint(1)); versões antigas devolviam texto ("0"). O==funciona nos dois casos. - '<s>' . e($t['titulo']) . '</s>'
- Tarefa feita aparece riscada. Repare: o
<s>é nosso e não é escapado; o título, que veio do usuário, é.
$ curl -s localhost:8000/ | grep -E 'pendente|class="tarefa"' <p>2 pendente(s) de 3 tarefa(s).</p> <span class="tarefa">Comprar pão</span> <span class="tarefa">Pagar a conta de luz</span> <span class="tarefa"><s>Estudar PHP</s></span>Missão: faça a página aceitar
?ver=pendentesno endereço: nesse caso, o SQL ganhaWHERE feita = 0e só as pendentes aparecem. Use umifcom($_GET['ver'] ?? '') === 'pendentes'e duas consultas fixas. (Não cole o valor do$_GETno SQL: a etapa 7 mostra por quê.)<?php require __DIR__ . '/banco.php'; function e(string $texto): string { return htmlspecialchars($texto); } $tarefas = $pdo->query('SELECT id, titulo, feita FROM tarefas ORDER BY feita, id')->fetchAll(); $pendentes = count(array_filter($tarefas, fn($t) => $t['feita'] == 0)); ?> <!DOCTYPE html> <html lang="pt-BR"> <head> <meta charset="utf-8"> <title>Tarefas</title> </head> <body> <h1>Tarefas da Ana</h1> <p><?= $pendentes ?> pendente(s) de <?= count($tarefas) ?> tarefa(s).</p> <ul> <?php foreach ($tarefas as $t): ?> <li> <span class="tarefa"><?= $t['feita'] ? '<s>' . e($t['titulo']) . '</s>' : e($t['titulo']) ?></span> </li> <?php endforeach; ?> </ul> </body> </html><?php // Conexão com o banco SQLite e criação da tabela date_default_timezone_set('America/Sao_Paulo'); $arquivo = __DIR__ . '/tarefas.sqlite'; $primeiraVez = !file_exists($arquivo); $pdo = new PDO('sqlite:' . $arquivo); $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); $pdo->exec('CREATE TABLE IF NOT EXISTS tarefas ( id INTEGER PRIMARY KEY AUTOINCREMENT, titulo TEXT NOT NULL, feita INTEGER NOT NULL DEFAULT 0, criada_em TEXT NOT NULL )'); // Só para testar: quando o banco acaba de ser criado, três tarefas de exemplo. if ($primeiraVez) { $exemplo = $pdo->prepare('INSERT INTO tarefas (titulo, feita, criada_em) VALUES (?, ?, ?)'); $exemplo->execute(['Comprar pão', 0, date('Y-m-d H:i:s')]); $exemplo->execute(['Pagar a conta de luz', 0, date('Y-m-d H:i:s')]); $exemplo->execute(['Estudar PHP', 1, date('Y-m-d H:i:s')]); }<?php require __DIR__ . '/banco.php'; function e(string $texto): string { return htmlspecialchars($texto); } if (($_GET['ver'] ?? '') === 'pendentes') { $tarefas = $pdo->query('SELECT id, titulo, feita FROM tarefas WHERE feita = 0 ORDER BY id')->fetchAll(); } else { $tarefas = $pdo->query('SELECT id, titulo, feita FROM tarefas ORDER BY feita, id')->fetchAll(); } $pendentes = count(array_filter($tarefas, fn($t) => $t['feita'] == 0)); ?> <!DOCTYPE html> <html lang="pt-BR"> <head> <meta charset="utf-8"> <title>Tarefas</title> </head> <body> <h1>Tarefas da Ana</h1> <p><?= $pendentes ?> pendente(s) de <?= count($tarefas) ?> tarefa(s).</p> <ul> <?php foreach ($tarefas as $t): ?> <li> <span class="tarefa"><?= $t['feita'] ? '<s>' . e($t['titulo']) . '</s>' : e($t['titulo']) ?></span> </li> <?php endforeach; ?> </ul> </body> </html> -
Etapa 3 · C de Create: adicionar tarefas
Um formulário no topo da lista. O campo escondido
acaodiz ao PHP o que o formulário quer:<form method="post" action="index.php"> <input type="hidden" name="acao" value="criar"> <input name="titulo" value="<?= e($novo) ?>" placeholder="Nova tarefa" maxlength="100"> <button>Adicionar</button> </form>
E, no topo do arquivo, o tratamento do envio:
function voltar(): never { header('Location: index.php', true, 303); exit; } $erro = ''; $novo = ''; if ($_SERVER['REQUEST_METHOD'] === 'POST') { $acao = $_POST['acao'] ?? ''; if ($acao === 'criar') { $novo = trim($_POST['titulo'] ?? ''); if ($novo === '' || mb_strlen($novo) > 100) { $erro = 'Escreva a tarefa (até 100 caracteres).'; } else { $sql = $pdo->prepare('INSERT INTO tarefas (titulo, criada_em) VALUES (?, ?)'); $sql->execute([$novo, date('Y-m-d H:i:s')]); voltar(); } } }- $pdo->prepare('INSERT … VALUES (?, ?)')
- Prepara o SQL com dois "buracos". O texto do SQL é sempre o mesmo, não importa o que a Ana digite.
- $sql->execute([$novo, date(…)])
- Envia os valores separados, na ordem dos
?. O banco os trata como dados, nunca como comandos. - feita
- Não aparece no
INSERT: vale oDEFAULT 0da tabela. - voltar()
- Post/Redirect/Get: depois de gravar, volta para a lista com um GET. O F5 não duplica a tarefa.
$ curl -si -d 'acao=criar&titulo=Ligar para a Carla' localhost:8000/ | grep -E 'HTTP|Location' HTTP/1.1 303 See Other Location: index.php $ curl -s localhost:8000/ | grep -E 'pendente|class="tarefa"' <p>3 pendente(s) de 4 tarefa(s).</p> <span class="tarefa">Comprar pão</span> <span class="tarefa">Pagar a conta de luz</span> <span class="tarefa">Ligar para a Carla</span> <span class="tarefa"><s>Estudar PHP</s></span> $ curl -s -d 'acao=criar&titulo= ' localhost:8000/ | grep erro <p class="erro">Escreva a tarefa (até 100 caracteres).</p> $ curl -sL -d 'acao=criar&titulo=<b>Urgente</b>' localhost:8000/ | grep 'class="tarefa"' <span class="tarefa">Comprar pão</span> <span class="tarefa">Pagar a conta de luz</span> <span class="tarefa">Ligar para a Carla</span> <span class="tarefa"><b>Urgente</b></span> <span class="tarefa"><s>Estudar PHP</s></span> $ sqlite3 tarefas.sqlite 'SELECT id, titulo FROM tarefas' 1|Comprar pão 2|Pagar a conta de luz 3|Estudar PHP 4|Ligar para a Carla 5|<b>Urgente</b>Repare nos dois últimos comandos: o banco guarda exatamente o que foi digitado (
<b>Urgente</b>), e a página escapa na hora de mostrar. Essa é a divisão certa: o prepared statement protege o banco; ohtmlspecialcharsprotege a página. Uma proteção não substitui a outra.Missão: não deixe cadastrar uma tarefa repetida. Antes do
INSERT, conte quantas tarefas têm o mesmo título (SELECT COUNT(*) FROM tarefas WHERE titulo = ?, comprepare/execute/fetchColumn); se for mais que zero, o erro éEssa tarefa já está na lista.A caixa tenta cadastrar "Comprar pão" de novo.<?php require __DIR__ . '/banco.php'; function e(string $texto): string { return htmlspecialchars($texto); } function voltar(): never { header('Location: index.php', true, 303); exit; } $erro = ''; $novo = ''; if ($_SERVER['REQUEST_METHOD'] === 'POST') { $acao = $_POST['acao'] ?? ''; if ($acao === 'criar') { $novo = trim($_POST['titulo'] ?? ''); if ($novo === '' || mb_strlen($novo) > 100) { $erro = 'Escreva a tarefa (até 100 caracteres).'; } else { $sql = $pdo->prepare('INSERT INTO tarefas (titulo, criada_em) VALUES (?, ?)'); $sql->execute([$novo, date('Y-m-d H:i:s')]); voltar(); } } } $tarefas = $pdo->query('SELECT id, titulo, feita FROM tarefas ORDER BY feita, id')->fetchAll(); $pendentes = count(array_filter($tarefas, fn($t) => $t['feita'] == 0)); ?> <!DOCTYPE html> <html lang="pt-BR"> <head> <meta charset="utf-8"> <title>Tarefas</title> </head> <body> <h1>Tarefas da Ana</h1> <p><?= $pendentes ?> pendente(s) de <?= count($tarefas) ?> tarefa(s).</p> <?php if ($erro): ?> <p class="erro"><?= e($erro) ?></p> <?php endif; ?> <form method="post" action="index.php"> <input type="hidden" name="acao" value="criar"> <input name="titulo" value="<?= e($novo) ?>" placeholder="Nova tarefa" maxlength="100"> <button>Adicionar</button> </form> <ul> <?php foreach ($tarefas as $t): ?> <li> <span class="tarefa"><?= $t['feita'] ? '<s>' . e($t['titulo']) . '</s>' : e($t['titulo']) ?></span> </li> <?php endforeach; ?> </ul> </body> </html><?php // Conexão com o banco SQLite e criação da tabela date_default_timezone_set('America/Sao_Paulo'); $arquivo = __DIR__ . '/tarefas.sqlite'; $primeiraVez = !file_exists($arquivo); $pdo = new PDO('sqlite:' . $arquivo); $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); $pdo->exec('CREATE TABLE IF NOT EXISTS tarefas ( id INTEGER PRIMARY KEY AUTOINCREMENT, titulo TEXT NOT NULL, feita INTEGER NOT NULL DEFAULT 0, criada_em TEXT NOT NULL )'); // Só para testar: quando o banco acaba de ser criado, três tarefas de exemplo. if ($primeiraVez) { $exemplo = $pdo->prepare('INSERT INTO tarefas (titulo, feita, criada_em) VALUES (?, ?, ?)'); $exemplo->execute(['Comprar pão', 0, date('Y-m-d H:i:s')]); $exemplo->execute(['Pagar a conta de luz', 0, date('Y-m-d H:i:s')]); $exemplo->execute(['Estudar PHP', 1, date('Y-m-d H:i:s')]); }<?php require __DIR__ . '/banco.php'; function e(string $texto): string { return htmlspecialchars($texto); } function voltar(): never { header('Location: index.php', true, 303); exit; } $erro = ''; $novo = ''; if ($_SERVER['REQUEST_METHOD'] === 'POST') { $acao = $_POST['acao'] ?? ''; if ($acao === 'criar') { $novo = trim($_POST['titulo'] ?? ''); if ($novo === '' || mb_strlen($novo) > 100) { $erro = 'Escreva a tarefa (até 100 caracteres).'; } else { $busca = $pdo->prepare('SELECT COUNT(*) FROM tarefas WHERE titulo = ?'); $busca->execute([$novo]); if ($busca->fetchColumn() > 0) { $erro = 'Essa tarefa já está na lista.'; } } if (!$erro) { $sql = $pdo->prepare('INSERT INTO tarefas (titulo, criada_em) VALUES (?, ?)'); $sql->execute([$novo, date('Y-m-d H:i:s')]); voltar(); } } } $tarefas = $pdo->query('SELECT id, titulo, feita FROM tarefas ORDER BY feita, id')->fetchAll(); $pendentes = count(array_filter($tarefas, fn($t) => $t['feita'] == 0)); ?> <!DOCTYPE html> <html lang="pt-BR"> <head> <meta charset="utf-8"> <title>Tarefas</title> </head> <body> <h1>Tarefas da Ana</h1> <p><?= $pendentes ?> pendente(s) de <?= count($tarefas) ?> tarefa(s).</p> <?php if ($erro): ?> <p class="erro"><?= e($erro) ?></p> <?php endif; ?> <form method="post" action="index.php"> <input type="hidden" name="acao" value="criar"> <input name="titulo" value="<?= e($novo) ?>" placeholder="Nova tarefa" maxlength="100"> <button>Adicionar</button> </form> <ul> <?php foreach ($tarefas as $t): ?> <li> <span class="tarefa"><?= $t['feita'] ? '<s>' . e($t['titulo']) . '</s>' : e($t['titulo']) ?></span> </li> <?php endforeach; ?> </ul> </body> </html> -
Etapa 4 · U de Update: marcar como feita
Cada tarefa ganha um botão. Ele é um pequeno formulário POST que leva a ação e o
idda tarefa:<form method="post" action="index.php" style="display:inline"> <input type="hidden" name="acao" value="alternar"> <input type="hidden" name="id" value="<?= $t['id'] ?>"> <button><?= $t['feita'] ? 'Desfazer' : 'Feito!' ?></button> </form>
$id = (int) ($_POST['id'] ?? 0); // ... if ($acao === 'alternar') { $sql = $pdo->prepare('UPDATE tarefas SET feita = 1 - feita WHERE id = ?'); $sql->execute([$id]); voltar(); }Trecho O que faz Botão dentro de <form method="post">Mudar dados é sempre POST, nunca um link (GET): links são seguidos por robôs, pré-carregados por navegadores e podem ser disparados por outros sites (veja a etapa 4 do projeto de login). (int) ($_POST['id'] ?? 0)O idchega como texto; o(int)garante um número inteiro ("abc"vira0, que não é id de ninguém).SET feita = 1 - feitaUm truque de conta: se era 0, vira 1; se era 1, vira 0. O mesmo botão marca e desmarca. WHERE id = ?Só a tarefa daquele id. UmUPDATEsemWHEREmuda todas as linhas da tabela.$ curl -s localhost:8000/ | grep -A4 'value="alternar"' | head -5 <input type="hidden" name="acao" value="alternar"> <input type="hidden" name="id" value="1"> <button>Feito!</button> </form> <span class="tarefa">Comprar pão</span> $ curl -si -d 'acao=alternar&id=1' localhost:8000/ | grep -E 'HTTP|Location' HTTP/1.1 303 See Other Location: index.php $ curl -s localhost:8000/ | grep -E 'pendente|class="tarefa"' <p>1 pendente(s) de 3 tarefa(s).</p> <span class="tarefa">Pagar a conta de luz</span> <span class="tarefa"><s>Comprar pão</s></span> « feita: foi para baixo, riscada <span class="tarefa"><s>Estudar PHP</s></span> $ curl -sL -d 'acao=alternar&id=1' localhost:8000/ | grep -E 'pendente|class="tarefa"' <p>2 pendente(s) de 3 tarefa(s).</p> <span class="tarefa">Comprar pão</span> « desfeita: voltou <span class="tarefa">Pagar a conta de luz</span> <span class="tarefa"><s>Estudar PHP</s></span>Missão: um botão Marcar todas como feitas (formulário com
acao=todas) e a ação correspondente:$pdo->exec('UPDATE tarefas SET feita = 1');seguido devoltar();. Aqui oUPDATEsemWHEREé de propósito! A caixa já enviaacao=todas.<?php require __DIR__ . '/banco.php'; function e(string $texto): string { return htmlspecialchars($texto); } function voltar(): never { header('Location: index.php', true, 303); exit; } $erro = ''; $novo = ''; if ($_SERVER['REQUEST_METHOD'] === 'POST') { $acao = $_POST['acao'] ?? ''; $id = (int) ($_POST['id'] ?? 0); if ($acao === 'criar') { $novo = trim($_POST['titulo'] ?? ''); if ($novo === '' || mb_strlen($novo) > 100) { $erro = 'Escreva a tarefa (até 100 caracteres).'; } else { $sql = $pdo->prepare('INSERT INTO tarefas (titulo, criada_em) VALUES (?, ?)'); $sql->execute([$novo, date('Y-m-d H:i:s')]); voltar(); } } if ($acao === 'alternar') { $sql = $pdo->prepare('UPDATE tarefas SET feita = 1 - feita WHERE id = ?'); $sql->execute([$id]); voltar(); } } $tarefas = $pdo->query('SELECT id, titulo, feita FROM tarefas ORDER BY feita, id')->fetchAll(); $pendentes = count(array_filter($tarefas, fn($t) => $t['feita'] == 0)); ?> <!DOCTYPE html> <html lang="pt-BR"> <head> <meta charset="utf-8"> <title>Tarefas</title> </head> <body> <h1>Tarefas da Ana</h1> <p><?= $pendentes ?> pendente(s) de <?= count($tarefas) ?> tarefa(s).</p> <?php if ($erro): ?> <p class="erro"><?= e($erro) ?></p> <?php endif; ?> <form method="post" action="index.php"> <input type="hidden" name="acao" value="criar"> <input name="titulo" value="<?= e($novo) ?>" placeholder="Nova tarefa" maxlength="100"> <button>Adicionar</button> </form> <ul> <?php foreach ($tarefas as $t): ?> <li> <form method="post" action="index.php" style="display:inline"> <input type="hidden" name="acao" value="alternar"> <input type="hidden" name="id" value="<?= $t['id'] ?>"> <button><?= $t['feita'] ? 'Desfazer' : 'Feito!' ?></button> </form> <span class="tarefa"><?= $t['feita'] ? '<s>' . e($t['titulo']) . '</s>' : e($t['titulo']) ?></span> </li> <?php endforeach; ?> </ul> </body> </html><?php // Conexão com o banco SQLite e criação da tabela date_default_timezone_set('America/Sao_Paulo'); $arquivo = __DIR__ . '/tarefas.sqlite'; $primeiraVez = !file_exists($arquivo); $pdo = new PDO('sqlite:' . $arquivo); $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); $pdo->exec('CREATE TABLE IF NOT EXISTS tarefas ( id INTEGER PRIMARY KEY AUTOINCREMENT, titulo TEXT NOT NULL, feita INTEGER NOT NULL DEFAULT 0, criada_em TEXT NOT NULL )'); // Só para testar: quando o banco acaba de ser criado, três tarefas de exemplo. if ($primeiraVez) { $exemplo = $pdo->prepare('INSERT INTO tarefas (titulo, feita, criada_em) VALUES (?, ?, ?)'); $exemplo->execute(['Comprar pão', 0, date('Y-m-d H:i:s')]); $exemplo->execute(['Pagar a conta de luz', 0, date('Y-m-d H:i:s')]); $exemplo->execute(['Estudar PHP', 1, date('Y-m-d H:i:s')]); }<?php require __DIR__ . '/banco.php'; function e(string $texto): string { return htmlspecialchars($texto); } function voltar(): never { header('Location: index.php', true, 303); exit; } $erro = ''; $novo = ''; if ($_SERVER['REQUEST_METHOD'] === 'POST') { $acao = $_POST['acao'] ?? ''; $id = (int) ($_POST['id'] ?? 0); if ($acao === 'criar') { $novo = trim($_POST['titulo'] ?? ''); if ($novo === '' || mb_strlen($novo) > 100) { $erro = 'Escreva a tarefa (até 100 caracteres).'; } else { $sql = $pdo->prepare('INSERT INTO tarefas (titulo, criada_em) VALUES (?, ?)'); $sql->execute([$novo, date('Y-m-d H:i:s')]); voltar(); } } if ($acao === 'todas') { $pdo->exec('UPDATE tarefas SET feita = 1'); voltar(); } if ($acao === 'alternar') { $sql = $pdo->prepare('UPDATE tarefas SET feita = 1 - feita WHERE id = ?'); $sql->execute([$id]); voltar(); } } $tarefas = $pdo->query('SELECT id, titulo, feita FROM tarefas ORDER BY feita, id')->fetchAll(); $pendentes = count(array_filter($tarefas, fn($t) => $t['feita'] == 0)); ?> <!DOCTYPE html> <html lang="pt-BR"> <head> <meta charset="utf-8"> <title>Tarefas</title> </head> <body> <h1>Tarefas da Ana</h1> <p><?= $pendentes ?> pendente(s) de <?= count($tarefas) ?> tarefa(s).</p> <?php if ($erro): ?> <p class="erro"><?= e($erro) ?></p> <?php endif; ?> <form method="post" action="index.php"> <input type="hidden" name="acao" value="criar"> <input name="titulo" value="<?= e($novo) ?>" placeholder="Nova tarefa" maxlength="100"> <button>Adicionar</button> </form> <form method="post" action="index.php"> <input type="hidden" name="acao" value="todas"> <button>Marcar todas como feitas</button> </form> <ul> <?php foreach ($tarefas as $t): ?> <li> <form method="post" action="index.php" style="display:inline"> <input type="hidden" name="acao" value="alternar"> <input type="hidden" name="id" value="<?= $t['id'] ?>"> <button><?= $t['feita'] ? 'Desfazer' : 'Feito!' ?></button> </form> <span class="tarefa"><?= $t['feita'] ? '<s>' . e($t['titulo']) . '</s>' : e($t['titulo']) ?></span> </li> <?php endforeach; ?> </ul> </body> </html> -
Etapa 5 · U de novo: editar o título
Para corrigir um título, cada tarefa ganha um link
editarque abreindex.php?editar=2. Nessa visita, a linha da tarefa 2 aparece como um formulário já preenchido. Abrir o formulário é só ler (GET); salvar é mudar (POST).// Tarefa aberta para edição: index.php?editar=3 if ($editando === null && isset($_GET['editar'])) { $sql = $pdo->prepare('SELECT id, titulo FROM tarefas WHERE id = ?'); $sql->execute([(int) $_GET['editar']]); $editando = $sql->fetch() ?: null; }if ($acao === 'editar') { $titulo = trim($_POST['titulo'] ?? ''); if ($titulo === '' || mb_strlen($titulo) > 100) { $erro = 'Escreva a tarefa (até 100 caracteres).'; $editando = ['id' => $id, 'titulo' => $titulo]; } else { $sql = $pdo->prepare('UPDATE tarefas SET titulo = ? WHERE id = ?'); $sql->execute([$titulo, $id]); voltar(); } }Na lista, um
ifescolhe entre mostrar a linha normal ou o formulário de edição:<?php if ($editando && $editando['id'] == $t['id']): ?> <form method="post" action="index.php"> <input type="hidden" name="acao" value="editar"> <input type="hidden" name="id" value="<?= $t['id'] ?>"> <input name="titulo" value="<?= e($editando['titulo']) ?>" maxlength="100"> <button>Salvar</button> <a href="index.php">Cancelar</a> </form> <?php else: ?> ... a linha normal, agora com o link: <a href="index.php?editar=<?= $t['id'] ?>">editar</a> <?php endif; ?>- $sql->fetch() ?: null
fetchdevolve a linha oufalsese oidnão existe. O?:troca ofalsepornull: "nenhuma tarefa em edição".- $editando = ['id' => $id, 'titulo' => $titulo]
- Se o título novo for inválido, o formulário de edição reaparece com o erro e com o que foi digitado, em vez de sumir.
- UPDATE … SET titulo = ? WHERE id = ?
- Dois
?, dois valores noexecute, na mesma ordem.
$ curl -s 'localhost:8000/?editar=2' | grep -B1 -A4 'value="editar"' <form method="post" action="index.php"> <input type="hidden" name="acao" value="editar"> <input type="hidden" name="id" value="2"> <input name="titulo" value="Pagar a conta de luz" maxlength="100"> <button>Salvar</button> <a href="index.php">Cancelar</a> </form> $ curl -s 'localhost:8000/?editar=99' | grep -c 'value="editar"' 0 « id inexistente: nenhum formulário de edição $ curl -s -d 'acao=editar&id=2&titulo=' localhost:8000/ | grep -E 'erro|name="titulo"' <p class="erro">Escreva a tarefa (até 100 caracteres).</p> <input name="titulo" value="" placeholder="Nova tarefa" maxlength="100"> <input name="titulo" value="" maxlength="100"> « o formulário de edição continua aberto $ curl -sL -d 'acao=editar&id=2&titulo=Pagar a luz e a água' localhost:8000/ | grep 'class="tarefa"' <span class="tarefa">Comprar pão</span> <span class="tarefa">Pagar a luz e a água</span> <span class="tarefa"><s>Estudar PHP</s></span>Missão: com
?editar=99(uma tarefa que não existe), a página hoje fica calada. Depois dofetch, se$editandoficounull, ponha em$erroa mensagemTarefa não encontrada.<?php require __DIR__ . '/banco.php'; function e(string $texto): string { return htmlspecialchars($texto); } function voltar(): never { header('Location: index.php', true, 303); exit; } $erro = ''; $novo = ''; $editando = null; if ($_SERVER['REQUEST_METHOD'] === 'POST') { $acao = $_POST['acao'] ?? ''; $id = (int) ($_POST['id'] ?? 0); if ($acao === 'criar') { $novo = trim($_POST['titulo'] ?? ''); if ($novo === '' || mb_strlen($novo) > 100) { $erro = 'Escreva a tarefa (até 100 caracteres).'; } else { $sql = $pdo->prepare('INSERT INTO tarefas (titulo, criada_em) VALUES (?, ?)'); $sql->execute([$novo, date('Y-m-d H:i:s')]); voltar(); } } if ($acao === 'alternar') { $sql = $pdo->prepare('UPDATE tarefas SET feita = 1 - feita WHERE id = ?'); $sql->execute([$id]); voltar(); } if ($acao === 'editar') { $titulo = trim($_POST['titulo'] ?? ''); if ($titulo === '' || mb_strlen($titulo) > 100) { $erro = 'Escreva a tarefa (até 100 caracteres).'; $editando = ['id' => $id, 'titulo' => $titulo]; } else { $sql = $pdo->prepare('UPDATE tarefas SET titulo = ? WHERE id = ?'); $sql->execute([$titulo, $id]); voltar(); } } } // Tarefa aberta para edição: index.php?editar=3 if ($editando === null && isset($_GET['editar'])) { $sql = $pdo->prepare('SELECT id, titulo FROM tarefas WHERE id = ?'); $sql->execute([(int) $_GET['editar']]); $editando = $sql->fetch() ?: null; } $tarefas = $pdo->query('SELECT id, titulo, feita FROM tarefas ORDER BY feita, id')->fetchAll(); $pendentes = count(array_filter($tarefas, fn($t) => $t['feita'] == 0)); ?> <!DOCTYPE html> <html lang="pt-BR"> <head> <meta charset="utf-8"> <title>Tarefas</title> </head> <body> <h1>Tarefas da Ana</h1> <p><?= $pendentes ?> pendente(s) de <?= count($tarefas) ?> tarefa(s).</p> <?php if ($erro): ?> <p class="erro"><?= e($erro) ?></p> <?php endif; ?> <form method="post" action="index.php"> <input type="hidden" name="acao" value="criar"> <input name="titulo" value="<?= e($novo) ?>" placeholder="Nova tarefa" maxlength="100"> <button>Adicionar</button> </form> <ul> <?php foreach ($tarefas as $t): ?> <li> <?php if ($editando && $editando['id'] == $t['id']): ?> <form method="post" action="index.php"> <input type="hidden" name="acao" value="editar"> <input type="hidden" name="id" value="<?= $t['id'] ?>"> <input name="titulo" value="<?= e($editando['titulo']) ?>" maxlength="100"> <button>Salvar</button> <a href="index.php">Cancelar</a> </form> <?php else: ?> <form method="post" action="index.php" style="display:inline"> <input type="hidden" name="acao" value="alternar"> <input type="hidden" name="id" value="<?= $t['id'] ?>"> <button><?= $t['feita'] ? 'Desfazer' : 'Feito!' ?></button> </form> <span class="tarefa"><?= $t['feita'] ? '<s>' . e($t['titulo']) . '</s>' : e($t['titulo']) ?></span> <a href="index.php?editar=<?= $t['id'] ?>">editar</a> <?php endif; ?> </li> <?php endforeach; ?> </ul> </body> </html><?php // Conexão com o banco SQLite e criação da tabela date_default_timezone_set('America/Sao_Paulo'); $arquivo = __DIR__ . '/tarefas.sqlite'; $primeiraVez = !file_exists($arquivo); $pdo = new PDO('sqlite:' . $arquivo); $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); $pdo->exec('CREATE TABLE IF NOT EXISTS tarefas ( id INTEGER PRIMARY KEY AUTOINCREMENT, titulo TEXT NOT NULL, feita INTEGER NOT NULL DEFAULT 0, criada_em TEXT NOT NULL )'); // Só para testar: quando o banco acaba de ser criado, três tarefas de exemplo. if ($primeiraVez) { $exemplo = $pdo->prepare('INSERT INTO tarefas (titulo, feita, criada_em) VALUES (?, ?, ?)'); $exemplo->execute(['Comprar pão', 0, date('Y-m-d H:i:s')]); $exemplo->execute(['Pagar a conta de luz', 0, date('Y-m-d H:i:s')]); $exemplo->execute(['Estudar PHP', 1, date('Y-m-d H:i:s')]); }<?php require __DIR__ . '/banco.php'; function e(string $texto): string { return htmlspecialchars($texto); } function voltar(): never { header('Location: index.php', true, 303); exit; } $erro = ''; $novo = ''; $editando = null; if ($_SERVER['REQUEST_METHOD'] === 'POST') { $acao = $_POST['acao'] ?? ''; $id = (int) ($_POST['id'] ?? 0); if ($acao === 'criar') { $novo = trim($_POST['titulo'] ?? ''); if ($novo === '' || mb_strlen($novo) > 100) { $erro = 'Escreva a tarefa (até 100 caracteres).'; } else { $sql = $pdo->prepare('INSERT INTO tarefas (titulo, criada_em) VALUES (?, ?)'); $sql->execute([$novo, date('Y-m-d H:i:s')]); voltar(); } } if ($acao === 'alternar') { $sql = $pdo->prepare('UPDATE tarefas SET feita = 1 - feita WHERE id = ?'); $sql->execute([$id]); voltar(); } if ($acao === 'editar') { $titulo = trim($_POST['titulo'] ?? ''); if ($titulo === '' || mb_strlen($titulo) > 100) { $erro = 'Escreva a tarefa (até 100 caracteres).'; $editando = ['id' => $id, 'titulo' => $titulo]; } else { $sql = $pdo->prepare('UPDATE tarefas SET titulo = ? WHERE id = ?'); $sql->execute([$titulo, $id]); voltar(); } } } // Tarefa aberta para edição: index.php?editar=3 if ($editando === null && isset($_GET['editar'])) { $sql = $pdo->prepare('SELECT id, titulo FROM tarefas WHERE id = ?'); $sql->execute([(int) $_GET['editar']]); $editando = $sql->fetch() ?: null; if ($editando === null) { $erro = 'Tarefa não encontrada.'; } } $tarefas = $pdo->query('SELECT id, titulo, feita FROM tarefas ORDER BY feita, id')->fetchAll(); $pendentes = count(array_filter($tarefas, fn($t) => $t['feita'] == 0)); ?> <!DOCTYPE html> <html lang="pt-BR"> <head> <meta charset="utf-8"> <title>Tarefas</title> </head> <body> <h1>Tarefas da Ana</h1> <p><?= $pendentes ?> pendente(s) de <?= count($tarefas) ?> tarefa(s).</p> <?php if ($erro): ?> <p class="erro"><?= e($erro) ?></p> <?php endif; ?> <form method="post" action="index.php"> <input type="hidden" name="acao" value="criar"> <input name="titulo" value="<?= e($novo) ?>" placeholder="Nova tarefa" maxlength="100"> <button>Adicionar</button> </form> <ul> <?php foreach ($tarefas as $t): ?> <li> <?php if ($editando && $editando['id'] == $t['id']): ?> <form method="post" action="index.php"> <input type="hidden" name="acao" value="editar"> <input type="hidden" name="id" value="<?= $t['id'] ?>"> <input name="titulo" value="<?= e($editando['titulo']) ?>" maxlength="100"> <button>Salvar</button> <a href="index.php">Cancelar</a> </form> <?php else: ?> <form method="post" action="index.php" style="display:inline"> <input type="hidden" name="acao" value="alternar"> <input type="hidden" name="id" value="<?= $t['id'] ?>"> <button><?= $t['feita'] ? 'Desfazer' : 'Feito!' ?></button> </form> <span class="tarefa"><?= $t['feita'] ? '<s>' . e($t['titulo']) . '</s>' : e($t['titulo']) ?></span> <a href="index.php?editar=<?= $t['id'] ?>">editar</a> <?php endif; ?> </li> <?php endforeach; ?> </ul> </body> </html> -
Etapa 6 · D de Delete: excluir
O último botão de cada tarefa. Mesmo padrão do "Feito!": um formulário POST com a ação e o
id.<form method="post" action="index.php" style="display:inline"> <input type="hidden" name="acao" value="excluir"> <input type="hidden" name="id" value="<?= $t['id'] ?>"> <button>excluir</button> </form>
if ($acao === 'excluir') { $sql = $pdo->prepare('DELETE FROM tarefas WHERE id = ?'); $sql->execute([$id]); voltar(); }$ curl -sL -d 'acao=excluir&id=3' localhost:8000/ | grep -E 'pendente|class="tarefa"' <p>2 pendente(s) de 2 tarefa(s).</p> <span class="tarefa">Comprar pão</span> <span class="tarefa">Pagar a conta de luz</span> $ sqlite3 tarefas.sqlite 'SELECT id, titulo, feita FROM tarefas' 1|Comprar pão|0 2|Pagar a conta de luz|0Cuidado com oWHEREDELETE FROM tarefassemWHEREapaga a tabela inteira, sem perguntar e sem "desfazer". Sempre que escreverUPDATEouDELETE, escreva oWHEREprimeiro. Num sistema de verdade, muitas vezes nem se apaga: marca-se a linha como "excluída" (uma colunaexcluida_em), para poder recuperar.Missão: um botão Limpar concluídas no fim da lista (
acao=limpar) que apaga só as feitas:DELETE FROM tarefas WHERE feita = 1. A caixa já enviaacao=limpar: sobram as 2 pendentes.<?php require __DIR__ . '/banco.php'; function e(string $texto): string { return htmlspecialchars($texto); } function voltar(): never { header('Location: index.php', true, 303); exit; } $erro = ''; $novo = ''; $editando = null; if ($_SERVER['REQUEST_METHOD'] === 'POST') { $acao = $_POST['acao'] ?? ''; $id = (int) ($_POST['id'] ?? 0); if ($acao === 'criar') { $novo = trim($_POST['titulo'] ?? ''); if ($novo === '' || mb_strlen($novo) > 100) { $erro = 'Escreva a tarefa (até 100 caracteres).'; } else { $sql = $pdo->prepare('INSERT INTO tarefas (titulo, criada_em) VALUES (?, ?)'); $sql->execute([$novo, date('Y-m-d H:i:s')]); voltar(); } } if ($acao === 'alternar') { $sql = $pdo->prepare('UPDATE tarefas SET feita = 1 - feita WHERE id = ?'); $sql->execute([$id]); voltar(); } if ($acao === 'editar') { $titulo = trim($_POST['titulo'] ?? ''); if ($titulo === '' || mb_strlen($titulo) > 100) { $erro = 'Escreva a tarefa (até 100 caracteres).'; $editando = ['id' => $id, 'titulo' => $titulo]; } else { $sql = $pdo->prepare('UPDATE tarefas SET titulo = ? WHERE id = ?'); $sql->execute([$titulo, $id]); voltar(); } } if ($acao === 'excluir') { $sql = $pdo->prepare('DELETE FROM tarefas WHERE id = ?'); $sql->execute([$id]); voltar(); } } // Tarefa aberta para edição: index.php?editar=3 if ($editando === null && isset($_GET['editar'])) { $sql = $pdo->prepare('SELECT id, titulo FROM tarefas WHERE id = ?'); $sql->execute([(int) $_GET['editar']]); $editando = $sql->fetch() ?: null; } $tarefas = $pdo->query('SELECT id, titulo, feita FROM tarefas ORDER BY feita, id')->fetchAll(); $pendentes = count(array_filter($tarefas, fn($t) => $t['feita'] == 0)); ?> <!DOCTYPE html> <html lang="pt-BR"> <head> <meta charset="utf-8"> <title>Tarefas</title> </head> <body> <h1>Tarefas da Ana</h1> <p><?= $pendentes ?> pendente(s) de <?= count($tarefas) ?> tarefa(s).</p> <?php if ($erro): ?> <p class="erro"><?= e($erro) ?></p> <?php endif; ?> <form method="post" action="index.php"> <input type="hidden" name="acao" value="criar"> <input name="titulo" value="<?= e($novo) ?>" placeholder="Nova tarefa" maxlength="100"> <button>Adicionar</button> </form> <ul> <?php foreach ($tarefas as $t): ?> <li> <?php if ($editando && $editando['id'] == $t['id']): ?> <form method="post" action="index.php"> <input type="hidden" name="acao" value="editar"> <input type="hidden" name="id" value="<?= $t['id'] ?>"> <input name="titulo" value="<?= e($editando['titulo']) ?>" maxlength="100"> <button>Salvar</button> <a href="index.php">Cancelar</a> </form> <?php else: ?> <form method="post" action="index.php" style="display:inline"> <input type="hidden" name="acao" value="alternar"> <input type="hidden" name="id" value="<?= $t['id'] ?>"> <button><?= $t['feita'] ? 'Desfazer' : 'Feito!' ?></button> </form> <span class="tarefa"><?= $t['feita'] ? '<s>' . e($t['titulo']) . '</s>' : e($t['titulo']) ?></span> <a href="index.php?editar=<?= $t['id'] ?>">editar</a> <form method="post" action="index.php" style="display:inline"> <input type="hidden" name="acao" value="excluir"> <input type="hidden" name="id" value="<?= $t['id'] ?>"> <button>excluir</button> </form> <?php endif; ?> </li> <?php endforeach; ?> </ul> </body> </html><?php // Conexão com o banco SQLite e criação da tabela date_default_timezone_set('America/Sao_Paulo'); $arquivo = __DIR__ . '/tarefas.sqlite'; $primeiraVez = !file_exists($arquivo); $pdo = new PDO('sqlite:' . $arquivo); $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); $pdo->exec('CREATE TABLE IF NOT EXISTS tarefas ( id INTEGER PRIMARY KEY AUTOINCREMENT, titulo TEXT NOT NULL, feita INTEGER NOT NULL DEFAULT 0, criada_em TEXT NOT NULL )'); // Só para testar: quando o banco acaba de ser criado, três tarefas de exemplo. if ($primeiraVez) { $exemplo = $pdo->prepare('INSERT INTO tarefas (titulo, feita, criada_em) VALUES (?, ?, ?)'); $exemplo->execute(['Comprar pão', 0, date('Y-m-d H:i:s')]); $exemplo->execute(['Pagar a conta de luz', 0, date('Y-m-d H:i:s')]); $exemplo->execute(['Estudar PHP', 1, date('Y-m-d H:i:s')]); }<?php require __DIR__ . '/banco.php'; function e(string $texto): string { return htmlspecialchars($texto); } function voltar(): never { header('Location: index.php', true, 303); exit; } $erro = ''; $novo = ''; $editando = null; if ($_SERVER['REQUEST_METHOD'] === 'POST') { $acao = $_POST['acao'] ?? ''; $id = (int) ($_POST['id'] ?? 0); if ($acao === 'criar') { $novo = trim($_POST['titulo'] ?? ''); if ($novo === '' || mb_strlen($novo) > 100) { $erro = 'Escreva a tarefa (até 100 caracteres).'; } else { $sql = $pdo->prepare('INSERT INTO tarefas (titulo, criada_em) VALUES (?, ?)'); $sql->execute([$novo, date('Y-m-d H:i:s')]); voltar(); } } if ($acao === 'alternar') { $sql = $pdo->prepare('UPDATE tarefas SET feita = 1 - feita WHERE id = ?'); $sql->execute([$id]); voltar(); } if ($acao === 'editar') { $titulo = trim($_POST['titulo'] ?? ''); if ($titulo === '' || mb_strlen($titulo) > 100) { $erro = 'Escreva a tarefa (até 100 caracteres).'; $editando = ['id' => $id, 'titulo' => $titulo]; } else { $sql = $pdo->prepare('UPDATE tarefas SET titulo = ? WHERE id = ?'); $sql->execute([$titulo, $id]); voltar(); } } if ($acao === 'limpar') { $pdo->exec('DELETE FROM tarefas WHERE feita = 1'); voltar(); } if ($acao === 'excluir') { $sql = $pdo->prepare('DELETE FROM tarefas WHERE id = ?'); $sql->execute([$id]); voltar(); } } // Tarefa aberta para edição: index.php?editar=3 if ($editando === null && isset($_GET['editar'])) { $sql = $pdo->prepare('SELECT id, titulo FROM tarefas WHERE id = ?'); $sql->execute([(int) $_GET['editar']]); $editando = $sql->fetch() ?: null; } $tarefas = $pdo->query('SELECT id, titulo, feita FROM tarefas ORDER BY feita, id')->fetchAll(); $pendentes = count(array_filter($tarefas, fn($t) => $t['feita'] == 0)); ?> <!DOCTYPE html> <html lang="pt-BR"> <head> <meta charset="utf-8"> <title>Tarefas</title> </head> <body> <h1>Tarefas da Ana</h1> <p><?= $pendentes ?> pendente(s) de <?= count($tarefas) ?> tarefa(s).</p> <?php if ($erro): ?> <p class="erro"><?= e($erro) ?></p> <?php endif; ?> <form method="post" action="index.php"> <input type="hidden" name="acao" value="criar"> <input name="titulo" value="<?= e($novo) ?>" placeholder="Nova tarefa" maxlength="100"> <button>Adicionar</button> </form> <ul> <?php foreach ($tarefas as $t): ?> <li> <?php if ($editando && $editando['id'] == $t['id']): ?> <form method="post" action="index.php"> <input type="hidden" name="acao" value="editar"> <input type="hidden" name="id" value="<?= $t['id'] ?>"> <input name="titulo" value="<?= e($editando['titulo']) ?>" maxlength="100"> <button>Salvar</button> <a href="index.php">Cancelar</a> </form> <?php else: ?> <form method="post" action="index.php" style="display:inline"> <input type="hidden" name="acao" value="alternar"> <input type="hidden" name="id" value="<?= $t['id'] ?>"> <button><?= $t['feita'] ? 'Desfazer' : 'Feito!' ?></button> </form> <span class="tarefa"><?= $t['feita'] ? '<s>' . e($t['titulo']) . '</s>' : e($t['titulo']) ?></span> <a href="index.php?editar=<?= $t['id'] ?>">editar</a> <form method="post" action="index.php" style="display:inline"> <input type="hidden" name="acao" value="excluir"> <input type="hidden" name="id" value="<?= $t['id'] ?>"> <button>excluir</button> </form> <?php endif; ?> </li> <?php endforeach; ?> </ul> <form method="post" action="index.php"> <input type="hidden" name="acao" value="limpar"> <button>Limpar concluídas</button> </form> </body> </html> -
Etapa 7 · A prova: injeção de SQL
Todos os SQL com valores de fora usaram
prepare+execute. Para ver o que isso evita, troque (só para testar!) oexcluirpela versão "ingênua", que cola o valor no texto do SQL:if ($acao === 'excluir') { $pdo->exec('DELETE FROM tarefas WHERE id = ' . $_POST['id']); // ERRADO! voltar(); }Com um
idnormal, funciona igual. Agora, com o banco recém-criado (3 tarefas), dois envios "estranhos":$ curl -s localhost:8000/ | grep pendente <p>2 pendente(s) de 3 tarefa(s).</p> $ curl -s -d 'acao=excluir&id=abc' localhost:8000/ | head -8 <br /> <b>Fatal error</b>: Uncaught PDOException: SQLSTATE[HY000]: General error: 1 no such column: abc in /home/ana/tarefas/index.php:53 Stack trace: #0 /home/ana/tarefas/index.php(53): PDO->exec('DELETE FROM tar...') #1 {main} thrown in <b>/home/ana/tarefas/index.php</b> on line <b>53</b><br /> $ curl -sL -d 'acao=excluir&id=0 OR 1=1' localhost:8000/ | grep -E 'pendente|class="tarefa"' <p>0 pendente(s) de 0 tarefa(s).</p> « TODAS as tarefas sumiram $ sqlite3 tarefas.sqlite 'SELECT COUNT(*) FROM tarefas' 0O que aconteceu: o SQL montado foi
DELETE FROM tarefas WHERE id = 0 OR 1=1. "id é 0 ou 1 é igual a 1" é verdade para toda linha, e o banco apagou tudo. No primeiro envio,abcvirou o nome de uma coluna que não existe (no such column: abc): o texto do usuário foi lido como parte do comando. Isso é a injeção de SQL, um dos ataques mais antigos e mais comuns da web.A mesma tentativa contra a versão correta, também com o banco recém-criado:
$ curl -sL -d 'acao=excluir&id=0 OR 1=1' localhost:8000/ | grep -E 'pendente|class="tarefa"' <p>2 pendente(s) de 3 tarefa(s).</p> <span class="tarefa">Comprar pão</span> <span class="tarefa">Pagar a conta de luz</span> <span class="tarefa"><s>Estudar PHP</s></span> $ sqlite3 tarefas.sqlite 'SELECT COUNT(*) FROM tarefas' 3Nada apagado: com o prepared statement,
0 OR 1=1é só um valor procurado na colunaid(e o(int)ainda o reduz a0), nunca um pedaço de SQL. A regra, sem exceção: valor que vem de fora entra no SQL só por?. Mais em Prepared Statements e SQL Injection.
Quando dá errado
O SQL fala de uma coluna que a tabela não tem. Pode ser um erro de digitação (titlo), uma coluna que você acrescentou ao CREATE TABLE depois de o banco já existir (o IF NOT EXISTS não altera tabelas existentes: apague o tarefas.sqlite para recomeçar, ou use ALTER TABLE), ou, como na etapa 7, um valor colado no SQL.
O PHP abriu um arquivo de banco diferente do que você imagina, um arquivo novo e vazio. Acontece com caminhos relativos (new PDO('sqlite:tarefas.sqlite')), que dependem da pasta de onde o PHP foi chamado. Use sempre __DIR__ . '/tarefas.sqlite'.
O PHP consegue ler o arquivo, mas não escrever. Num servidor, o usuário do PHP (www-data) precisa de permissão de escrita no arquivo e na pasta (o SQLite cria arquivos temporários ao lado).
Faltou o voltar() (Post/Redirect/Get) depois de gravar, ou o exit dentro dele. Confira no terminal do servidor: cada clique deve aparecer como [303]: POST / seguido de [200]: GET /index.php.
O código completo
<?php
// Conexão com o banco SQLite e criação da tabela
date_default_timezone_set('America/Sao_Paulo');
$arquivo = __DIR__ . '/tarefas.sqlite';
$primeiraVez = !file_exists($arquivo);
$pdo = new PDO('sqlite:' . $arquivo);
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
$pdo->exec('CREATE TABLE IF NOT EXISTS tarefas (
id INTEGER PRIMARY KEY AUTOINCREMENT,
titulo TEXT NOT NULL,
feita INTEGER NOT NULL DEFAULT 0,
criada_em TEXT NOT NULL
)');
// Só para testar: quando o banco acaba de ser criado, três tarefas de exemplo.
if ($primeiraVez) {
$exemplo = $pdo->prepare('INSERT INTO tarefas (titulo, feita, criada_em) VALUES (?, ?, ?)');
$exemplo->execute(['Comprar pão', 0, date('Y-m-d H:i:s')]);
$exemplo->execute(['Pagar a conta de luz', 0, date('Y-m-d H:i:s')]);
$exemplo->execute(['Estudar PHP', 1, date('Y-m-d H:i:s')]);
}
<?php
require __DIR__ . '/banco.php';
function e(string $texto): string
{
return htmlspecialchars($texto);
}
function voltar(): never
{
header('Location: index.php', true, 303);
exit;
}
$erro = '';
$novo = '';
$editando = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$acao = $_POST['acao'] ?? '';
$id = (int) ($_POST['id'] ?? 0);
if ($acao === 'criar') {
$novo = trim($_POST['titulo'] ?? '');
if ($novo === '' || mb_strlen($novo) > 100) {
$erro = 'Escreva a tarefa (até 100 caracteres).';
} else {
$sql = $pdo->prepare('INSERT INTO tarefas (titulo, criada_em) VALUES (?, ?)');
$sql->execute([$novo, date('Y-m-d H:i:s')]);
voltar();
}
}
if ($acao === 'alternar') {
$sql = $pdo->prepare('UPDATE tarefas SET feita = 1 - feita WHERE id = ?');
$sql->execute([$id]);
voltar();
}
if ($acao === 'editar') {
$titulo = trim($_POST['titulo'] ?? '');
if ($titulo === '' || mb_strlen($titulo) > 100) {
$erro = 'Escreva a tarefa (até 100 caracteres).';
$editando = ['id' => $id, 'titulo' => $titulo];
} else {
$sql = $pdo->prepare('UPDATE tarefas SET titulo = ? WHERE id = ?');
$sql->execute([$titulo, $id]);
voltar();
}
}
if ($acao === 'excluir') {
$sql = $pdo->prepare('DELETE FROM tarefas WHERE id = ?');
$sql->execute([$id]);
voltar();
}
}
// Tarefa aberta para edição: index.php?editar=3
if ($editando === null && isset($_GET['editar'])) {
$sql = $pdo->prepare('SELECT id, titulo FROM tarefas WHERE id = ?');
$sql->execute([(int) $_GET['editar']]);
$editando = $sql->fetch() ?: null;
}
$tarefas = $pdo->query('SELECT id, titulo, feita FROM tarefas ORDER BY feita, id')->fetchAll();
$pendentes = count(array_filter($tarefas, fn($t) => $t['feita'] == 0));
?>
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="utf-8">
<title>Tarefas</title>
</head>
<body>
<h1>Tarefas da Ana</h1>
<p><?= $pendentes ?> pendente(s) de <?= count($tarefas) ?> tarefa(s).</p>
<?php if ($erro): ?>
<p class="erro"><?= e($erro) ?></p>
<?php endif; ?>
<form method="post" action="index.php">
<input type="hidden" name="acao" value="criar">
<input name="titulo" value="<?= e($novo) ?>" placeholder="Nova tarefa" maxlength="100">
<button>Adicionar</button>
</form>
<ul>
<?php foreach ($tarefas as $t): ?>
<li>
<?php if ($editando && $editando['id'] == $t['id']): ?>
<form method="post" action="index.php">
<input type="hidden" name="acao" value="editar">
<input type="hidden" name="id" value="<?= $t['id'] ?>">
<input name="titulo" value="<?= e($editando['titulo']) ?>" maxlength="100">
<button>Salvar</button> <a href="index.php">Cancelar</a>
</form>
<?php else: ?>
<form method="post" action="index.php" style="display:inline">
<input type="hidden" name="acao" value="alternar">
<input type="hidden" name="id" value="<?= $t['id'] ?>">
<button><?= $t['feita'] ? 'Desfazer' : 'Feito!' ?></button>
</form>
<span class="tarefa"><?= $t['feita'] ? '<s>' . e($t['titulo']) . '</s>' : e($t['titulo']) ?></span>
<a href="index.php?editar=<?= $t['id'] ?>">editar</a>
<form method="post" action="index.php" style="display:inline">
<input type="hidden" name="acao" value="excluir">
<input type="hidden" name="id" value="<?= $t['id'] ?>">
<button>excluir</button>
</form>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
</body>
</html>
Desafios extras
- Data de entrega: uma coluna
prazo TEXT(formato2026-10-20), um<input type="date">no formulário e as tarefas atrasadas em vermelho. Lembre que mudar oCREATE TABLEnão altera um banco que já existe. - Token CSRF: traga as funções
token_csrf()econferir_csrf()do projeto de login e proteja todos os formulários. - Uma lista por pessoa: junte com o projeto de login: coluna
usuario_idna tabela eWHERE usuario_id = ?em todos os SQL (inclusive noUPDATEe noDELETE, senão um usuário apaga a tarefa do outro mandando outroid). - Confirmar antes de excluir:
<form … onsubmit="return confirm('Excluir esta tarefa?')">(um toque de JavaScript). - API: a mesma tabela servida como JSON; é o assunto de Criar uma API JSON e do projeto API REST.
Missão final: uma busca. Acrescente um formulário GET com o campo busca e, quando ele vier preenchido, troque a consulta da lista por SELECT … WHERE titulo LIKE ? ORDER BY feita, id, executada com ['%' . $busca . '%'] (o % do LIKE quer dizer "qualquer coisa": %conta% acha "conta" em qualquer lugar do título). A caixa abre com ?busca=conta.
<?php
require __DIR__ . '/banco.php';
function e(string $texto): string
{
return htmlspecialchars($texto);
}
function voltar(): never
{
header('Location: index.php', true, 303);
exit;
}
$erro = '';
$novo = '';
$editando = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$acao = $_POST['acao'] ?? '';
$id = (int) ($_POST['id'] ?? 0);
if ($acao === 'criar') {
$novo = trim($_POST['titulo'] ?? '');
if ($novo === '' || mb_strlen($novo) > 100) {
$erro = 'Escreva a tarefa (até 100 caracteres).';
} else {
$sql = $pdo->prepare('INSERT INTO tarefas (titulo, criada_em) VALUES (?, ?)');
$sql->execute([$novo, date('Y-m-d H:i:s')]);
voltar();
}
}
if ($acao === 'alternar') {
$sql = $pdo->prepare('UPDATE tarefas SET feita = 1 - feita WHERE id = ?');
$sql->execute([$id]);
voltar();
}
if ($acao === 'editar') {
$titulo = trim($_POST['titulo'] ?? '');
if ($titulo === '' || mb_strlen($titulo) > 100) {
$erro = 'Escreva a tarefa (até 100 caracteres).';
$editando = ['id' => $id, 'titulo' => $titulo];
} else {
$sql = $pdo->prepare('UPDATE tarefas SET titulo = ? WHERE id = ?');
$sql->execute([$titulo, $id]);
voltar();
}
}
if ($acao === 'excluir') {
$sql = $pdo->prepare('DELETE FROM tarefas WHERE id = ?');
$sql->execute([$id]);
voltar();
}
}
// Tarefa aberta para edição: index.php?editar=3
if ($editando === null && isset($_GET['editar'])) {
$sql = $pdo->prepare('SELECT id, titulo FROM tarefas WHERE id = ?');
$sql->execute([(int) $_GET['editar']]);
$editando = $sql->fetch() ?: null;
}
$tarefas = $pdo->query('SELECT id, titulo, feita FROM tarefas ORDER BY feita, id')->fetchAll();
$pendentes = count(array_filter($tarefas, fn($t) => $t['feita'] == 0));
?>
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="utf-8">
<title>Tarefas</title>
</head>
<body>
<h1>Tarefas da Ana</h1>
<p><?= $pendentes ?> pendente(s) de <?= count($tarefas) ?> tarefa(s).</p>
<?php if ($erro): ?>
<p class="erro"><?= e($erro) ?></p>
<?php endif; ?>
<form method="post" action="index.php">
<input type="hidden" name="acao" value="criar">
<input name="titulo" value="<?= e($novo) ?>" placeholder="Nova tarefa" maxlength="100">
<button>Adicionar</button>
</form>
<ul>
<?php foreach ($tarefas as $t): ?>
<li>
<?php if ($editando && $editando['id'] == $t['id']): ?>
<form method="post" action="index.php">
<input type="hidden" name="acao" value="editar">
<input type="hidden" name="id" value="<?= $t['id'] ?>">
<input name="titulo" value="<?= e($editando['titulo']) ?>" maxlength="100">
<button>Salvar</button> <a href="index.php">Cancelar</a>
</form>
<?php else: ?>
<form method="post" action="index.php" style="display:inline">
<input type="hidden" name="acao" value="alternar">
<input type="hidden" name="id" value="<?= $t['id'] ?>">
<button><?= $t['feita'] ? 'Desfazer' : 'Feito!' ?></button>
</form>
<span class="tarefa"><?= $t['feita'] ? '<s>' . e($t['titulo']) . '</s>' : e($t['titulo']) ?></span>
<a href="index.php?editar=<?= $t['id'] ?>">editar</a>
<form method="post" action="index.php" style="display:inline">
<input type="hidden" name="acao" value="excluir">
<input type="hidden" name="id" value="<?= $t['id'] ?>">
<button>excluir</button>
</form>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
</body>
</html>
<?php
// Conexão com o banco SQLite e criação da tabela
date_default_timezone_set('America/Sao_Paulo');
$arquivo = __DIR__ . '/tarefas.sqlite';
$primeiraVez = !file_exists($arquivo);
$pdo = new PDO('sqlite:' . $arquivo);
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
$pdo->exec('CREATE TABLE IF NOT EXISTS tarefas (
id INTEGER PRIMARY KEY AUTOINCREMENT,
titulo TEXT NOT NULL,
feita INTEGER NOT NULL DEFAULT 0,
criada_em TEXT NOT NULL
)');
// Só para testar: quando o banco acaba de ser criado, três tarefas de exemplo.
if ($primeiraVez) {
$exemplo = $pdo->prepare('INSERT INTO tarefas (titulo, feita, criada_em) VALUES (?, ?, ?)');
$exemplo->execute(['Comprar pão', 0, date('Y-m-d H:i:s')]);
$exemplo->execute(['Pagar a conta de luz', 0, date('Y-m-d H:i:s')]);
$exemplo->execute(['Estudar PHP', 1, date('Y-m-d H:i:s')]);
}
<?php
require __DIR__ . '/banco.php';
function e(string $texto): string
{
return htmlspecialchars($texto);
}
function voltar(): never
{
header('Location: index.php', true, 303);
exit;
}
$erro = '';
$novo = '';
$editando = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$acao = $_POST['acao'] ?? '';
$id = (int) ($_POST['id'] ?? 0);
if ($acao === 'criar') {
$novo = trim($_POST['titulo'] ?? '');
if ($novo === '' || mb_strlen($novo) > 100) {
$erro = 'Escreva a tarefa (até 100 caracteres).';
} else {
$sql = $pdo->prepare('INSERT INTO tarefas (titulo, criada_em) VALUES (?, ?)');
$sql->execute([$novo, date('Y-m-d H:i:s')]);
voltar();
}
}
if ($acao === 'alternar') {
$sql = $pdo->prepare('UPDATE tarefas SET feita = 1 - feita WHERE id = ?');
$sql->execute([$id]);
voltar();
}
if ($acao === 'editar') {
$titulo = trim($_POST['titulo'] ?? '');
if ($titulo === '' || mb_strlen($titulo) > 100) {
$erro = 'Escreva a tarefa (até 100 caracteres).';
$editando = ['id' => $id, 'titulo' => $titulo];
} else {
$sql = $pdo->prepare('UPDATE tarefas SET titulo = ? WHERE id = ?');
$sql->execute([$titulo, $id]);
voltar();
}
}
if ($acao === 'excluir') {
$sql = $pdo->prepare('DELETE FROM tarefas WHERE id = ?');
$sql->execute([$id]);
voltar();
}
}
// Tarefa aberta para edição: index.php?editar=3
if ($editando === null && isset($_GET['editar'])) {
$sql = $pdo->prepare('SELECT id, titulo FROM tarefas WHERE id = ?');
$sql->execute([(int) $_GET['editar']]);
$editando = $sql->fetch() ?: null;
}
$busca = trim($_GET['busca'] ?? '');
if ($busca !== '') {
$sql = $pdo->prepare('SELECT id, titulo, feita FROM tarefas WHERE titulo LIKE ? ORDER BY feita, id');
$sql->execute(['%' . $busca . '%']);
$tarefas = $sql->fetchAll();
} else {
$tarefas = $pdo->query('SELECT id, titulo, feita FROM tarefas ORDER BY feita, id')->fetchAll();
}
$pendentes = count(array_filter($tarefas, fn($t) => $t['feita'] == 0));
?>
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="utf-8">
<title>Tarefas</title>
</head>
<body>
<h1>Tarefas da Ana</h1>
<p><?= $pendentes ?> pendente(s) de <?= count($tarefas) ?> tarefa(s).</p>
<?php if ($erro): ?>
<p class="erro"><?= e($erro) ?></p>
<?php endif; ?>
<form method="post" action="index.php">
<input type="hidden" name="acao" value="criar">
<input name="titulo" value="<?= e($novo) ?>" placeholder="Nova tarefa" maxlength="100">
<button>Adicionar</button>
</form>
<form method="get" action="index.php">
<input name="busca" value="<?= e($busca) ?>" placeholder="Buscar">
<button>Buscar</button>
</form>
<ul>
<?php foreach ($tarefas as $t): ?>
<li>
<?php if ($editando && $editando['id'] == $t['id']): ?>
<form method="post" action="index.php">
<input type="hidden" name="acao" value="editar">
<input type="hidden" name="id" value="<?= $t['id'] ?>">
<input name="titulo" value="<?= e($editando['titulo']) ?>" maxlength="100">
<button>Salvar</button> <a href="index.php">Cancelar</a>
</form>
<?php else: ?>
<form method="post" action="index.php" style="display:inline">
<input type="hidden" name="acao" value="alternar">
<input type="hidden" name="id" value="<?= $t['id'] ?>">
<button><?= $t['feita'] ? 'Desfazer' : 'Feito!' ?></button>
</form>
<span class="tarefa"><?= $t['feita'] ? '<s>' . e($t['titulo']) . '</s>' : e($t['titulo']) ?></span>
<a href="index.php?editar=<?= $t['id'] ?>">editar</a>
<form method="post" action="index.php" style="display:inline">
<input type="hidden" name="acao" value="excluir">
<input type="hidden" name="id" value="<?= $t['id'] ?>">
<button>excluir</button>
</form>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
</body>
</html>
Exercícios
Complete o UPDATE seguro que troca o título da tarefa:
$sql->([$titulo, $id]);
Complete o SQL que marca/desmarca uma tarefa com o mesmo botão:
O que significa a letra U de CRUD, e qual comando SQL faz isso?
Com $pdo->exec('DELETE FROM tarefas WHERE id = ' . $_POST['id']), alguém envia id=0 OR 1=1. O que acontece?
Por que o botão "excluir" é um formulário POST e não um link <a href="?excluir=3">?
Resumo
new PDO('sqlite:' . __DIR__ . '/arquivo.sqlite')+CREATE TABLE IF NOT EXISTSdeixam o banco pronto, sem instalar servidor.- CRUD:
INSERT(criar),SELECT(ler),UPDATE(atualizar),DELETE(apagar). - Valor que vem de fora: sempre
preparecom?+execute([...]). SQL fixo pode usarquery/exec. UPDATEeDELETEsempre comWHERE(a não ser que você queira mesmo mexer em todas as linhas).- Ações que mudam dados: formulário POST + Post/Redirect/Get. Ler (como abrir a edição) pode ser GET.
- O banco guarda o texto como veio; a página escapa na hora de mostrar.
Próximo projeto: Mini Blog, com posts no SQLite, páginas separadas e paginação.