CRUD completo con PHP, MySQL y PDO: crear, listar, editar, eliminar y buscar


Ya vimos cómo conectar PHP con MySQL mediante PDO, cómo leer datos y cómo ejecutar INSERT, UPDATE y DELETE.

Ahora vamos a integrar todo eso en una pequeña aplicación funcional. Construiremos un CRUD completo de usuarios con listado, búsqueda, alta, edición y eliminación.

CRUD es el acrónimo de:

Create  → crear
Read    → leer
Update  → actualizar
Delete  → eliminar

Para profundizar en SQL: en este proyecto utilizamos SELECT, WHERE, LIKE, INSERT, UPDATE y DELETE. La teoría de esas sentencias está desarrollada en nuestra ruta de aprendizaje SQL / MySQL.

Qué vamos a construir

La aplicación tendrá estas pantallas:

  • index.php: listado y búsqueda de usuarios;
  • crear.php: alta de usuarios;
  • editar.php: modificación;
  • eliminar.php: eliminación mediante POST;
  • conexion.php: conexión centralizada mediante PDO.

Estructura del proyecto

crud-usuarios/
│
├── conexion.php
├── index.php
├── crear.php
├── editar.php
└── eliminar.php

Base de datos

Seguiremos usando la base club_programador y la tabla usuarios:

CREATE TABLE usuarios (
    id INT AUTO_INCREMENT PRIMARY KEY,
    nombre VARCHAR(100) NOT NULL,
    email VARCHAR(150) NOT NULL UNIQUE,
    edad INT
);

1. conexion.php

Centralizamos la conexión para no repetirla en cada archivo.

<?php

$host = 'localhost';
$base = 'club_programador';
$usuario = 'root';
$password = '';

$dsn =
    "mysql:host=$host;dbname=$base;charset=utf8mb4";

try {

    $pdo = new PDO(
        $dsn,
        $usuario,
        $password,
        [
            PDO::ATTR_ERRMODE =>
                PDO::ERRMODE_EXCEPTION,

            PDO::ATTR_DEFAULT_FETCH_MODE =>
                PDO::FETCH_ASSOC
        ]
    );

} catch (PDOException $e) {

    exit(
        'No se pudo conectar a la base de datos'
    );
}

2. index.php: listado y buscador

La página principal mostrará todos los usuarios y permitirá buscar por nombre o email.

<?php

require 'conexion.php';

$busqueda = trim($_GET['q'] ?? '');

if ($busqueda === '') {

    $sql =
        'SELECT id, nombre, email, edad
         FROM usuarios
         ORDER BY nombre';

    $stmt = $pdo->query($sql);

} else {

    $sql =
        'SELECT id, nombre, email, edad
         FROM usuarios
         WHERE nombre LIKE :busqueda
            OR email LIKE :busqueda
         ORDER BY nombre';

    $stmt = $pdo->prepare($sql);

    $stmt->execute([
        'busqueda' => '%' . $busqueda . '%'
    ]);
}

$usuarios = $stmt->fetchAll();

$mensaje = $_GET['mensaje'] ?? '';

?>

<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <title>Usuarios</title>
</head>
<body>

<h1>Usuarios</h1>

<p>
    <a href="crear.php">
        Crear usuario
    </a>
</p>

<?php if ($mensaje === 'creado'): ?>
    <p>Usuario creado correctamente.</p>
<?php elseif ($mensaje === 'actualizado'): ?>
    <p>Usuario actualizado correctamente.</p>
<?php elseif ($mensaje === 'eliminado'): ?>
    <p>Usuario eliminado correctamente.</p>
<?php endif; ?>

<form method="get">

    <input
        type="text"
        name="q"
        placeholder="Buscar..."
        value="<?php echo htmlspecialchars(
            $busqueda,
            ENT_QUOTES,
            'UTF-8'
        ); ?>"
    >

    <button type="submit">
        Buscar
    </button>

