INSERT, SELECT,
UPDATE y DELETE. Con PDO y consultas preparadas, validación en el servidor,
escape al imprimir y POST para todo lo que modifica datos, tienes la base de casi cualquier panel de
administración.
CRUD viene de Create, Read, Update, Delete. Si piensas en cualquier aplicación (un blog, una tienda, una agenda), detrás casi siempre hay varios CRUD. Por eso es el ejercicio que consolida todo lo anterior: formularios, conexión a MySQL y consultas seguras. En el libro Programación Full-Stack el CRUD completo llega justo antes de las APIs, el despliegue y los tres proyectos finales. Aquí construimos uno de productos usando la tabla del artículo sobre PDO.
Estructura del proyecto
crud-productos/
├── config/
│ └── db.php # función db() con la conexión PDO
├── index.php # Read: listado
├── crear.php # Create: formulario + INSERT
├── editar.php # Update: formulario + UPDATE
├── eliminar.php # Delete: solo acepta POST
└── helpers.php # e() para escapar y validarProducto()
<?php
function e(?string $v): string
{
return htmlspecialchars($v ?? '', ENT_QUOTES, 'UTF-8');
}
// Devuelve [datos limpios, errores]: la usan crear.php y editar.php
function validarProducto(array $in): array
{
$datos = [
'nombre' => trim($in['nombre'] ?? ''),
'precio' => $in['precio'] ?? '',
'stock' => $in['stock'] ?? '',
];
$errores = [];
if ($datos['nombre'] === '' || mb_strlen($datos['nombre']) > 120) {
$errores['nombre'] = 'Nombre obligatorio (máx. 120 caracteres).';
}
if (filter_var($datos['precio'], FILTER_VALIDATE_FLOAT) === false || $datos['precio'] < 0) {
$errores['precio'] = 'Precio no válido.';
}
if (filter_var($datos['stock'], FILTER_VALIDATE_INT, ['options' => ['min_range' => 0]]) === false) {
$errores['stock'] = 'El stock debe ser un número entero positivo.';
}
return [$datos, $errores];
}
Centralizar la validación en una función evita el error más común de los CRUD: validar bien al crear y olvidarse al editar.
Read: listar los productos
<?php
require_once __DIR__ . '/config/db.php';
require_once __DIR__ . '/helpers.php';
$productos = db()->query('SELECT id, nombre, precio, stock FROM productos ORDER BY id DESC')->fetchAll();
?>
<h1>Productos</h1>
<a href="crear.php">+ Nuevo producto</a>
<table>
<thead>
<tr><th>Nombre</th><th>Precio</th><th>Stock</th><th>Acciones</th></tr>
</thead>
<tbody>
<?php foreach ($productos as $p): ?>
<tr>
<td><?= e($p['nombre']) ?></td>
<td>$<?= number_format((float) $p['precio'], 2, ',', '.') ?></td>
<td><?= (int) $p['stock'] ?></td>
<td>
<a href="editar.php?id=<?= (int) $p['id'] ?>">Editar</a>
<form action="eliminar.php" method="post"
onsubmit="return confirm('¿Eliminar este producto?')">
<input type="hidden" name="id" value="<?= (int) $p['id'] ?>">
<button type="submit">Eliminar</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
Create: alta de productos
<?php
require_once __DIR__ . '/config/db.php';
require_once __DIR__ . '/helpers.php';
$datos = ['nombre' => '', 'precio' => '', 'stock' => '0'];
$errores = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
[$datos, $errores] = validarProducto($_POST);
if (!$errores) {
$stmt = db()->prepare('INSERT INTO productos (nombre, precio, stock) VALUES (:nombre, :precio, :stock)');
$stmt->execute($datos);
header('Location: index.php'); // patrón PRG
exit;
}
}
?>
<form method="post">
<label for="nombre">Nombre</label>
<input id="nombre" name="nombre" value="<?= e($datos['nombre']) ?>">
<small><?= e($errores['nombre'] ?? '') ?></small>
<label for="precio">Precio</label>
<input id="precio" name="precio" type="number" step="0.01" min="0" value="<?= e((string) $datos['precio']) ?>">
<small><?= e($errores['precio'] ?? '') ?></small>
<label for="stock">Stock</label>
<input id="stock" name="stock" type="number" min="0" value="<?= e((string) $datos['stock']) ?>">
<small><?= e($errores['stock'] ?? '') ?></small>
<button type="submit">Guardar</button>
</form>
Como las claves del array $datos coinciden con los marcadores :nombre,
:precio y :stock, se puede pasar directamente a execute().
Update: editar un producto
<?php
require_once __DIR__ . '/config/db.php';
require_once __DIR__ . '/helpers.php';
$id = filter_var($_GET['id'] ?? '', FILTER_VALIDATE_INT);
$stmt = db()->prepare('SELECT nombre, precio, stock FROM productos WHERE id = ?');
$stmt->execute([$id]);
$datos = $stmt->fetch();
if (!$datos) {
http_response_code(404);
exit('Producto no encontrado.');
}
$errores = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
[$datos, $errores] = validarProducto($_POST);
if (!$errores) {
$stmt = db()->prepare(
'UPDATE productos SET nombre = :nombre, precio = :precio, stock = :stock WHERE id = :id'
);
$stmt->execute($datos + ['id' => $id]);
header('Location: index.php');
exit;
}
}
// ...el mismo formulario que en crear.php, rellenado con $datos
AND usuario_id = ? en el SELECT, el UPDATE y el
DELETE. Si no, cualquiera podría editar registros ajenos cambiando el id de la URL.
Delete: eliminar con POST
Eliminar con un enlace eliminar.php?id=5 parece cómodo, pero es peligroso: los navegadores,
los rastreadores o una simple imagen en otra web pueden “visitar” ese enlace. Las acciones que modifican
datos se hacen siempre con POST.
<?php
require_once __DIR__ . '/config/db.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405); // Method Not Allowed
exit;
}
$id = filter_var($_POST['id'] ?? '', FILTER_VALIDATE_INT);
if ($id) {
$stmt = db()->prepare('DELETE FROM productos WHERE id = ?');
$stmt->execute([$id]);
}
header('Location: index.php');
exit;
Siguientes pasos
Con esta base funcionando, estas son las mejoras naturales:
- Proteger el panel con el sistema de login seguro para que solo los administradores accedan.
- Paginación con
LIMITyOFFSETcuando la tabla crezca. - Búsqueda con
LIKEy un formulario GET. - Tokens CSRF en los formularios de crear, editar y eliminar.
- Mensajes flash (“Producto guardado”) guardados en la sesión.
- Convertirlo en API para manejarlo desde JavaScript sin recargar, como vemos en el artículo sobre APIs REST.
Del CRUD a una tienda online
En el libro Programación Full-Stack, después del CRUD completo construyes un blog, un sistema de usuarios y una tienda online básica.
Comprar en Amazon