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 htmlspecialchars para escapar saída.

Referências

Exercícios

  1. 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.
  2. ✓ Resposta: Adicione o método 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.
  3. Implemente uma validação para o campo descrição: deve ter no máximo 500 caracteres. Adicione a mensagem de erro adequada.
  4. ✓ Resposta: No método validarDados, adicione:
    
    if (strlen($descricao) > 500) {
        $erros['descricao'] = 'A descrição deve ter no máximo 500 caracteres.';
    }
        
  5. Adicione uma nova coluna 'quantidade' na tabela produtos e inclua nos métodos de criar e atualizar, além de exibir na listagem.
  6. ✓ Resposta: Primeiro, altere o banco: 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.
  7. Crie uma página de confirmação antes de excluir um produto, usando JavaScript para exibir um alerta de confirmação.
  8. ✓ Resposta: Na view index, no link de exclusão, adicione:
    
    <a href="index.php?action=excluir&id=<?= $produto['id'] ?>" onclick="return confirm('Tem certeza que deseja excluir?')">Excluir</a>
        
  9. Implemente a funcionalidade de busca por nome na listagem, usando uma query com LIKE.
  10. ✓ Resposta: No Model, adicione:
    
    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.