Projeto: CRUD com PDO
LidoNesta aula, você vai construir um CRUD completo com PDO em PHP, adotando uma estrutura MVC simples. Aprenderá a conectar ao banco de dados, realizar operações de criação, leitura, atualização e exclusão, e implementar validações de dados.
Nesta aula, vamos colocar em prática tudo o que aprendemos sobre PDO construindo um sistema CRUD (Create, Read, Update, Delete) completo. Utilizaremos uma arquitetura MVC (Model-View-Controller) simplificada, que separa a lógica de negócio, a apresentação e o controle de fluxo. Isso torna o código mais organizado, reutilizável e fácil de manter.
O projeto será um gerenciador de produtos, onde você poderá listar, criar, editar e excluir registros. Ao final, você terá um entendimento sólido de como integrar PDO com uma estrutura profissional de desenvolvimento.
Estrutura MVC simples
O padrão MVC divide a aplicação em três componentes principais:
- Model: responsável pela interação com o banco de dados e pela lógica de negócio.
- View: responsável pela apresentação, ou seja, pelo HTML que o usuário vê.
- Controller: recebe as requisições do usuário, processa e decide qual view carregar, usando os models quando necessário.
Na nossa implementação, teremos uma pasta app com subpastas Models, Views e Controllers. Um arquivo index.php na raiz atuará como front controller, redirecionando as requisições para o controlador adequado.
projeto/
├── app/
│ ├── Controllers/
│ │ └── ProdutoController.php
│ ├── Models/
│ │ └── Produto.php
│ └── Views/
│ ├── index.php
│ ├── criar.php
│ ├── editar.php
│ └── show.php
├── config/
│ └── database.php
├── public/
│ └── index.php
└── .htaccess
O arquivo public/index.php é o ponto de entrada. Ele carrega as configurações e despacha a requisição para o controlador com base no parâmetro action.
// public/index.php
require_once '../config/database.php';
require_once '../app/Controllers/ProdutoController.php';
$action = $_GET['action'] ?? 'index';
$controller = new ProdutoController($pdo);
switch ($action) {
case 'criar':
$controller->criar();
break;
case 'editar':
$controller->editar($_GET['id'] ?? null);
break;
case 'excluir':
$controller->excluir($_GET['id'] ?? null);
break;
case 'show':
$controller->show($_GET['id'] ?? null);
break;
default:
$controller->index();
}
Conexão
O arquivo config/database.php retorna uma instância de PDO. Vamos usar variáveis de ambiente para configurar o banco, mas por simplicidade, definimos constantes.
// config/database.php
$host = 'localhost';
$dbname = 'crud_pdo';
$user = 'root';
$pass = '';
$pdo = new PDO("mysql:host=$host;dbname=$dbname;charset=utf8mb4", $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]);
return $pdo;
Configuramos o modo de erro para exceções, o fetch mode para array associativo e desabilitamos a emulação de prepared statements para usar a funcionalidade nativa do MySQL. Isso garante mais segurança e desempenho.
Operações
O Model Produto conterá métodos para as operações CRUD. Utilizaremos prepared statements para evitar SQL injection.
// app/Models/Produto.php
class Produto {
private $pdo;
public function __construct($pdo) {
$this->pdo = $pdo;
}
public function listar() {
$stmt = $this->pdo->query("SELECT * FROM produtos ORDER BY id DESC");
return $stmt->fetchAll();
}
public function buscar($id) {
$stmt = $this->pdo->prepare("SELECT * FROM produtos WHERE id = ?");
$stmt->execute([$id]);
return $stmt->fetch();
}
public function criar($nome, $preco, $descricao) {
$stmt = $this->pdo->prepare("INSERT INTO produtos (nome, preco, descricao) VALUES (?, ?, ?)");
return $stmt->execute([$nome, $preco, $descricao]);
}
public function atualizar($id, $nome, $preco, $descricao) {
$stmt = $this->pdo->prepare("UPDATE produtos SET nome = ?, preco = ?, descricao = ? WHERE id = ?");
return $stmt->execute([$nome, $preco, $descricao, $id]);
}
public function excluir($id) {
$stmt = $this->pdo->prepare("DELETE FROM produtos WHERE id = ?");
return $stmt->execute([$id]);
}
}
O Controller orquestra as operações e inclui a lógica de validação. Ele também é responsável por redirecionar após as ações.
// app/Controllers/ProdutoController.php
class ProdutoController {
private $model;
public function __construct($pdo) {
$this->model = new Produto($pdo);
}
public function index() {
$produtos = $this->model->listar();
require '../app/Views/index.php';
}
public function criar() {
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$dados = $this->validarDados($_POST);
if (empty($dados['erros'])) {
$this->model->criar($dados['nome'], $dados['preco'], $dados['descricao']);
header('Location: index.php');
exit;
}
$erros = $dados['erros'];
}
require '../app/Views/criar.php';
}
public function editar($id) {
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$dados = $this->validarDados($_POST);
if (empty($dados['erros'])) {
$this->model->atualizar($id, $dados['nome'], $dados['preco'], $dados['descricao']);
header('Location: index.php');
exit;
}
$erros = $dados['erros'];
}
$produto = $this->model->buscar($id);
require '../app/Views/editar.php';
}
public function excluir($id) {
$this->model->excluir($id);
header('Location: index.php');
exit;
}
public function show($id) {
$produto = $this->model->buscar($id);
require '../app/Views/show.php';
}
private function validarDados($post) {
$erros = [];
$nome = trim($post['nome'] ?? '');
$preco = $post['preco'] ?? '';
$descricao = trim($post['descricao'] ?? '');
if (empty($nome)) {
$erros['nome'] = 'O nome é obrigatório.';
} elseif (strlen($nome) < 3) {
$erros['nome'] = 'O nome deve ter pelo menos 3 caracteres.';
}
if (empty($preco) || !is_numeric($preco) || $preco <= 0) {
$erros['preco'] = 'Informe um preço válido.';
}
return ['nome' => $nome, 'preco' => $preco, 'descricao' => $descricao, 'erros' => $erros];
}
}
Validação
A validação é crucial para garantir a integridade dos dados. No método validarDados, verificamos se o nome não está vazio e tem pelo menos 3 caracteres, e se o preço é numérico e maior que zero. Essa validação é feita no servidor, pois a validação no cliente pode ser contornada.
Nas views, exibimos os erros de forma amigável, destacando os campos com problemas. Por exemplo, na view de criação:
<!-- app/Views/criar.php -->
<form method="post">
<div class="mb-3">
<label for="nome" class="form-label">Nome</label>
<input type="text" class="form-control" id="nome" name="nome" value="<?= htmlspecialchars($dados['nome'] ?? '') ?>">
<?php if (isset($erros['nome'])): ?>
<div class="text-danger"><?= $erros['nome'] ?></div>
<?php endif; ?>
</div>
<!-- similar para preço e descrição -->
<button type="submit" class="btn btn-primary">Salvar</button>
</form>
É importante sempre usar htmlspecialchars ao exibir dados no HTML para prevenir XSS.
Boas práticas e observações finais
Algumas boas práticas aplicadas neste projeto:
- Uso de prepared statements para evitar SQL injection.
- Configuração do PDO para lançar exceções e usar fetch mode associativo.
- Separação de responsabilidades com MVC.
- Validação no servidor com feedback de erros.
- Redirecionamento após ações POST para evitar reenvio do formulário.
- Utilização de
htmlspecialcharspara escapar saída.
Referências
- Documentação oficial do PDO
- Prepared Statements no PHP
- MVC - MDN Web Docs
- htmlspecialchars - PHP
- PDO MySQL - PHP
- PSR-1: Basic Coding Standard
Exercícios
- Crie uma função no Model que retorne os produtos ordenados por nome em ordem crescente. Use-a no método index do Controller e exiba na view.
- Implemente uma validação para o campo descrição: deve ter no máximo 500 caracteres. Adicione a mensagem de erro adequada.
- Adicione uma nova coluna 'quantidade' na tabela produtos e inclua nos métodos de criar e atualizar, além de exibir na listagem.
- Crie uma página de confirmação antes de excluir um produto, usando JavaScript para exibir um alerta de confirmação.
- Implemente a funcionalidade de busca por nome na listagem, usando uma query com LIKE.
listarPorNome() no Model:
public function listarPorNome() {
$stmt = $this->pdo->query("SELECT * FROM produtos ORDER BY nome ASC");
return $stmt->fetchAll();
}
No Controller, altere o método index para usar esse método. A view continua a mesma.
validarDados, adicione:
if (strlen($descricao) > 500) {
$erros['descricao'] = 'A descrição deve ter no máximo 500 caracteres.';
}
ALTER TABLE produtos ADD COLUMN quantidade INT NOT NULL DEFAULT 0. Depois, no Model, ajuste os métodos:
// criar
$stmt = $this->pdo->prepare("INSERT INTO produtos (nome, preco, descricao, quantidade) VALUES (?, ?, ?, ?)");
$stmt->execute([$nome, $preco, $descricao, $quantidade]);
// atualizar
$stmt = $this->pdo->prepare("UPDATE produtos SET nome = ?, preco = ?, descricao = ?, quantidade = ? WHERE id = ?");
$stmt->execute([$nome, $preco, $descricao, $quantidade, $id]);
E no Controller, valide e passe a quantidade.
<a href="index.php?action=excluir&id=<?= $produto['id'] ?>" onclick="return confirm('Tem certeza que deseja excluir?')">Excluir</a>
public function buscarPorNome($termo) {
$stmt = $this->pdo->prepare("SELECT * FROM produtos WHERE nome LIKE ? ORDER BY nome");
$stmt->execute(["%{$termo}%"]);
return $stmt->fetchAll();
}
No Controller, no método index, verifique se há um parâmetro de busca e use a função apropriada.