</form>

<hr>

<?php if (empty($usuarios)): ?>

    <p>No se encontraron usuarios.</p>

<?php else: ?>

    <table border="1" cellpadding="8">

        <thead>
        <tr>
            <th>ID</th>
            <th>Nombre</th>
            <th>Email</th>
            <th>Edad</th>
            <th>Acciones</th>
        </tr>
        </thead>

        <tbody>

        <?php foreach ($usuarios as $usuario): ?>

            <tr>

                <td>
                    <?php echo $usuario['id']; ?>
                </td>

                <td>
                    <?php echo htmlspecialchars(
                        $usuario['nombre'],
                        ENT_QUOTES,
                        'UTF-8'
                    ); ?>
                </td>

                <td>
                    <?php echo htmlspecialchars(
                        $usuario['email'],
                        ENT_QUOTES,
                        'UTF-8'
                    ); ?>
                </td>

                <td>
                    <?php echo htmlspecialchars(
                        $usuario['edad'],
                        ENT_QUOTES,
                        'UTF-8'
                    ); ?>
                </td>

                <td>

                    <a href="editar.php?id=<?php
                        echo $usuario['id'];
                    ?>">
                        Editar
                    </a>

                    <form
                        method="post"
                        action="eliminar.php"
                        style="display:inline"
                        onsubmit="return confirm(
                            '¿Eliminar este usuario?'
                        );"
                    >

                        <input
                            type="hidden"
                            name="id"
                            value="<?php
                                echo $usuario['id'];
                            ?>"
                        >

                        <button type="submit">
                            Eliminar
                        </button>

                    </form>

                </td>

            </tr>

        <?php endforeach; ?>

        </tbody>

    </table>

<?php endif; ?>

</body>
</html>

Qué hace index.php

La página combina varias cosas que ya aprendimos:

  • recibe un filtro mediante GET;
  • usa una consulta fija cuando no existe búsqueda;
  • usa una consulta preparada cuando existe un valor dinámico;
  • recupera los resultados con fetchAll();
  • recorre los registros con foreach;
  • escapa los datos antes de mostrarlos en HTML;
  • ofrece acciones de editar y eliminar.

Para profundizar en SQL: en este proyecto utilizamos SELECT, WHERE, LIKE, INSERT, UPDATE y DELETE. La teoría de esas sentencias está desarrollada en nuestra ruta de aprendizaje SQL / MySQL.

3. crear.php

Esta pantalla permitirá agregar usuarios nuevos.

<?php

require 'conexion.php';

$nombre = '';
$email = '';
$edad = '';

$errores = [];

if ($_SERVER['REQUEST_METHOD'] === 'POST') {

    $nombre = trim($_POST['nombre'] ?? '');
    $email = trim($_POST['email'] ?? '');
    $edad = trim($_POST['edad'] ?? '');

    if ($nombre === '') {
        $errores[] =
            'El nombre es obligatorio';
    }

    if (!filter_var(
        $email,
        FILTER_VALIDATE_EMAIL
    )) {
        $errores[] =
            'El email no es válido';
    }

    if (
        $edad !== '' &&
        filter_var(
            $edad,
            FILTER_VALIDATE_INT
        ) === false
    ) {
        $errores[] =
            'La edad debe ser un número entero';
    }

    if (empty($errores)) {

        $sql =
            'INSERT INTO usuarios
                (nombre, email, edad)
             VALUES
                (:nombre, :email, :edad)';

        $stmt = $pdo->prepare($sql);

        $stmt->execute([
            'nombre' => $nombre,
            'email' => $email,
            'edad' =>
                $edad === ''
                ? null
                : (int) $edad
        ]);

        header(
            'Location: index.php?mensaje=creado'
        );

        exit;
    }
}

?>

<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <title>Crear usuario</title>
</head>
<body>

<h1>Crear usuario</h1>

