Ejemplo básico y seguro de INSERT, LISTAR y DELETE con PDO, prepared statements, PRG y token CSRF para borrar.
-- Tabla 'usuarios' CREATE TABLE usuarios ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, nombre VARCHAR(80) NOT NULL, email VARCHAR(120) NOT NULL UNIQUE, creado_en TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
/crud
config/
db.php
public/
form.html
insert.php
list.php
delete.php
<?php
// config/db.php
declare(strict_types=1);
function pdo(): PDO {
$dsn = 'mysql:host=localhost;dbname=miapp;charset=utf8mb4';
$user = 'usuario_app';
$pass = 'secreto';
return new PDO($dsn, $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]);
}
?>
<!DOCTYPE html>
<html lang="es">
<head><meta charset="UTF-8"><title>Alta de usuario</title></head>
<body>
<nav>
<a href="form.html">Alta</a> |
<a href="list.php">Listado</a>
</nav>
<h1>Alta de usuario</h1>
<form action="insert.php" method="post" autocomplete="off">
<label>Nombre</label>
<input type="text" name="nombre" required maxlength="80">
<label>Email</label>
<input type="email" name="email" required maxlength="120">
<button type="submit">Guardar</button>
</form>
<?php
// Mensaje flash por PRG
session_start();
if (!empty($_SESSION['flash'])) {
echo '<p>' . htmlspecialchars($_SESSION['flash'], ENT_QUOTES, 'UTF-8') . '</p>';
unset($_SESSION['flash']);
}
?>
</body>
</html>
<?php
declare(strict_types=1);
session_start();
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
exit('Método no permitido');
}
$nombre = trim($_POST['nombre'] ?? '');
$email = trim($_POST['email'] ?? '');
if ($nombre === '' || $email === '') {
$_SESSION['flash'] = 'Completá todos los campos.';
header('Location: form.html'); exit;
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$_SESSION['flash'] = 'Email inválido.';
header('Location: form.html'); exit;
}
require __DIR__ . '/../config/db.php';
try {
$pdo = pdo();
$stmt = $pdo->prepare('INSERT INTO usuarios (nombre, email) VALUES (:n, :e)');
$stmt->execute([':n' => $nombre, ':e' => $email]);
$_SESSION['flash'] = 'Usuario creado (ID: ' . (int)$pdo->lastInsertId() . ') ✅';
} catch (PDOException $ex) {
if ((int)($ex->errorInfo[1] ?? 0) === 1062) {
$_SESSION['flash'] = 'El email ya existe.';
} else {
// error_log($ex->getMessage());
$_SESSION['flash'] = 'Ocurrió un error. Intente nuevamente.';
}
}
header('Location: form.html'); exit;
?>
Lista todos los usuarios y ofrece un botón de borrado por POST con CSRF token.
<?php
declare(strict_types=1);
session_start();
require __DIR__ . '/../config/db.php';
$pdo = pdo();
// Generar token CSRF si no existe
if (empty($_SESSION['csrf'])) {
$_SESSION['csrf'] = bin2hex(random_bytes(16));
}
$csrf = $_SESSION['csrf'];
// Paginación simple (opcional)
$page = max(1, (int)($_GET['page'] ?? 1));
$perPage = 10;
$offset = ($page - 1) * $perPage;
// Total y fetch
$total = (int)$pdo->query('SELECT COUNT(*) FROM usuarios')->fetchColumn();
$stmt = $pdo->prepare('SELECT id, nombre, email, creado_en FROM usuarios ORDER BY id DESC LIMIT :lim OFFSET :off');
$stmt->bindValue(':lim', $perPage, PDO::PARAM_INT);
$stmt->bindValue(':off', $offset, PDO::PARAM_INT);
$stmt->execute();
$rows = $stmt->fetchAll();
?>
<!DOCTYPE html>
<html lang="es">
<head><meta charset="UTF-8"><title>Listado de usuarios</title></head>
<body>
<nav>
<a href="form.html">Alta</a> |
<a href="list.php">Listado</a>
</nav>
<h1>Usuarios (= $total ?>)</h1>
<?php
if (!empty($_SESSION['flash'])) {
echo '<p>' . htmlspecialchars($_SESSION['flash'], ENT_QUOTES, 'UTF-8') . '</p>';
unset($_SESSION['flash']);
}
?>
<table border="1" cellpadding="6" cellspacing="0">
<tr><th>ID</th><th>Nombre</th><th>Email</th><th>Creado</th><th>Acciones</th></tr>
<?php foreach ($rows as $r): ?>
<tr>
<td><?= (int)$r['id'] ?></td>
<td><?= htmlspecialchars($r['nombre'], ENT_QUOTES, 'UTF-8') ?></td>
<td><?= htmlspecialchars($r['email'], ENT_QUOTES, 'UTF-8') ?></td>
<td><?= htmlspecialchars($r['creado_en'], ENT_QUOTES, 'UTF-8') ?></td>
<td>
<form action="delete.php" method="post" onsubmit="return confirm('¿Borrar usuario?');" style="display:inline">
<input type="hidden" name="id" value="<?= (int)$r['id'] ?>">
<input type="hidden" name="csrf" value="<?= $csrf ?>">
<button type="submit">Borrar</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</table>
<!-- Paginación mínima -->
<div>
<?php
$pages = max(1, (int)ceil($total / $perPage));
for ($i=1; $i<=$pages; $i++) {
echo ($i === $page) ? "<strong>$i</strong> " : '<a href="?page='.$i.'">'.$i.'</a> ';
}
?>
</div>
</body>
</html>
Valida POST, token CSRF y el ID antes de borrar. Aplica PRG.
<?php
declare(strict_types=1);
session_start();
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
exit('Método no permitido');
}
$id = (int)($_POST['id'] ?? 0);
$csrf = $_POST['csrf'] ?? '';
if ($id <= 0 || empty($_SESSION['csrf']) || !hash_equals($_SESSION['csrf'], $csrf)) {
$_SESSION['flash'] = 'Solicitud inválida.';
header('Location: list.php'); exit;
}
require __DIR__ . '/../config/db.php';
try {
$pdo = pdo();
$stmt = $pdo->prepare('DELETE FROM usuarios WHERE id = :id');
$stmt->execute([':id' => $id]);
$_SESSION['flash'] = $stmt->rowCount() ? 'Usuario borrado ✅' : 'No se encontró el usuario.';
} catch (Throwable $ex) {
// error_log($ex->getMessage());
$_SESSION['flash'] = 'No se pudo borrar. Intente nuevamente.';
}
header('Location: list.php'); exit;
?>
htmlspecialchars(..., ENT_QUOTES, 'UTF-8').utf8mb4 y columnas en UTF-8 para evitar problemas de caracteres.config/db.php).UPDATE ... WHERE id = :id por POST con CSRF.