CRUD completo em PHP moderno com PDO (Create, Read, Update, Delete)

Se você mexe com PHP e banco de dados, mais cedo ou mais tarde vai precisar de um CRUD:

  • Create → Cadastrar
  • Read → Listar / Ver
  • Update → Editar
  • Delete → Excluir

Neste guia, vamos fazer um CRUD completo de usuários usando:

  • PHP 8+
  • PDO
  • Prepared statements (seguro contra SQL Injection)
  • Código simples, em um arquivo só, fácil de entender.

Objetivo: no final você terá um arquivo usuarios.php que lista, cadastra, edita e exclui usuários.


1. Estrutura da tabela no banco de dados

Vamos usar um exemplo simples de tabela usuarios.

SQL para criar a tabela (MySQL):

CREATE TABLE usuarios (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    nome VARCHAR(150) NOT NULL,
    email VARCHAR(150) NOT NULL UNIQUE,
    criado_em DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

Colunas:

  • id: identificador único do usuário
  • nome: nome completo
  • email: e-mail (único)
  • criado_em: data/hora do cadastro

2. Arquivo de conexão (reaproveitando padrão moderno)

Aqui vou usar a mesma ideia do post anterior: PDO + Conexao centralizada.

Se você já tiver a classe Conexao, ótimo, é só incluir.
Se não tiver, pode usar esse exemplo rápido em Conexao.php:

<?php
// Conexao.php
declare(strict_types=1);

use PDO;
use PDOException;

const DB_HOST = 'localhost';
const DB_NOME = 'meu_banco';
const DB_USUARIO = 'root';
const DB_SENHA = '';
const DB_CHARSET = 'utf8mb4';

function obterConexao(): PDO
{
    static $pdo;

    if ($pdo instanceof PDO) {
        return $pdo;
    }

    $dsn = 'mysql:host=' . DB_HOST . ';dbname=' . DB_NOME . ';charset=' . DB_CHARSET;

    $opcoes = [
        PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        PDO::ATTR_EMULATE_PREPARES   => false,
    ];

    try {
        $pdo = new PDO($dsn, DB_USUARIO, DB_SENHA, $opcoes);
        return $pdo;
    } catch (PDOException $e) {
        die('Erro ao conectar ao banco de dados.');
    }
}

Se você já estiver usando .env e uma classe mais elaborada, pode adaptar.
Aqui a ideia é deixar simples para quem está começando.


3. Estrutura do arquivo usuarios.php

Vamos ter um único arquivo controlado por um parâmetro acao:

  • acao=listar → ver todos os usuários (padrão)
  • acao=criar → exibir formulário e salvar novo usuário
  • acao=editar&id=... → carregar dados e salvar edição
  • acao=excluir&id=... → excluir usuário e redirecionar

Arquivo: usuarios.php

<?php
declare(strict_types=1);

require __DIR__ . '/Conexao.php';

$acao = $_GET['acao'] ?? 'listar';

switch ($acao) {
    case 'criar':
        processarCriacao();
        break;

    case 'editar':
        processarEdicao();
        break;

    case 'excluir':
        processarExclusao();
        break;

    default:
        listarUsuarios();
        break;
}

/**
 * Lista todos os usuários.
 */
function listarUsuarios(): void
{
    $pdo = obterConexao();

    $sql = "SELECT id, nome, email, criado_em FROM usuarios ORDER BY id DESC";
    $stmt = $pdo->prepare($sql);
    $stmt->execute();
    $usuarios = $stmt->fetchAll();

    ?>
    <!doctype html>
    <html lang="pt-BR">
    <head>
        <meta charset="UTF-8">
        <title>CRUD de Usuários</title>
        <style>
            body { font-family: Arial, sans-serif; margin: 20px; }
            table { border-collapse: collapse; width: 100%; margin-top: 20px; }
            th, td { border: 1px solid #ccc; padding: 8px; text-align: left; }
            th { background: #f0f0f0; }
            a.botao { display: inline-block; padding: 6px 10px; background: #2563eb; color: #fff; text-decoration: none; border-radius: 4px; font-size: 14px; }
            a.botao-vermelho { background: #b91c1c; }
            .topo { display: flex; justify-content: space-between; align-items: center; }
        </style>
    </head>
    <body>

    <div class="topo">
        <h1>Usuários</h1>
        <a href="?acao=criar" class="botao">+ Novo usuário</a>
    </div>

    <?php if (empty($usuarios)): ?>
        <p>Nenhum usuário cadastrado.</p>
    <?php else: ?>
        <table>
            <thead>
            <tr>
                <th>ID</th>
                <th>Nome</th>
                <th>E-mail</th>
                <th>Cadastrado em</th>
                <th>Ações</th>
            </tr>
            </thead>
            <tbody>
            <?php foreach ($usuarios as $usuario): ?>
                <tr>
                    <td><?= (int)$usuario['id'] ?></td>
                    <td><?= htmlspecialchars($usuario['nome']) ?></td>
                    <td><?= htmlspecialchars($usuario['email']) ?></td>
                    <td><?= htmlspecialchars($usuario['criado_em']) ?></td>
                    <td>
                        <a href="?acao=editar&id=<?= (int)$usuario['id'] ?>" class="botao">Editar</a>
                        <a href="?acao=excluir&id=<?= (int)$usuario['id'] ?>"
                           class="botao botao-vermelho"
                           onclick="return confirm('Tem certeza que deseja excluir este usuário?');">
                           Excluir
                        </a>
                    </td>
                </tr>
            <?php endforeach; ?>
            </tbody>
        </table>
    <?php endif; ?>

    </body>
    </html>
    <?php
}

/**
 * Processa criação de usuário (exibe formulário e salva).
 */
function processarCriacao(): void
{
    if ($_SERVER['REQUEST_METHOD'] === 'POST') {
        $nome  = trim($_POST['nome'] ?? '');
        $email = trim($_POST['email'] ?? '');

        // Validação simples
        $erros = [];

        if ($nome === '') {
            $erros[] = 'O nome é obrigatório.';
        }

        if ($email === '') {
            $erros[] = 'O e-mail é obrigatório.';
        } elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            $erros[] = 'E-mail inválido.';
        }

        if (empty($erros)) {
            try {
                $pdo = obterConexao();

                $sql = "INSERT INTO usuarios (nome, email) VALUES (:nome, :email)";
                $stmt = $pdo->prepare($sql);
                $stmt->bindValue(':nome', $nome);
                $stmt->bindValue(':email', $email);

                $stmt->execute();

                // Redireciona de volta para a listagem
                header('Location: usuarios.php');
                exit;
            } catch (PDOException $e) {
                $erros[] = 'Erro ao salvar no banco de dados (e-mail duplicado ou outro problema).';
            }
        }
    }

    // Exibe o formulário (se GET ou se houve erro)
    exibirFormularioUsuario('criar', $erros ?? [], [
        'nome'  => $nome  ?? '',
        'email' => $email ?? '',
    ]);
}

/**
 * Processa edição de usuário (carrega, exibe formulário e salva).
 */
function processarEdicao(): void
{
    $id = isset($_GET['id']) ? (int)$_GET['id'] : 0;

    if ($id <= 0) {
        header('Location: usuarios.php');
        exit;
    }

    $pdo = obterConexao();

    // Busca dados atuais do usuário
    $sql = "SELECT id, nome, email FROM usuarios WHERE id = :id LIMIT 1";
    $stmt = $pdo->prepare($sql);
    $stmt->bindValue(':id', $id, PDO::PARAM_INT);
    $stmt->execute();
    $usuario = $stmt->fetch();

    if (!$usuario) {
        header('Location: usuarios.php');
        exit;
    }

    if ($_SERVER['REQUEST_METHOD'] === 'POST') {
        $nome  = trim($_POST['nome'] ?? '');
        $email = trim($_POST['email'] ?? '');

        $erros = [];

        if ($nome === '') {
            $erros[] = 'O nome é obrigatório.';
        }

        if ($email === '') {
            $erros[] = 'O e-mail é obrigatório.';
        } elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            $erros[] = 'E-mail inválido.';
        }

        if (empty($erros)) {
            try {
                $sql = "UPDATE usuarios
                        SET nome = :nome, email = :email
                        WHERE id = :id";
                $stmt = $pdo->prepare($sql);
                $stmt->bindValue(':nome', $nome);
                $stmt->bindValue(':email', $email);
                $stmt->bindValue(':id', $id, PDO::PARAM_INT);

                $stmt->execute();

                header('Location: usuarios.php');
                exit;
            } catch (PDOException $e) {
                $erros[] = 'Erro ao atualizar o usuário. Talvez o e-mail já esteja em uso.';
            }
        }

        $usuario['nome']  = $nome;
        $usuario['email'] = $email;
    }

    exibirFormularioUsuario('editar', $erros ?? [], $usuario);
}

/**
 * Exclui um usuário e redireciona.
 */
function processarExclusao(): void
{
    if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
        header('Location: usuarios.php');
        exit;
    }

    $id = isset($_GET['id']) ? (int)$_GET['id'] : 0;

    if ($id <= 0) {
        header('Location: usuarios.php');
        exit;
    }

    $pdo = obterConexao();

    $sql = "DELETE FROM usuarios WHERE id = :id";
    $stmt = $pdo->prepare($sql);
    $stmt->bindValue(':id', $id, PDO::PARAM_INT);
    $stmt->execute();

    header('Location: usuarios.php');
    exit;
}

/**
 * Exibe o formulário de criação/edição.
 *
 * @param string $modo 'criar' ou 'editar'
 * @param array  $erros
 * @param array  $dados
 */
function exibirFormularioUsuario(string $modo, array $erros, array $dados): void
{
    $tituloPagina = $modo === 'criar' ? 'Novo usuário' : 'Editar usuário';
    $acaoForm     = $modo === 'criar' ? 'criar'         : 'editar&id=' . (int)$dados['id'];

    ?>
    <!doctype html>
    <html lang="pt-BR">
    <head>
        <meta charset="UTF-8">
        <title><?= htmlspecialchars($tituloPagina) ?></title>
        <style>
            body { font-family: Arial, sans-serif; margin: 20px; }
            form { max-width: 400px; }
            label { display: block; margin-top: 10px; }
            input[type="text"], input[type="email"] {
                width: 100%; padding: 8px; margin-top: 4px; box-sizing: border-box;
            }
            button { margin-top: 15px; padding: 8px 12px; }
            .erros { background: #fee2e2; color: #b91c1c; padding: 10px; border-radius: 4px; margin-bottom: 10px; }
            a { color: #2563eb; text-decoration: none; }
        </style>
    </head>
    <body>

    <h1><?= htmlspecialchars($tituloPagina) ?></h1>

    <p><a href="usuarios.php">&larr; Voltar para a lista</a></p>

    <?php if (!empty($erros)): ?>
        <div class="erros">
            <ul>
                <?php foreach ($erros as $erro): ?>
                    <li><?= htmlspecialchars($erro) ?></li>
                <?php endforeach; ?>
            </ul>
        </div>
    <?php endif; ?>

    <form method="post" action="usuarios.php?acao=<?= htmlspecialchars($acaoForm) ?>">
        <label for="nome">Nome</label>
        <input type="text" name="nome" id="nome"
               value="<?= htmlspecialchars($dados['nome'] ?? '') ?>" required>

        <label for="email">E-mail</label>
        <input type="email" name="email" id="email"
               value="<?= htmlspecialchars($dados['email'] ?? '') ?>" required>

        <button type="submit">Salvar</button>
    </form>

    </body>
    </html>
    <?php
}

4. O que esse CRUD já faz corretamente

  • Usa PDO com prepared statements (seguro contra SQL Injection);
  • Valida nome e e-mail antes de salvar;
  • Trata e-mails duplicados com mensagem amigável;
  • Usa htmlspecialchars() para exibir texto (proteção contra XSS);
  • Usa switch simples para separar ações:
    • listar
    • criar
    • editar
    • excluir

5. Próximos passos para deixar mais profissional

Se você quiser evoluir esse código:

  • Separar em Controller/Model/View (como no post anterior de estrutura de pastas);
  • Trocar a URL de usuarios.php?acao=editar&id=1 para algo mais amigável (via HTACCESS / router);
  • Criar paginação na listagem de usuários;
  • Adicionar busca por nome/e-mail;
  • Adicionar autenticação (permitir CRUD só para usuários logados).

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Rolar para cima