<?php if (!empty($errores)): ?>

    <ul>
        <?php foreach ($errores as $error): ?>
            <li>
                <?php echo htmlspecialchars(
                    $error,
                    ENT_QUOTES,
                    'UTF-8'
                ); ?>
            </li>
        <?php endforeach; ?>
    </ul>

<?php endif; ?>

<form method="post">

    <label for="nombre">
        Nombre:
    </label>

    <input
        type="text"
        id="nombre"
        name="nombre"
        value="<?php echo htmlspecialchars(
            $nombre,
            ENT_QUOTES,
            'UTF-8'
        ); ?>"
    >

    <br><br>

    <label for="email">
        Email:
    </label>

    <input
        type="email"
        id="email"
        name="email"
        value="<?php echo htmlspecialchars(
            $email,
            ENT_QUOTES,
            'UTF-8'
        ); ?>"
    >

    <br><br>

    <label for="edad">
        Edad:
    </label>

    <input
        type="number"
        id="edad"
        name="edad"
        value="<?php echo htmlspecialchars(
            $edad,
            ENT_QUOTES,
            'UTF-8'
        ); ?>"
    >

    <br><br>

    <button type="submit">
        Guardar
    </button>

    <a href="index.php">
        Cancelar
    </a>

</form>

</body>
</html>

Post/Redirect/Get en el alta

Después de insertar el usuario no mostramos directamente una página de éxito.

En cambio hacemos:

header(
    'Location: index.php?mensaje=creado'
);

exit;

Esto evita que el navegador repita el INSERT si el usuario actualiza la página.

4. editar.php

La edición tiene dos etapas:

GET  → cargar el usuario
POST → validar y actualizar

Primero obtenemos y validamos el ID.

$id = filter_input(
    INPUT_GET,
    'id',
    FILTER_VALIDATE_INT
);

if ($id === false || $id === null) {
    exit('ID no válido');
}

Cargar el usuario

$sql =
    'SELECT id, nombre, email, edad
     FROM usuarios
     WHERE id = :id';

$stmt = $pdo->prepare($sql);

$stmt->execute([
    'id' => $id
]);

$usuario = $stmt->fetch();

if (!$usuario) {
    exit('Usuario no encontrado');
}

editar.php completo

<?php

require 'conexion.php';

$id = filter_input(
    INPUT_GET,
    'id',
    FILTER_VALIDATE_INT
);

if ($id === false || $id === null) {
    exit('ID no válido');
}

$stmt = $pdo->prepare(
    'SELECT id, nombre, email, edad
     FROM usuarios
     WHERE id = :id'
);

$stmt->execute([
    'id' => $id
]);

$usuario = $stmt->fetch();

if (!$usuario) {
    exit('Usuario no encontrado');
}

$nombre = $usuario['nombre'];
$email = $usuario['email'];
$edad = $usuario['edad'];

$errores = [];

if ($_SERVER['REQUEST_METHOD'] === 'POST') {

    $nombre = trim($_POST['nombre'] ?? '');
    $email = trim($_POST['email'] ?? '');
    $edad = trim($_POST['edad'] ?? '');

    if ($nombre === '') {
        $errores[] =
            'El nombre es obligatorio';
    }

    if (!filter_var(
        $email,
        FILTER_VALIDATE_EMAIL
    )) {
        $errores[] =
            'El email no es válido';
    }

    if (
        $edad !== '' &&
        filter_var(
            $edad,
            FILTER_VALIDATE_INT
        ) === false
    ) {
        $errores[] =
            'La edad no es válida';
    }

    if (empty($errores)) {

        $sql =
            'UPDATE usuarios
             SET nombre = :nombre,
                 email = :email,
                 edad = :edad
             WHERE id = :id';

        $stmt = $pdo->prepare($sql);

        $stmt->execute([
            'nombre' => $nombre,
            'email' => $email,
            'edad' =>
                $edad === ''
                ? null
                : (int) $edad,
            'id' => $id
        ]);

        header(
            'Location: index.php?mensaje=actualizado'
        );

        exit;
    }
}

