PHP CRUD com PDO
Quase todo sistema que guarda dados faz só quatro coisas com eles: cria, lê, atualiza e apaga. Em inglês, Create, Read, Update, Delete: as iniciais formam CRUD, a palavra que todo programador usa para isso. Nesta aula, a Ana vai montar o cadastro de produtos da lojinha com PDO e SQLite: um programa para cada uma das quatro operações e, no fim, uma página web de cadastro.
- O que é CRUD e qual comando SQL faz cada letra.
- Como cadastrar com
INSERTe descobrir o id novo comlastInsertId(). - Como listar com
fetchAll()e buscar um registro só comfetch()(e o que fazer quando ele não existe). - Como atualizar com
UPDATEe apagar comDELETE, conferindo comrowCount()quantas linhas mudaram. - Por que o
WHEREé a parte mais importante doUPDATEe doDELETE. - Como juntar tudo numa página web com formulário.
Antes, veja Banco de Dados: o Básico (o que são tabelas e SQL) e Conectar com PDO (new PDO, query, exec, fetch). Os programas foram executados com o PHP 8.5 na máquina fictícia da Ana, na pasta ~/loja, e cada sessão de terminal continua a anterior: o banco vai mudando ao longo da aula.
A ideia: o fichário da loja
Antes do computador, a Ana tinha um fichário com uma ficha por produto. O que ela fazia com ele?
| Letra | Inglês | No fichário | No SQL | No PDO |
|---|---|---|---|---|
| C | Create (criar) | Escrever uma ficha nova e numerá-la | INSERT | prepare + execute, lastInsertId() |
| R | Read (ler) | Folhear todas, ou puxar a ficha nº 3 | SELECT | query/execute + fetchAll()/fetch() |
| U | Update (atualizar) | Riscar o preço e escrever o novo | UPDATE | prepare + execute, rowCount() |
| D | Delete (apagar) | Tirar a ficha do fichário | DELETE | prepare + execute, rowCount() |
O número da ficha é o id, a chave primária: é por ele que a Ana diz "a ficha 3", sem confusão, mesmo que existam dois produtos com nomes parecidos.
Preparando: a conexão e a tabela
Os quatro programas precisam do banco. Em vez de repetir a conexão em cada um, a Ana a guardou num arquivo e o inclui com require (veja include e require):
<?php
// Abre (ou cria) o arquivo loja.db, na mesma pasta deste arquivo.
$pdo = new PDO("sqlite:" . __DIR__ . "/loja.db", options: [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
- __DIR__
- Constante mágica com a pasta deste arquivo (aqui,
/home/ana/loja). Assim o banco é sempre oloja.dbao lado dos programas, de qualquer pasta que você os rode. O cenário do fim da aula mostra o que acontece sem isso. - options: [...]
- As opções da conexão, passadas pelo nome do parâmetro (argumento nomeado, PHP 8.0+), porque o SQLite não usa usuário e senha. É o mesmo que
new PDO($dsn, null, null, [...]). - ERRMODE_EXCEPTION
- Erro de SQL vira exceção (o padrão desde o PHP 8.0).
- FETCH_ASSOC
- Cada linha volta como array associativo:
$p["nome"].
A tabela é criada uma vez, por um programa só para isso:
<?php
require "conexao.php";
$pdo->exec("CREATE TABLE IF NOT EXISTS produtos (
id INTEGER PRIMARY KEY,
nome TEXT NOT NULL,
preco REAL NOT NULL,
estoque INTEGER NOT NULL DEFAULT 0
)");
echo "Tabela produtos pronta.\n";
| Trecho | O que faz |
|---|---|
IF NOT EXISTS | "Se ainda não existir": rodar o programa de novo não dá erro nem apaga nada. |
id INTEGER PRIMARY KEY | O número da ficha. O SQLite preenche sozinho (1, 2, 3…) quando você não informa. |
NOT NULL | A coluna é obrigatória. |
DEFAULT 0 | Se o estoque não for informado, vale 0. |
$ php criar-tabela.php Tabela produtos pronta. $ ls -l loja.db -rw-r--r-- 1 ana ana 8192 out 16 20:58 loja.db « o arquivo do banco foi criado $ php criar-tabela.php Tabela produtos pronta. « de novo: sem erro, graças ao IF NOT EXISTS
C: cadastrar (INSERT)
Para cadastrar, a Ana passa o nome, o preço e o estoque na linha de comando ($argv, veja PHP na Linha de Comando). Num site, os valores viriam de um formulário; você vai ver isso no fim da aula.
<?php
require "conexao.php";
$nome = $argv[1];
$preco = (float) $argv[2];
$estoque = (int) $argv[3];
$stmt = $pdo->prepare("INSERT INTO produtos (nome, preco, estoque) VALUES (?, ?, ?)");
$stmt->execute([$nome, $preco, $estoque]);
echo "Produto cadastrado com o id ", $pdo->lastInsertId(), ".\n";
- (float), (int)
- Tudo o que chega de fora é texto. A Ana converte para número antes de gravar (veja Conversão de Tipos).
- INSERT INTO produtos (nome, preco, estoque)
- "Insira na tabela
produtos, nestas colunas…". Oidfica de fora: o banco escolhe. - VALUES (?, ?, ?)
- Três lacunas, uma para cada coluna, na mesma ordem.
- $pdo->prepare(...)
- Prepara o comando com as lacunas.
- $stmt->execute([$nome, $preco, $estoque])
- Executa, pondo os três valores nas lacunas, na ordem.
- $pdo->lastInsertId()
- "Último id inserido": o número que o banco acabou de dar ao produto. Vem como texto (
"1").
? e não '$nome' direto no SQL?
Os valores vêm de fora (aqui, do terminal; num site, de qualquer visitante). Colados no SQL, um simples apóstrofo (copo d'água) quebra o comando, e alguém mal-intencionado pode reescrevê-lo. Com prepare e ?, o valor nunca vira SQL. Por enquanto, siga a regra: todo valor variável entra por um ?. A aula Prepared Statements e SQL Injection mostra o ataque e explica tudo em detalhe.
$ php cadastrar.php Caderno 15.90 30 Produto cadastrado com o id 1. $ php cadastrar.php "Caneta azul" 2.50 100 « aspas: o nome tem espaço Produto cadastrado com o id 2. $ php cadastrar.php Mochila 89.90 5 Produto cadastrado com o id 3.
Cada execução é um programa novo, mas os produtos ficam: estão gravados no arquivo loja.db. E cada um ganhou o próximo número livre.
No editor do guia, o banco é criado na memória a cada vez que você clica em Rodar (veja a aba banco.php): ele já vem com cinco produtos, ids 1 a 5.
Missão: o comando já está preparado. Execute-o com os valores "Borracha", 0.90 e 50 e, logo depois, mostre Cadastrado com o id seguido do id novo (use lastInsertId()). A lista do fim deve mostrar a Borracha com o id 6.
<?php
require "banco.php";
$stmt = $pdo->prepare("INSERT INTO produtos (nome, preco, estoque) VALUES (?, ?, ?)");
// 1. execute o $stmt com "Borracha", 0.90 e 50
// 2. mostre "Cadastrado com o id " e o id novo
foreach ($pdo->query("SELECT id, nome FROM produtos") as $p) {
echo "#", $p["id"], " ", $p["nome"], "\n";
}
<?php
// Cria a lojinha da Ana na memória, com 5 produtos.
$pdo = new PDO("sqlite::memory:", options: [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
$pdo->exec("CREATE TABLE produtos (
id INTEGER PRIMARY KEY,
nome TEXT NOT NULL,
preco REAL NOT NULL,
estoque INTEGER NOT NULL DEFAULT 0
)");
$pdo->exec("INSERT INTO produtos (nome, preco, estoque) VALUES
('Caderno', 15.90, 30),
('Caneta azul', 2.50, 100),
('Mochila', 89.90, 5),
('Lápis', 1.20, 200),
('Estojo', 9.90, 12)");
<?php
require "banco.php";
$stmt = $pdo->prepare("INSERT INTO produtos (nome, preco, estoque) VALUES (?, ?, ?)");
$stmt->execute(["Borracha", 0.90, 50]);
echo "Cadastrado com o id ", $pdo->lastInsertId(), "\n";
foreach ($pdo->query("SELECT id, nome FROM produtos") as $p) {
echo "#", $p["id"], " ", $p["nome"], "\n";
}
R: listar e buscar (SELECT)
Listar todos: fetchAll()
Para a lista, não há nenhum valor de fora no SQL, então um query simples basta:
<?php
require "conexao.php";
$produtos = $pdo->query("SELECT id, nome, preco, estoque FROM produtos ORDER BY nome")->fetchAll();
echo "Produtos cadastrados: ", count($produtos), "\n";
foreach ($produtos as $p) {
echo "#", $p["id"], " ", $p["nome"], ": R$ ", number_format($p["preco"], 2, ",", "."),
" (", $p["estoque"], " em estoque)\n";
}
- SELECT id, nome, preco, estoque
- As colunas que você quer (melhor que
*: fica claro o que o programa usa). - ORDER BY nome
- Em ordem alfabética do nome.
- ->fetchAll()
- Busca todas as linhas de uma vez: um array de arrays (veja Arrays Multidimensionais). Se não houver nenhuma, um array vazio.
- count($produtos)
- Quantos produtos vieram.
- number_format(…, 2, ",", ".")
- Preço no formato brasileiro (veja Formatar Números).
$ php listar.php Produtos cadastrados: 3 #1 Caderno: R$ 15,90 (30 em estoque) #2 Caneta azul: R$ 2,50 (100 em estoque) #3 Mochila: R$ 89,90 (5 em estoque)
Buscar um só: fetch()
Para ver um produto pelo id, o id vem de fora, então volta o prepare. E como o resultado é uma linha (ou nenhuma), usa-se fetch():
<?php
require "conexao.php";
$id = (int) $argv[1];
$stmt = $pdo->prepare("SELECT * FROM produtos WHERE id = ?");
$stmt->execute([$id]);
$produto = $stmt->fetch();
if ($produto === false) {
echo "Não existe produto com o id $id.\n";
} else {
echo $produto["nome"], " custa R$ ", number_format($produto["preco"], 2, ",", "."), "\n";
}
$ php ver.php 3 Mochila custa R$ 89,90 $ php ver.php 42 Não existe produto com o id 42.
- WHERE id = ?
- Só a linha com aquele id. Como o id é a chave primária, vem no máximo uma.
- $stmt->fetch()
- Pega a próxima linha do resultado, como array. Se não houver mais linhas (ou nenhuma), devolve
false. - $produto === false
- Sempre confira antes de usar: "não achei" é um caso normal, não um erro do banco (veja o cenário do
Trying to access array offset on false).
| Quero… | Use | Devolve |
|---|---|---|
| Todas as linhas | fetchAll() | Array de linhas (vazio se nada) |
| Uma linha (por id, por e-mail…) | fetch() | A linha, ou false |
| Um valor só (uma contagem, um preço) | fetchColumn() | O valor da 1ª coluna, ou false |
| Percorrer muitas linhas sem guardar todas | foreach ($stmt as $linha) | Uma linha por volta |
Experimente: troque o $id por outros números (de 1 a 5, e depois um que não existe). Depois, mude o SQL para buscar pelo nome: WHERE nome = ? e execute(["Mochila"]).
<?php
require "banco.php";
$id = 2;
$stmt = $pdo->prepare("SELECT * FROM produtos WHERE id = ?");
$stmt->execute([$id]);
$produto = $stmt->fetch();
if ($produto === false) {
echo "Não existe produto com o id $id.\n";
} else {
print_r($produto);
}
<?php
// Cria a lojinha da Ana na memória, com 5 produtos.
$pdo = new PDO("sqlite::memory:", options: [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
$pdo->exec("CREATE TABLE produtos (
id INTEGER PRIMARY KEY,
nome TEXT NOT NULL,
preco REAL NOT NULL,
estoque INTEGER NOT NULL DEFAULT 0
)");
$pdo->exec("INSERT INTO produtos (nome, preco, estoque) VALUES
('Caderno', 15.90, 30),
('Caneta azul', 2.50, 100),
('Mochila', 89.90, 5),
('Lápis', 1.20, 200),
('Estojo', 9.90, 12)");
U: atualizar (UPDATE)
O Caderno subiu de preço. O programa recebe o id e o preço novo:
<?php
require "conexao.php";
$id = (int) $argv[1];
$preco = (float) $argv[2];
$stmt = $pdo->prepare("UPDATE produtos SET preco = ? WHERE id = ?");
$stmt->execute([$preco, $id]);
echo $stmt->rowCount(), " produto(s) atualizado(s).\n";
- UPDATE produtos
- "Altere a tabela
produtos…" - SET preco = ?
- "…pondo este valor na coluna
preco…". Para mudar várias colunas:SET preco = ?, estoque = ?. - WHERE id = ?
- "…só na linha com este id." Sem o
WHERE, seriam todas as linhas. - [$preco, $id]
- Os valores na ordem em que os
?aparecem: primeiro o doSET, depois o doWHERE. - $stmt->rowCount()
- "Contagem de linhas": quantas linhas o comando mudou.
$ php atualizar-preco.php 1 17.50 1 produto(s) atualizado(s). $ php atualizar-preco.php 42 1.00 0 produto(s) atualizado(s). « não existe o id 42: não é erro, só não mudou nada $ php listar.php Produtos cadastrados: 3 #1 Caderno: R$ 17,50 (30 em estoque) #2 Caneta azul: R$ 2,50 (100 em estoque) #3 Mochila: R$ 89,90 (5 em estoque)
Repare: um UPDATE que não encontra nenhuma linha não dá erro. Para o banco, "mudar zero linhas" é um resultado válido. Se isso importa para você (e quase sempre importa: o usuário quer saber se deu certo), confira o rowCount().
O SET pode usar o valor que já está na coluna: SET estoque = estoque - 1 (vendeu um) ou SET preco = preco * 1.10 (10% mais caro). O banco faz a conta linha por linha.
Missão: reajuste em 10% o preço dos produtos que custam menos que $limite (use preco = preco * 1.10 e WHERE preco < ?) e mostre quantos foram alterados com rowCount(). A saída deve começar assim:
2 produto(s) reajustado(s) Lápis: R$ 1,32 Caneta azul: R$ 2,75 Estojo: R$ 9,90 …
<?php
require "banco.php";
$limite = 5.00;
// reajuste aqui os produtos que custam menos que $limite
// e mostre "N produto(s) reajustado(s)"
foreach ($pdo->query("SELECT nome, preco FROM produtos ORDER BY preco") as $p) {
echo $p["nome"], ": R$ ", number_format($p["preco"], 2, ",", "."), "\n";
}
<?php
// Cria a lojinha da Ana na memória, com 5 produtos.
$pdo = new PDO("sqlite::memory:", options: [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
$pdo->exec("CREATE TABLE produtos (
id INTEGER PRIMARY KEY,
nome TEXT NOT NULL,
preco REAL NOT NULL,
estoque INTEGER NOT NULL DEFAULT 0
)");
$pdo->exec("INSERT INTO produtos (nome, preco, estoque) VALUES
('Caderno', 15.90, 30),
('Caneta azul', 2.50, 100),
('Mochila', 89.90, 5),
('Lápis', 1.20, 200),
('Estojo', 9.90, 12)");
<?php
require "banco.php";
$limite = 5.00;
$stmt = $pdo->prepare("UPDATE produtos SET preco = preco * 1.10 WHERE preco < ?");
$stmt->execute([$limite]);
echo $stmt->rowCount(), " produto(s) reajustado(s)\n";
foreach ($pdo->query("SELECT nome, preco FROM produtos ORDER BY preco") as $p) {
echo $p["nome"], ": R$ ", number_format($p["preco"], 2, ",", "."), "\n";
}
D: apagar (DELETE)
A Caneta azul saiu de linha:
<?php
require "conexao.php";
$id = (int) $argv[1];
$stmt = $pdo->prepare("DELETE FROM produtos WHERE id = ?");
$stmt->execute([$id]);
if ($stmt->rowCount() === 0) {
echo "Nada apagado: não existe produto com o id $id.\n";
} else {
echo "Produto $id apagado.\n";
}
- DELETE FROM produtos
- "Apague linhas da tabela
produtos…" - WHERE id = ?
- "…só a que tem este id." Mais uma vez: sem
WHERE, apaga todas. - rowCount() === 0
- Nenhuma linha apagada: o id não existia (ou já tinha sido apagado).
$ php apagar.php 2 Produto 2 apagado. $ php apagar.php 2 Nada apagado: não existe produto com o id 2. « já tinha sido apagado $ php listar.php Produtos cadastrados: 2 #1 Caderno: R$ 17,50 (30 em estoque) #3 Mochila: R$ 89,90 (5 em estoque) $ php cadastrar.php Lápis 1.20 200 Produto cadastrado com o id 4. « o 2 não foi reaproveitado $ php listar.php Produtos cadastrados: 3 #1 Caderno: R$ 17,50 (30 em estoque) #4 Lápis: R$ 1,20 (200 em estoque) #3 Mochila: R$ 89,90 (5 em estoque)
Um buraco na numeração (não existe mais o 2) é normal e desejável: se alguém guardou "o produto 2" num pedido antigo, ele não vai, de repente, apontar para o Lápis. Não tente "arrumar" os ids.
Muitos sistemas não apagam: marcam o registro como inativo (uma coluna ativo com 0 ou 1, ou apagado_em com a data) e filtram com WHERE ativo = 1. Isso se chama soft delete ("apagar suave") e permite desfazer e manter o histórico de vendas. Para a lojinha, o DELETE basta; em sistemas com pedidos e notas fiscais, pense nisso antes.
Juntando tudo: uma página web de cadastro
Agora o CRUD vai para o navegador: um formulário para cadastrar e a lista dos produtos logo abaixo, na mesma página. Tudo o que aparece aqui foi visto em aulas anteriores: PHP dentro do HTML, $_POST, validação e htmlspecialchars.
<?php
$pdo = new PDO("sqlite:" . __DIR__ . "/loja.db", options: [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
$pdo->exec("CREATE TABLE IF NOT EXISTS produtos (
id INTEGER PRIMARY KEY,
nome TEXT NOT NULL,
preco REAL NOT NULL,
estoque INTEGER NOT NULL DEFAULT 0
)");
$mensagem = "";
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$nome = trim($_POST["nome"] ?? "");
$preco = (float) str_replace(",", ".", $_POST["preco"] ?? "");
$estoque = (int) ($_POST["estoque"] ?? 0);
if ($nome === "" || $preco <= 0) {
$mensagem = "Preencha o nome e um preço maior que zero.";
} else {
$stmt = $pdo->prepare("INSERT INTO produtos (nome, preco, estoque) VALUES (?, ?, ?)");
$stmt->execute([$nome, $preco, $estoque]);
$mensagem = "Produto nº " . $pdo->lastInsertId() . " cadastrado: " . $nome;
}
}
$produtos = $pdo->query("SELECT * FROM produtos ORDER BY nome")->fetchAll();
?>
<!doctype html>
<html lang="pt-BR">
<head><meta charset="utf-8"><title>Lojinha da Ana</title></head>
<body>
<h1>Cadastro de produtos</h1>
<?php if ($mensagem !== ""): ?>
<p><strong><?= htmlspecialchars($mensagem) ?></strong></p>
<?php endif; ?>
<form method="post">
<label>Nome: <input name="nome"></label>
<label>Preço: <input name="preco"></label>
<label>Estoque: <input name="estoque" type="number" value="0"></label>
<button>Cadastrar</button>
</form>
<h2>Produtos (<?= count($produtos) ?>)</h2>
<ul>
<?php foreach ($produtos as $p): ?>
<li><?= htmlspecialchars($p["nome"]) ?>: R$ <?= number_format($p["preco"], 2, ",", ".") ?> (<?= $p["estoque"] ?> un.)</li>
<?php endforeach; ?>
</ul>
</body>
</html>
| Parte | O que faz |
|---|---|
Conexão + CREATE TABLE IF NOT EXISTS | Na primeira visita, cria o banco e a tabela; nas outras, não faz nada. |
if ($_SERVER["REQUEST_METHOD"] === "POST") | Só cadastra quando o formulário foi enviado; uma visita normal (GET) só mostra a página. |
trim, str_replace(",", ".", …), (float), (int) | Limpa o nome e aceita preço com vírgula (0,90), como os brasileiros digitam. |
if ($nome === "" || $preco <= 0) | Validação: nada de produto sem nome ou de graça. |
prepare + execute + lastInsertId | O C do CRUD, igual ao do terminal. |
query(...)->fetchAll() | O R: a lista, depois do cadastro (então o produto novo já aparece). |
htmlspecialchars(...) | Todo texto que veio do usuário (o nome, a mensagem) é escapado antes de ir para o HTML. |
A Ana rodou a página com o servidor embutido (veja O Servidor Embutido) e, em outro terminal, enviou o formulário com o curl (o -d manda os campos por POST, como o navegador faz ao clicar em Cadastrar):
$ php -S localhost:8000 -t web [Fri Oct 16 21:01:35 2026] PHP 8.5.8 Development Server (http://localhost:8000) started [Fri Oct 16 21:01:36 2026] 127.0.0.1:54646 Accepted [Fri Oct 16 21:01:36 2026] 127.0.0.1:54646 [200]: POST / [Fri Oct 16 21:01:36 2026] 127.0.0.1:54646 Closing [Fri Oct 16 21:01:36 2026] 127.0.0.1:54650 Accepted [Fri Oct 16 21:01:37 2026] 127.0.0.1:54650 [200]: POST / [Fri Oct 16 21:01:37 2026] 127.0.0.1:54650 Closing [Fri Oct 16 21:01:37 2026] 127.0.0.1:54652 Accepted [Fri Oct 16 21:01:37 2026] 127.0.0.1:54652 [200]: POST / [Fri Oct 16 21:01:37 2026] 127.0.0.1:54652 Closing
$ curl -s -d "nome=Borracha&preco=0,90&estoque=50" localhost:8000/ <!doctype html> <html lang="pt-BR"> <head><meta charset="utf-8"><title>Lojinha da Ana</title></head> <body> <h1>Cadastro de produtos</h1> <p><strong>Produto nº 1 cadastrado: Borracha</strong></p> <form method="post"> <label>Nome: <input name="nome"></label> <label>Preço: <input name="preco"></label> <label>Estoque: <input name="estoque" type="number" value="0"></label> <button>Cadastrar</button> </form> <h2>Produtos (1)</h2> <ul> <li>Borracha: R$ 0,90 (50 un.)</li> </ul> </body> </html> $ curl -s -d "nome=<b>Régua</b>&preco=4.50&estoque=20" localhost:8000/ | grep -E "<p>|<li>" <p><strong>Produto nº 2 cadastrado: <b>Régua</b></strong></p> <li><b>Régua</b>: R$ 4,50 (20 un.)</li> <li>Borracha: R$ 0,90 (50 un.)</li> $ curl -s -d "nome=&preco=" localhost:8000/ | grep "<p>" <p><strong>Preencha o nome e um preço maior que zero.</strong></p>
Três envios: um cadastro normal (o 0,90 com vírgula virou 0.90); um nome com tags HTML, que foi gravado como veio, mas mostrado escapado (<b>), então o navegador exibe o texto <b>Régua</b> em vez de pôr a palavra em negrito; e um envio vazio, barrado pela validação. O servidor registra cada pedido no Terminal 1: três conexões (Accepted), três POST / respondidos com [200] ("OK"). Encerre o servidor com Ctrl+C.
Missão: a página já valida o formulário, mas não grava nada. No lugar do comentário, grave o produto com prepare + execute e ponha em $mensagem o texto Produto nº … cadastrado: … (com o lastInsertId() e o nome). O editor já envia a Borracha na primeira execução; depois, use o formulário da aba Resultado para cadastrar outros produtos.
<?php
$pdo = new PDO("sqlite:" . __DIR__ . "/loja.db", options: [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
$pdo->exec("CREATE TABLE IF NOT EXISTS produtos (
id INTEGER PRIMARY KEY,
nome TEXT NOT NULL,
preco REAL NOT NULL,
estoque INTEGER NOT NULL DEFAULT 0
)");
$mensagem = "";
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$nome = trim($_POST["nome"] ?? "");
$preco = (float) str_replace(",", ".", $_POST["preco"] ?? "");
$estoque = (int) ($_POST["estoque"] ?? 0);
if ($nome === "" || $preco <= 0) {
$mensagem = "Preencha o nome e um preço maior que zero.";
} else {
// grave o produto aqui e preencha $mensagem
}
}
$produtos = $pdo->query("SELECT * FROM produtos ORDER BY nome")->fetchAll();
?>
<!doctype html>
<html lang="pt-BR">
<head><meta charset="utf-8"><title>Lojinha da Ana</title></head>
<body>
<h1>Cadastro de produtos</h1>
<?php if ($mensagem !== ""): ?>
<p><strong><?= htmlspecialchars($mensagem) ?></strong></p>
<?php endif; ?>
<form method="post">
<label>Nome: <input name="nome"></label>
<label>Preço: <input name="preco"></label>
<label>Estoque: <input name="estoque" type="number" value="0"></label>
<button>Cadastrar</button>
</form>
<h2>Produtos (<?= count($produtos) ?>)</h2>
<ul>
<?php foreach ($produtos as $p): ?>
<li><?= htmlspecialchars($p["nome"]) ?>: R$ <?= number_format($p["preco"], 2, ",", ".") ?> (<?= $p["estoque"] ?> un.)</li>
<?php endforeach; ?>
</ul>
</body>
</html>
<?php
$pdo = new PDO("sqlite:" . __DIR__ . "/loja.db", options: [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
$pdo->exec("CREATE TABLE IF NOT EXISTS produtos (
id INTEGER PRIMARY KEY,
nome TEXT NOT NULL,
preco REAL NOT NULL,
estoque INTEGER NOT NULL DEFAULT 0
)");
$mensagem = "";
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$nome = trim($_POST["nome"] ?? "");
$preco = (float) str_replace(",", ".", $_POST["preco"] ?? "");
$estoque = (int) ($_POST["estoque"] ?? 0);
if ($nome === "" || $preco <= 0) {
$mensagem = "Preencha o nome e um preço maior que zero.";
} else {
$stmt = $pdo->prepare("INSERT INTO produtos (nome, preco, estoque) VALUES (?, ?, ?)");
$stmt->execute([$nome, $preco, $estoque]);
$mensagem = "Produto nº " . $pdo->lastInsertId() . " cadastrado: " . $nome;
}
}
$produtos = $pdo->query("SELECT * FROM produtos ORDER BY nome")->fetchAll();
?>
<!doctype html>
<html lang="pt-BR">
<head><meta charset="utf-8"><title>Lojinha da Ana</title></head>
<body>
<h1>Cadastro de produtos</h1>
<?php if ($mensagem !== ""): ?>
<p><strong><?= htmlspecialchars($mensagem) ?></strong></p>
<?php endif; ?>
<form method="post">
<label>Nome: <input name="nome"></label>
<label>Preço: <input name="preco"></label>
<label>Estoque: <input name="estoque" type="number" value="0"></label>
<button>Cadastrar</button>
</form>
<h2>Produtos (<?= count($produtos) ?>)</h2>
<ul>
<?php foreach ($produtos as $p): ?>
<li><?= htmlspecialchars($p["nome"]) ?>: R$ <?= number_format($p["preco"], 2, ",", ".") ?> (<?= $p["estoque"] ?> un.)</li>
<?php endforeach; ?>
</ul>
</body>
</html>
Seguem a mesma receita: um link ou botão por produto que envia o id (<form method="post"> com um <input type="hidden" name="apagar" value="3">), e o PHP faz o UPDATE ou o DELETE com WHERE id = ?. Apagar e alterar sempre por POST, nunca por um link GET (um robô de busca que siga os links apagaria tudo). O projeto Tarefas com SQLite monta o CRUD completo na web, e a aula CSRF mostra como proteger esses formulários.
Quando dá errado
WHERE no UPDATE (ou no DELETE)
<?php
require "conexao.php";
// A Ana queria zerar o estoque só da Mochila (id 3)... e esqueceu o WHERE.
$afetados = $pdo->exec("UPDATE produtos SET estoque = 0");
echo $afetados, " produto(s) com estoque zerado.\n";
$ php zerar-estoque.php 3 produto(s) com estoque zerado. $ php listar.php Produtos cadastrados: 3 #1 Caderno: R$ 17,50 (0 em estoque) #4 Lápis: R$ 1,20 (0 em estoque) #3 Mochila: R$ 89,90 (0 em estoque)
O que significa: nenhuma mensagem de erro, porque o SQL estava certo: "zere o estoque de todos". O banco obedeceu. (O exec devolve, como o rowCount, quantas linhas mudaram.) Com DELETE FROM produtos sem WHERE, seria a tabela inteira apagada.
Como evitar: escreva o WHERE primeiro. Antes de um UPDATE ou DELETE importante, rode um SELECT com o mesmo WHERE para ver quais linhas seriam atingidas. Confira o rowCount(): se você esperava 1 e veio 3, algo está errado. Use transações para poder desfazer. E tenha cópia de segurança do loja.db (é só copiar o arquivo).
Trying to access array offset on false
<?php
require "conexao.php";
$stmt = $pdo->prepare("SELECT * FROM produtos WHERE id = ?");
$stmt->execute([42]);
$produto = $stmt->fetch();
echo $produto["nome"], " custa R$ ", $produto["preco"], "\n";
$ php ver-sem-teste.php Warning: Trying to access array offset on false in /home/ana/loja/ver-sem-teste.php on line 8 custa R$ Warning: Trying to access array offset on false in /home/ana/loja/ver-sem-teste.php on line 8
O que significa: "tentando acessar uma posição de array em false". Não existe produto 42, então o fetch() devolveu false, e o programa tentou ler $produto["nome"] como se fosse um array. O PHP avisa duas vezes (uma para ["nome"], outra para ["preco"]) e continua, mostrando um texto pela metade.
Como consertar: confira o resultado antes de usar: if ($produto === false) { … }, como em ver.php.
NOT NULL constraint failed: produtos.preco
<?php
require "conexao.php";
$stmt = $pdo->prepare("INSERT INTO produtos (nome, estoque) VALUES (?, ?)");
$stmt->execute(["Borracha", 50]);
$ php cadastrar-sem-preco.php
Fatal error: Uncaught PDOException: SQLSTATE[23000]: Integrity constraint violation: 19 NOT NULL constraint failed: produtos.preco in /home/ana/loja/cadastrar-sem-preco.php:5
Stack trace:
#0 /home/ana/loja/cadastrar-sem-preco.php(5): PDOStatement->execute(Array)
#1 {main}
thrown in /home/ana/loja/cadastrar-sem-preco.php on line 5
O que significa: "violação de restrição de integridade: a restrição NOT NULL falhou em produtos.preco". A coluna preco é obrigatória e o INSERT não a preencheu. O código 23000 é o de "regra da tabela violada".
Como consertar: inclua a coluna no INSERT. Num formulário, valide no PHP antes (como a página web faz com $preco <= 0) para mostrar uma mensagem amigável em vez de um erro.
no such table: produto
<?php
require "conexao.php";
$produtos = $pdo->query("SELECT * FROM produto")->fetchAll();
$ php errou-tabela.php
Fatal error: Uncaught PDOException: SQLSTATE[HY000]: General error: 1 no such table: produto in /home/ana/loja/errou-tabela.php:4
Stack trace:
#0 /home/ana/loja/errou-tabela.php(4): PDO->query('SELECT * FROM p...')
#1 {main}
thrown in /home/ana/loja/errou-tabela.php on line 4
O que significa: "não existe a tabela produto": faltou o s. Com um nome de coluna errado, a mensagem é parecida: no such column: ….
Como consertar: confira a grafia. Se o nome está certo, a tabela realmente não existe neste banco: rodou o criar-tabela.php? Está abrindo o arquivo certo? (Veja o próximo cenário.)
no such table: produtos só quando roda de outra pasta
<?php
$pdo = new PDO("sqlite:loja.db"); // caminho relativo: depende da pasta em que você está
$produtos = $pdo->query("SELECT nome FROM produtos")->fetchAll();
echo count($produtos), " produtos\n";
$ cd ~
$ php loja/listar-relativo.php
Fatal error: Uncaught PDOException: SQLSTATE[HY000]: General error: 1 no such table: produtos in /home/ana/loja/listar-relativo.php:4
Stack trace:
#0 /home/ana/loja/listar-relativo.php(4): PDO->query('SELECT nome FRO...')
#1 {main}
thrown in /home/ana/loja/listar-relativo.php on line 4
$ ls -l loja.db
-rw-r--r-- 1 ana ana 0 out 16 20:58 loja.db « um banco novo e vazio, criado na pasta errada
$ rm loja.db
O que significa: sqlite:loja.db é um caminho relativo: é procurado na pasta em que você está, não na pasta do programa. Rodando de ~, o PDO não achou ~/loja.db e, sem avisar, criou um arquivo vazio ali (0 bytes). Num banco vazio, não existe tabela nenhuma. Num site, a "pasta em que você está" depende do servidor, e o erro aparece só lá.
Como consertar: use "sqlite:" . __DIR__ . "/loja.db", como em conexao.php, e apague o arquivo vazio criado por engano.
Referência
| Operação | SQL | PHP | Para conferir |
|---|---|---|---|
| Criar | INSERT INTO produtos (nome, preco) VALUES (?, ?) | $stmt->execute([$nome, $preco]) | $pdo->lastInsertId() (o id novo, como texto) |
| R: todos | SELECT … FROM produtos ORDER BY nome | $pdo->query($sql)->fetchAll() | count($lista) |
| R: um | SELECT … WHERE id = ? | $stmt->execute([$id]); $stmt->fetch() | === false se não achou |
| R: um valor | SELECT COUNT(*) FROM produtos | ->fetchColumn() | |
| Update | UPDATE produtos SET preco = ? WHERE id = ? | $stmt->execute([$preco, $id]) | $stmt->rowCount() |
| Delete | DELETE FROM produtos WHERE id = ? | $stmt->execute([$id]) | $stmt->rowCount() |
| SQL fixo, sem resultado | CREATE TABLE …, UPDATE … sem variáveis | $pdo->exec($sql) | devolve as linhas afetadas |
Exercícios
1. O id novo
Complete para mostrar o id do produto que acabou de ser cadastrado:
2. Atualizar o estoque
Complete o SQL para mudar o estoque do produto de um certo id:
3. Apagar
Complete o SQL para apagar o produto de um certo id:
4. O que $stmt->fetch() devolve quando o SELECT … WHERE id = ? não encontra nada?
5. Um UPDATE … WHERE id = ? com um id que não existe…
6. O que acontece com $pdo->exec("DELETE FROM produtos")?
7. Por que conexao.php usa __DIR__ no caminho do banco?
Resumo
- CRUD = Create, Read, Update, Delete =
INSERT,SELECT,UPDATE,DELETE. - Guarde a conexão num arquivo (
conexao.php) e use"sqlite:" . __DIR__ . "/loja.db". - Todo valor que vem de fora entra por
?:prepare+execute([...]). - Depois do
INSERT,$pdo->lastInsertId()dá o id novo. fetchAll()para listas;fetch()para um registro, conferindo se veiofalse.UPDATEeDELETEsempre comWHERE; confira comrowCount(). Zero linhas não é erro.- Na web: cadastre só no POST, valide antes de gravar e use
htmlspecialcharsao mostrar.
Próxima aula: Prepared Statements e SQL Injection, para entender por que o ? é obrigatório e como agrupar comandos com transações.