🎯 Objetivo del tema
El administrador aún no ha insertado un video para esta sección.
🗺️ Mapa del tema
CRUD es el pan de cada día del desarrollo backend. El 80% de las aplicaciones web son variaciones de CRUD: un blog (CRUD de posts), una tienda (CRUD de productos), una red social (CRUD de usuarios/posts/comentarios), un sistema de tickets (CRUD de tickets). Dominar CRUD es dominar la base de la programación backend. Vamos a hacerlo bien desde el inicio.
El acrónimo más repetido del desarrollo backend.
SELECT con WHERE, ORDER BY, LIMIT, y los 3 patrones de listado.
INSERT con PDO y validación en el servidor.
UPDATE con formulario pre-rellenado y patrón POST-Redirect-GET.
DELETE con confirmación JavaScript y soft delete.
LIMIT y OFFSET: cómo mostrar 1 millón de registros sin romper la página.
2.5.1 Qué es CRUD y dónde se usa
CRUD son las 4 operaciones básicas que cualquier aplicación que persiste datos necesita: Create (crear), Read (leer), Update (actualizar), Delete (eliminar). Se mapean directamente a los 4 verbos SQL (INSERT, SELECT, UPDATE, DELETE) y a los 4 verbos HTTP (POST, GET, PUT/PATCH, DELETE).
| Operación | SQL | HTTP | URL típica | Ejemplo |
|---|---|---|---|---|
| Create (Crear) | INSERT | POST | /usuarios/crear | Registrar nuevo usuario. |
| Read (Leer) | SELECT | GET | /usuarios | Listar todos los usuarios. |
| Read (uno) | SELECT WHERE | GET | /usuarios/ver/5 | Ver detalles de un usuario. |
| Update (Actualizar) | UPDATE | POST o PUT | /usuarios/editar/5 | Modificar usuario. |
| Delete (Eliminar) | DELETE | POST o DELETE | /usuarios/eliminar/5 | Borrar usuario. |
El administrador aún no ha insertado un video para esta sección.
2.5.2 READ: listar y mostrar registros
La operación más común: mostrar datos. En PHP, esto se hace con un SELECT a la base de datos, recorrer los resultados con foreach, y mostrarlos en HTML. Es el patrón más repetido del backend.
El patrón: SELECT + foreach + HTML
<?php
// usuarios/listar.php: listar todos los usuarios
require_once '../conexion.php';
// 1. PREPARAR la consulta (con prepared statement)
$sql = 'SELECT id, nombre, email, edad, activo FROM usuarios ORDER BY id DESC';
$stmt = $pdo->prepare($sql);
$stmt->execute();
// 2. OBTENER todos los resultados
$usuarios = $stmt->fetchAll();
// 3. MOSTRAR en HTML
?>
<h1>Lista de usuarios (<?= count($usuarios) ?>)</h1>
<table>
<thead>
<tr><th>ID</th><th>Nombre</th><th>Email</th><th>Edad</th><th>Activo</th><th>Acciones</th></tr>
</thead>
<tbody>
<?php foreach ($usuarios as $u): ?>
<tr>
<td><?= htmlspecialchars($u['id']) ?></td>
<td><?= htmlspecialchars($u['nombre']) ?></td>
<td><?= htmlspecialchars($u['email']) ?></td>
<td><?= htmlspecialchars($u['edad']) ?></td>
<td><?= $u['activo'] ? '✅' : '❌' ?></td>
<td>
<a href="ver.php?id=<?= $u['id'] ?>">Ver</a> |
<a href="editar.php?id=<?= $u['id'] ?>">Editar</a> |
<a href="eliminar.php?id=<?= $u['id'] ?>">Eliminar</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<a href="crear.php">+ Crear nuevo usuario</a>Listar usuarios: SELECT + foreach + HTML
El administrador aún no ha insertado un video para esta sección.
2.5.3 CREATE: insertar nuevos registros
Insertar es la operación que añade nuevos registros. Viene siempre de un formulario (POST) que el usuario llena. PHP valida los datos, los inserta con INSERT, y redirige al listado (patrón PRG del Tema 2.3).
<?php
// usuarios/crear.php: formulario que crea un nuevo usuario
require_once '../conexion.php';
$errores = [];
$nombre = $email = $edad = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// 1. LEER y SANITIZAR inputs
$nombre = trim($_POST['nombre'] ?? '');
$email = trim($_POST['email'] ?? '');
$edad = trim($_POST['edad'] ?? '');
// 2. VALIDAR
if (empty($nombre)) $errores[] = 'El nombre es obligatorio.';
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) $errores[] = 'Email inválido.';
if (!filter_var($edad, FILTER_VALIDATE_INT, ['options' => ['min_range' => 0, 'max_range' => 120]])) {
$errores[] = 'Edad debe ser entre 0 y 120.';
}
// 3. Si NO hay errores, INSERT con prepared statement
if (empty($errores)) {
try {
$sql = 'INSERT INTO usuarios (nombre, email, edad) VALUES (?, ?, ?)';
$stmt = $pdo->prepare($sql);
$stmt->execute([$nombre, $email, $edad]);
// Patrón PRG: redirigir al listado
header('Location: listar.php?msg=creado');
exit;
} catch (PDOException $e) {
// Email duplicado (UNIQUE constraint) o similar
if ($e->getCode() == 23000) {
$errores[] = 'Ya existe un usuario con ese email.';
} else {
$errores[] = 'Error al guardar. Intenta más tarde.';
error_log($e->getMessage()); // log técnico, no al usuario
}
}
}
}
?>
<h1>Crear usuario</h1>
<?php if (!empty($errores)): ?>
<ul class="errores"><?php foreach ($errores as $e) echo "<li>$e</li>"; ?></ul>
<?php endif; ?>
<form method="post">
<input name="nombre" value="<?= htmlspecialchars($nombre) ?>" placeholder="Nombre">
<input name="email" type="email" value="<?= htmlspecialchars($email) ?>" placeholder="Email">
<input name="edad" type="number" value="<?= htmlspecialchars($edad) ?>" placeholder="Edad">
<button type="submit">Guardar</button>
<a href="listar.php">Cancelar</a>
</form>Crear usuario: formulario + validación + INSERT + PRG
El administrador aún no ha insertado un video para esta sección.
2.5.4 UPDATE: editar registros existentes
Editar es similar a crear, pero con dos diferencias: (1) precarga los valores actuales en el formulario, (2) usa UPDATE en vez de INSERT. La estructura de validación es idéntica.
<?php
// usuarios/editar.php: editar un usuario existente
require_once '../conexion.php';
// 1. SIEMPRE validar el id de la URL
$id = (int)($_GET['id'] ?? 0);
if ($id <= 0) { die('ID inválido'); }
$errores = [];
// 2. Si es GET, CARGAR los datos actuales para pre-rellenar el formulario
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
$stmt = $pdo->prepare('SELECT * FROM usuarios WHERE id = ?');
$stmt->execute([$id]);
$usuario = $stmt->fetch();
if (!$usuario) { die('Usuario no encontrado'); }
$nombre = $usuario['nombre'];
$email = $usuario['email'];
$edad = $usuario['edad'];
}
// 3. Si es POST, VALIDAR y ACTUALIZAR
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$nombre = trim($_POST['nombre'] ?? '');
$email = trim($_POST['email'] ?? '');
$edad = trim($_POST['edad'] ?? '');
$id = (int)($_POST['id'] ?? 0); // el id viene hidden en el form
// ... validaciones (igual que en crear.php) ...
if (empty($errores)) {
$sql = 'UPDATE usuarios SET nombre = ?, email = ?, edad = ? WHERE id = ?';
$stmt = $pdo->prepare($sql);
$stmt->execute([$nombre, $email, $edad, $id]);
header('Location: listar.php?msg=editado');
exit;
}
}
?>
<h1>Editar usuario #<?= $id ?></h1>
<form method="post">
<input type="hidden" name="id" value="<?= $id ?>">
<input name="nombre" value="<?= htmlspecialchars($nombre) ?>">
<input name="email" type="email" value="<?= htmlspecialchars($email) ?>">
<input name="edad" type="number" value="<?= htmlspecialchars($edad) ?>">
<button type="submit">Actualizar</button>
<a href="listar.php">Cancelar</a>
</form>Editar usuario: cargar + validar + UPDATE + PRG
El administrador aún no ha insertado un video para esta sección.
2.5.5 DELETE: eliminar con confirmación
Eliminar es la operación más PELIGROSA: un error aquí puede borrar datos irrecuperables. Por eso siempre: (1) pide confirmación, (2) usa POST (no GET) para evitar borrados accidentales por un enlace mal clickeado, (3) considera 'soft delete' en producción.
<?php
// usuarios/eliminar.php: eliminar con confirmación
require_once '../conexion.php';
// 1. Validar id
$id = (int)($_GET['id'] ?? 0);
if ($id <= 0) { die('ID inválido'); }
// 2. Si es GET, MOSTRAR página de confirmación
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
$stmt = $pdo->prepare('SELECT nombre, email FROM usuarios WHERE id = ?');
$stmt->execute([$id]);
$usuario = $stmt->fetch();
if (!$usuario) { die('Usuario no encontrado'); }
?>
<h1>¿Eliminar usuario?</h1>
<p>Estás a punto de eliminar a <strong><?= htmlspecialchars($usuario['nombre']) ?></strong> (<?= htmlspecialchars($usuario['email']) ?>).</p>
<p>Esta acción NO se puede deshacer.</p>
<form method="post">
<input type="hidden" name="id" value="<?= $id ?>">
<button type="submit" name="confirmar" value="si">Sí, eliminar</button>
<a href="listar.php">Cancelar</a>
</form>
<?php
}
// 3. Si es POST (usuario confirmó), EJECUTAR el DELETE
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['confirmar'] ?? '') === 'si') {
$id = (int)($_POST['id'] ?? 0);
$stmt = $pdo->prepare('DELETE FROM usuarios WHERE id = ?');
$stmt->execute([$id]);
header('Location: listar.php?msg=eliminado');
exit;
}
?>
<!-- Soft delete (alternativa PROFESIONAL): -->
<!-- En vez de DELETE, hacer UPDATE activo = FALSE: -->
<?php
// $stmt = $pdo->prepare('UPDATE usuarios SET activo = FALSE WHERE id = ?');
// $stmt->execute([$id]);
// Y en los SELECT, filtrar WHERE activo = TRUE
?>Eliminar con confirmación + soft delete
El administrador aún no ha insertado un video para esta sección.
2.5.6 Paginación: el patrón que escala
Si tu tabla tiene 1 millón de usuarios y los muestras todos en una sola página, el navegador tarda 30 segundos en renderizar y el servidor se queda sin memoria. La solución: paginación. Muestras solo 20-50 registros por página, con enlaces 'Anterior / Siguiente'.
<?php
// usuarios/listar.php CON PAGINACIÓN
require_once '../conexion.php';
// 1. Calcular total de registros
$total = $pdo->query('SELECT COUNT(*) FROM usuarios')->fetchColumn();
$por_pagina = 20;
$total_paginas = ceil($total / $por_pagina);
// 2. Obtener la página actual (con validación)
$pagina = max(1, min($total_paginas, (int)($_GET['pagina'] ?? 1)));
$offset = ($pagina - 1) * $por_pagina;
// 3. SELECT solo los registros de esta página
$stmt = $pdo->prepare('SELECT id, nombre, email, edad FROM usuarios ORDER BY id DESC LIMIT ? OFFSET ?');
$stmt->execute([$por_pagina, $offset]);
$usuarios = $stmt->fetchAll();
// 4. Mostrar + enlaces de paginación
?>
<h1>Usuarios (página <?= $pagina ?> de <?= $total_paginas ?>)</h1>
<table><?php foreach ($usuarios as $u): ?><tr>
<td><?= htmlspecialchars($u['nombre']) ?></td>
<td><?= htmlspecialchars($u['email']) ?></td>
</tr><?php endforeach; ?></table>
<div class="paginacion">
<?php if ($pagina > 1): ?>
<a href="?pagina=<?= $pagina - 1 ?>">← Anterior</a>
<?php endif; ?>
<?php for ($i = 1; $i <= $total_paginas; $i++): ?>
<a href="?pagina=<?= $i ?>" <?= $i === $pagina ? 'class="activa"' : '' ?>><?= $i ?></a>
<?php endfor; ?>
<?php if ($pagina < $total_paginas): ?>
<a href="?pagina=<?= $pagina + 1 ?>">Siguiente →</a>
<?php endif; ?>
</div>Paginación con LIMIT y OFFSET
El administrador aún no ha insertado un video para esta sección.
📚 Contenido ampliado
Material adicional, referencias externas verificadas y ejemplos extendidos.
🤖 AI Mission
CRUD se aprende construyendo uno, no leyendo sobre CRUD.
Misión: Construye un CRUD completo de gestión de PRODUCTOS (no usuarios) con PHP, MySQL y PDO. Implementa: listar paginado, crear, editar, eliminar con confirmación. Después, pídele a la IA que audite tu código y te sugiera 2 mejoras (UX o seguridad). Anota en tu cuaderno: ¿qué parte fue la más difícil? ¿qué te hizo perder más tiempo?
Pasos sugeridos
- Crea la tabla 'productos' en MySQL: id, nombre, descripcion, precio, stock, categoria, creado_en.
- Inserta 25 productos de prueba (5 categorías diferentes).
- Crea productos/listar.php con paginación (10 por página).
- Crea productos/crear.php con formulario + validación + INSERT.
- Crea productos/editar.php con carga previa + UPDATE.
- Crea productos/eliminar.php con confirmación POST.
- Estiliza con CSS externo: tabla limpia, formulario ordenado, mensajes de éxito/error.
- Prueba el flujo: crear 3 productos, editar 1, eliminar 1, navegar páginas.
- Intenta SQL injection: en el buscador, escribe ' OR '1'='1. ¿Tu prepared statement lo bloquea?
- Pídele a la IA: 'Tengo este CRUD de productos. Sugiere 2 mejoras: UX (búsqueda, orden, filtros) o seguridad (CSRF, rate limit). NO me des código, dime QUÉ agregar.'
- Aplica 2 mejoras. Vuelve a probar.
- Anota: 3 cosas que aprendiste HACIENDO que ningún tutorial te enseñó.
📓 Entregable: Capturas del CRUD funcionando (listar, crear, editar, eliminar), intento de SQL injection bloqueado, código de los 4 archivos PHP, y media página de cuaderno con tu reflexión.
🚫 Errores típicos de razonamiento
Por qué: Si pones <a href='eliminar.php?id=5'>, un usuario puede borrar registros sin querer haciendo clic, o un bot de Google puede seguir el enlace y borrar tu BD. SIEMPRE usa POST para acciones destructivas, con un formulario que pida confirmación.
Por qué: Si el usuario edita la URL manualmente y pone id=99999, tu UPDATE no actualizará nada (0 filas afectadas) pero tú pensarás que sí. O peor: tu DELETE borrará 0 registros y mostrarás 'eliminado correctamente' cuando no se eliminó nada. SIEMPRE verifica con un SELECT primero, o al menos chequea $stmt->rowCount() > 0 después.
Por qué: Si recibes ?id=X y X es 'abc' o un string malicioso, tu query puede romperse. SIEMPRE (int) $_GET['id'] para forzar a entero, y verifica que sea > 0 antes de usarlo. Es una línea de código que te ahorra horas de debugging.
Por qué: El usuario puede enviar precio = -1000, o stock = 'mucho', o email con un script. SIEMPRE valida con filter_var, ctype_digit, o verificaciones explícitas antes de hacer el INSERT/UPDATE. Tu capa de validación es la primera línea de defensa.
Por qué: Si después de INSERT/UPDATE/DELETE muestras el mensaje de éxito SIN redirigir, el usuario ve '¿Reenviar formulario?' al recargar, y si confirma, se crea otro registro duplicado. SIEMPRE header('Location: ...') + exit después de una operación que modifica datos.
🧪 Laboratorio práctico
El CRUD no se hace una vez: se hace 100 veces. Por eso se aprende bien la primera.
Laboratorio: CRUD completo de gestión de productos
Objetivo: Construir un CRUD profesional de productos con PHP, MySQL y PDO. La aplicación debe permitir: listar paginado, crear con validación, editar con carga previa, eliminar con confirmación. Listo para subir a un hosting con CPANEL.
Pasos
- Crea la tabla 'productos' en MySQL: id, nombre, descripcion, precio, stock, categoria, creado_en.
- Inserta 20+ productos de prueba (5 categorías).
- Estructura de archivos: productos/listar.php, crear.php, editar.php, eliminar.php, conexion.php, estilos.css.
- listar.php: SELECT con paginación (10 por página), tabla con columnas, enlaces a Ver/Editar/Eliminar.
- crear.php: formulario + validación (precio > 0, stock >= 0) + INSERT + PRG.
- editar.php: cargar datos del id + formulario pre-rellenado + UPDATE + PRG.
- eliminar.php: GET muestra confirmación, POST ejecuta DELETE. Soft delete con activo=FALSE.
- Mensajes de éxito: ?msg=creado / editado / eliminado en listar.php muestra banner verde.
- Estiliza con CSS: tabla limpia, formulario ordenado, errores en rojo, éxito en verde.
- Prueba el flujo: crear 3, editar 1, eliminar 1, navegar páginas, intentar SQL injection.
- Sube a tu hosting con CPANEL: archivos, BD, conexion.php actualizado.
- Verifica en HTTPS. Comparte la URL con un compañero y pídele feedback.
- Haz commit: 'feat: CRUD de productos con paginación, soft delete y deploy'.
📓 Entregable: URL pública del CRUD funcionando, capturas de las 4 operaciones, intento de SQL injection bloqueado, código de los 4 archivos PHP, y commit en Git.