?>

<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <title>Editar usuario</title>
</head>
<body>

<h1>Editar usuario</h1>

<?php if (!empty($errores)): ?>

    <ul>
        <?php foreach ($errores as $error): ?>
            <li>
                <?php echo htmlspecialchars(
                    $error,
                    ENT_QUOTES,
                    'UTF-8'
                ); ?>
            </li>
        <?php endforeach; ?>
    </ul>

<?php endif; ?>

<form method="post">

    <label for="nombre">
        Nombre:
    </label>

    <input
        type="text"
        id="nombre"
        name="nombre"
        value="<?php echo htmlspecialchars(
            $nombre,
            ENT_QUOTES,
            'UTF-8'
        ); ?>"
    >

    <br><br>

    <label for="email">
        Email:
    </label>

    <input
        type="email"
        id="email"
        name="email"
        value="<?php echo htmlspecialchars(
            $email,
            ENT_QUOTES,
            'UTF-8'
        ); ?>"
    >

    <br><br>

    <label for="edad">
        Edad:
    </label>

    <input
        type="number"
        id="edad"
        name="edad"
        value="<?php echo htmlspecialchars(
            $edad,
            ENT_QUOTES,
            'UTF-8'
        ); ?>"
    >

    <br><br>

    <button type="submit">
        Guardar cambios
    </button>

    <a href="index.php">
        Cancelar
    </a>

</form>

</body>
</html>

5. eliminar.php

La eliminación se ejecutará solamente mediante POST.

<?php

require 'conexion.php';

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    exit('Método no permitido');
}

$id = filter_input(
    INPUT_POST,
    'id',
    FILTER_VALIDATE_INT
);

if ($id === false || $id === null) {
    exit('ID no válido');
}

$stmt = $pdo->prepare(
    'DELETE FROM usuarios
     WHERE id = :id'
);

$stmt->execute([
    'id' => $id
]);

header(
    'Location: index.php?mensaje=eliminado'
);

exit;

Por qué DELETE usa POST

Eliminar información cambia el estado de la aplicación. No debería ocurrir simplemente al visitar una URL.

Por eso en index.php utilizamos:

<form
    method="post"
    action="eliminar.php"
>

    <input
        type="hidden"
        name="id"
        value="<?php echo $usuario['id']; ?>"
    >

    <button type="submit">
        Eliminar
    </button>

</form>

Flujo completo del CRUD

LISTAR
index.php
    ↓
SELECT
    ↓
tabla HTML


CREAR
crear.php
    ↓
formulario POST
    ↓
validación
    ↓
INSERT
    ↓
redirect a index.php


EDITAR
editar.php?id=5
    ↓
SELECT
    ↓
formulario precargado
    ↓
POST
    ↓
UPDATE
    ↓
redirect


ELIMINAR
index.php
    ↓
formulario POST
    ↓
eliminar.php
    ↓
DELETE
    ↓
redirect

Buscar forma parte del Read

En un CRUD real, leer datos no significa solamente listar todos los registros.

También suele incluir:

  • búsqueda;
  • filtros;
  • ordenamiento;
  • paginación;
  • vista de detalle.

En nuestro ejemplo incorporamos búsqueda mediante LIKE.

Para profundizar en SQL: en este proyecto utilizamos SELECT, WHERE, LIKE, INSERT, UPDATE y DELETE. La teoría de esas sentencias está desarrollada en nuestra ruta de aprendizaje SQL / MySQL.

Separar responsabilidades

Aunque esta estructura es correcta para aprender, vemos que crear.php y editar.php comienzan a tener bastante código.

En proyectos mayores normalmente separamos:

acceso a datos
validación
lógica de negocio
controladores
vistas

Ese tipo de organización nos prepara para comprender posteriormente frameworks como Laravel.

Un posible siguiente paso: reutilizar el formulario

