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
- crear los cinco archivos del proyecto;
- probar el listado sin usuarios;
- crear tres usuarios;
- buscar por nombre;
- buscar por email;
- editar un usuario;
- eliminar un usuario;
- comprobar que actualizar la página no repite una operación POST;
- mostrar un mensaje cuando una búsqueda no devuelve resultados;
- agregar una página de detalle;
- agregar un campo nuevo a la tabla y a todos los formularios;
- 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:
Descubre más desde Club Programador
Suscríbete y recibe las últimas entradas en tu correo electrónico.