Crear y editar contienen HTML muy parecido.

Podríamos extraerlo a un archivo común, por ejemplo:

formulario.php

y reutilizarlo desde ambas pantallas.

Ese tipo de mejoras reduce duplicación, aunque en esta guía mantuvimos los archivos completos para que pueda entenderse cada flujo por separado.

Seguridad: lo que ya estamos haciendo

Nuestro CRUD aplica varias prácticas importantes:

  • consultas preparadas;
  • validación de formularios;
  • validación de IDs;
  • escapado de salida con htmlspecialchars();
  • POST para eliminaciones;
  • Post/Redirect/Get.

Lo que todavía falta en una aplicación real

Este CRUD es didáctico. Un sistema de producción todavía debería considerar, entre otras cosas:

  • autenticación de usuarios;
  • autorización y permisos;
  • protección CSRF;
  • registro controlado de errores;
  • validaciones de negocio más completas;
  • manejo de duplicados, por ejemplo emails únicos;
  • paginación para grandes cantidades de registros;
  • separación más clara de responsabilidades.

Qué ocurre si el email ya existe

La base tiene una restricción UNIQUE sobre email. Si intentamos insertar un email duplicado, MySQL rechazará la operación.

Esto muestra algo importante: la validación de PHP y las restricciones de la base de datos trabajan juntas.

La aplicación puede validar primero, pero la base debe seguir protegiendo la integridad de los datos.

Mejoras propuestas para practicar

  • agregar una página de detalle;
  • agregar teléfono;
  • agregar estado activo/inactivo;
  • permitir ordenar por nombre;
  • agregar filtro por edad;
  • agregar paginación;
  • mejorar el diseño con CSS;
  • reutilizar el formulario de crear/editar;
  • manejar el error de email duplicado;
  • agregar un campo de fecha de creación.

Ejercicios

  1. crear los cinco archivos del proyecto;
  2. probar el listado sin usuarios;
  3. crear tres usuarios;
  4. buscar por nombre;
  5. buscar por email;
  6. editar un usuario;
  7. eliminar un usuario;
  8. comprobar que actualizar la página no repite una operación POST;
  9. mostrar un mensaje cuando una búsqueda no devuelve resultados;
  10. agregar una página de detalle;
  11. agregar un campo nuevo a la tabla y a todos los formularios;
  12. implementar una mejora propia al CRUD.

Qué aprendimos

  • qué significa CRUD;
  • cómo organizar un CRUD sencillo en varios archivos;
  • cómo listar datos desde MySQL;
  • cómo implementar búsqueda;
  • cómo crear registros;
  • cómo precargar y editar registros;
  • cómo eliminar mediante POST;
  • cómo utilizar Post/Redirect/Get;
  • cómo integrar formularios, PDO, MySQL y HTML;
  • qué diferencias existen entre un ejemplo didáctico y una aplicación de producción.

Continuá aprendiendo SQL / MySQL

Este CRUD utiliza varias consultas SQL, pero el objetivo de esta guía es integrarlas con PHP. Para profundizar en consultas, filtros, relaciones, índices, normalización y otros temas de base de datos:

👉 Ver la ruta completa de SQL / MySQL

¿Qué sigue?

Después de construir nuestro primer CRUD, el siguiente paso natural es aprender a trabajar con sesiones y autenticación en PHP: iniciar sesión, mantener un usuario autenticado, cerrar sesión y proteger páginas privadas.

¿Te sirvió esta guía? ☕

Si este contenido te ayudó y querés apoyar a Club Programador, podés colaborar mediante:

☕ Apoyar con Mercado Pago

🌎 Apoyar con PayPal


Descubre más desde Club Programador

Suscríbete y recibe las últimas entradas en tu correo electrónico.

Deja un comentario

Descubre más desde Club Programador

Suscríbete ahora para seguir leyendo y obtener acceso al archivo completo.

Seguir leyendo