Práctica 08: CRUD de Alumnos con PHP y PDO
Objetivo
Desarrollar una aplicación web CRUD (Create, Read, Update, Delete) en PHP utilizando PDO para la conexión a una base de datos MySQL. La aplicación permite gestionar el registro de alumnos de una escuela: matrícula, nombre, apellidos, semestre y especialidad.
Requisitos previos
- Servidor local con PHP (XAMPP, WAMP, Laragon, etc.)
- MySQL / MariaDB en ejecución
- Editor de código
- Conocimientos básicos de HTML, SQL y PHP
Estructura del proyecto
crud_alumnos/
├── config/
│ └── conexion.php # Conexión PDO a la base de datos
├── css/
│ └── estilos.css # Hoja de estilos
├── index.php # Listar alumnos (Read)
├── crear.php # Formulario de registro (Create - vista)
├── guardar.php # Insertar en la BD (Create - acción)
├── editar.php # Formulario de edición (Update - vista)
├── actualizar.php # Actualizar en la BD (Update - acción)
└── eliminar.php # Borrar registro (Delete)
Idea clave: el proyecto separa las vistas (formularios HTML) de las acciones (scripts PHP que solo procesan datos y redirigen). Cada operación CRUD tiene su propio archivo, lo que facilita su mantenimiento.
Paso 1 — Crear la base de datos
En phpMyAdmin (o el cliente MySQL de tu preferencia) ejecuta:
CREATE DATABASE IF NOT EXISTS escuela
CHARACTER SET utf8mb4
COLLATE utf8mb4_general_ci;
USE escuela;
CREATE TABLE alumnos (
id INT AUTO_INCREMENT PRIMARY KEY,
matricula VARCHAR(10) NOT NULL,
nombre VARCHAR(50) NOT NULL,
apellidos VARCHAR(100) NOT NULL,
semestre INT NOT NULL,
especialidad VARCHAR(50) NOT NULL
);
Partes importantes
utf8mb4permite guardar acentos y caracteres especiales sin problemas.AUTO_INCREMENT PRIMARY KEYgenera unidúnico para cada alumno automáticamente.- Los tipos
VARCHARse dimensionan según el contenido esperado.
Puedes insertar algunos datos de prueba:
INSERT INTO alumnos (matricula, nombre, apellidos, semestre, especialidad)
VALUES
('2024001', 'Ana', 'García López', 3, 'Programación'),
('2024002', 'Luis', 'Martínez Pérez', 1, 'Contabilidad'),
('2024003', 'María', 'Hernández Ruiz', 5, 'Administración');
Paso 2 — Conexión a la base de datos con PDO
Crear config/conexion.php:
<?php
$host = "127.0.0.1";
$dbname = "escuela";
$user = "root";
$password = "TU_PASSWORD";
try {
$pdo = new PDO(
"mysql:host=$host;dbname=$dbname;charset=utf8mb4",
$user,
$password
);
$pdo->setAttribute(
PDO::ATTR_ERRMODE,
PDO::ERRMODE_EXCEPTION
);
} catch (PDOException $e) {
die("Error de conexión: " . $e->getMessage());
}
Partes importantes
- PDO (PHP Data Objects) es una capa de abstracción que permite conectarse a distintos gestores de base de datos con la misma interfaz.
- El primer argumento del constructor es el DSN (Data Source Name):
indica el motor (
mysql), el host, la base de datos y el charset. PDO::ATTR_ERRMODE = PDO::ERRMODE_EXCEPTIONhace que los errores de SQL lancen excepciones, en lugar de fallar silenciosamente. Esto facilita detectar errores durante el desarrollo.- El
try/catchcaptura un posible fallo de conexión y termina el script con un mensaje, evitando que el resto de la página se ejecute sin base de datos. - En todos los demás archivos solo se incluye con
require_once, obteniendo la variable$pdolista para usarse:
require_once "config/conexion.php";
Paso 3 — Listar alumnos (Read)
Crear index.php. Este archivo hace dos cosas: primero consulta la base
de datos y después imprime los resultados en una tabla HTML.
<?php
require_once "config/conexion.php";
$sql = "SELECT * FROM alumnos ORDER BY id DESC";
$stmt = $pdo->query($sql);
$alumnos = $stmt->fetchAll(PDO::FETCH_ASSOC);
?>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>Lista de alumnos</title>
<link rel="stylesheet" href="css/estilos.css">
</head>
<body>
<h1>Lista de alumnos</h1>
<a href="crear.php">Registrar alumno</a>
<br><br>
<table border="1">
<thead>
<tr>
<th>ID</th>
<th>Matrícula</th>
<th>Nombre</th>
<th>Apellidos</th>
<th>Semestre</th>
<th>Especialidad</th>
<th>Acciones</th>
</tr>
</thead>
<tbody>
<?php foreach ($alumnos as $alumno): ?>
<tr>
<td><?= $alumno['id'] ?></td>
<td><?= htmlspecialchars($alumno['matricula']) ?></td>
<td><?= htmlspecialchars($alumno['nombre']) ?></td>
<td><?= htmlspecialchars($alumno['apellidos']) ?></td>
<td><?= $alumno['semestre'] ?></td>
<td><?= htmlspecialchars($alumno['especialidad']) ?></td>
<td>
<a href="editar.php?id=<?= $alumno['id'] ?>">
Editar
</a>
|
<a href="eliminar.php?id=<?= $alumno['id'] ?>"
onclick="return confirm('¿Deseas eliminar este alumno?')">
Eliminar
</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</body>
</html>
Partes importantes
$pdo->query($sql)ejecuta elSELECTdirectamente. Es seguro usarlo aquí porque la consulta no recibe datos del usuario.fetchAll(PDO::FETCH_ASSOC)devuelve todas las filas como un arreglo de arreglos asociativos (las llaves son los nombres de las columnas).foreach ... endforeaches la sintaxis alternativa de PHP, muy cómoda para mezclar con HTML porque evita llaves{ }entre etiquetas.<?= $variable ?>es la forma corta de<?php echo $variable ?>.htmlspecialchars()convierte caracteres como<,>y"en entidades HTML. Es indispensable para evitar ataques XSS (inyección de código en la página) si un campo contiene HTML o JavaScript.- Los enlaces Editar/Eliminar envían el
iddel registro por la URL:editar.php?id=3. Ese valor se leerá con$_GET['id']. onclick="return confirm(...)"pide confirmación en el navegador antes de ejecutar el borrado.
Paso 4 — Registrar alumno (Create)
Se divide en dos archivos: el formulario (crear.php) y el script que
inserta (guardar.php).
4.1 Formulario — crear.php
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>Registrar alumno</title>
<link rel="stylesheet" href="css/estilos.css">
</head>
<body>
<h1>Registrar alumno</h1>
<form action="guardar.php" method="POST">
<label>Matrícula:</label>
<br>
<input type="text" name="matricula" required>
<br><br>
<label>Nombre:</label>
<br>
<input type="text" name="nombre" required>
<br><br>
<label>Apellidos:</label>
<br>
<input type="text" name="apellidos" required>
<br><br>
<label>Semestre:</label>
<br>
<input type="number" name="semestre" min="1" max="6" required>
<br><br>
<label>Especialidad:</label>
<br>
<select name="especialidad" required>
<option value="">Selecciona una especialidad</option>
<option value="Programación">Programación</option>
<option value="Contabilidad">Contabilidad</option>
<option value="Administración">Administración</option>
<option value="Ventas">Ventas</option>
<option value="Construcción">Construcción</option>
</select>
<br><br>
<button type="submit">Guardar alumno</button>
</form>
<br>
<a href="index.php">Regresar</a>
</body>
</html>
Partes importantes
method="POST"envía los datos en el cuerpo de la petición HTTP (no en la URL). Se usa POST porque el formulario modifica el estado del servidor.- El atributo
namede cada input define la llave con la que se leerá el valor en PHP:name="matricula"→$_POST['matricula']. required,minymaxson validaciones del lado del cliente. Ayudan a la experiencia de usuario, pero no sustituyen la validación en el servidor.- El
<select>restringe la especialidad a un catálogo fijo de opciones.
4.2 Insertar en la base de datos — guardar.php
<?php
require_once "config/conexion.php";
$matricula = $_POST['matricula'];
$nombre = $_POST['nombre'];
$apellidos = $_POST['apellidos'];
$semestre = $_POST['semestre'];
$especialidad = $_POST['especialidad'];
$sql = "INSERT INTO alumnos
(matricula, nombre, apellidos, semestre, especialidad)
VALUES
(:matricula, :nombre, :apellidos, :semestre, :especialidad)";
$stmt = $pdo->prepare($sql);
$stmt->execute([
':matricula' => $matricula,
':nombre' => $nombre,
':apellidos' => $apellidos,
':semestre' => $semestre,
':especialidad' => $especialidad
]);
header("Location: index.php");
exit;
Partes importantes
- Consulta preparada: en lugar de concatenar valores dentro del SQL, se
usan marcadores con nombre (
:matricula, etc.). $pdo->prepare($sql)compila la consulta;$stmt->execute([...])envía los valores por separado. Esto protege contra inyección SQL, porque los datos nunca se interpretan como parte del comando SQL.header("Location: index.php")redirige al listado después de insertar.exitdetiene el script inmediatamente (obligatorio tras unLocation).- Este patrón (procesar el POST y luego redirigir) se conoce como Post/Redirect/Get y evita que al refrescar la página el navegador reenvíe el formulario duplicando el registro.
Paso 5 — Editar alumno (Update)
También se divide en dos archivos: editar.php carga los datos actuales
en un formulario y actualizar.php ejecuta el UPDATE.
5.1 Formulario precargado — editar.php
<?php
require_once "config/conexion.php";
$id = $_GET['id'];
$sql = "SELECT * FROM alumnos WHERE id = :id";
$stmt = $pdo->prepare($sql);
$stmt->execute([
':id' => $id
]);
$alumno = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$alumno) {
die("Alumno no encontrado");
}
?>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>Editar alumno</title>
<link rel="stylesheet" href="css/estilos.css">
</head>
<body>
<h1>Editar alumno</h1>
<form action="actualizar.php" method="POST">
<input type="hidden" name="id" value="<?= $alumno['id'] ?>">
<label>Matrícula:</label>
<br>
<input type="text" name="matricula"
value="<?= htmlspecialchars($alumno['matricula']) ?>" required>
<br><br>
<label>Nombre:</label>
<br>
<input type="text" name="nombre"
value="<?= htmlspecialchars($alumno['nombre']) ?>" required>
<br><br>
<label>Apellidos:</label>
<br>
<input type="text" name="apellidos"
value="<?= htmlspecialchars($alumno['apellidos']) ?>" required>
<br><br>
<label>Semestre:</label>
<br>
<input type="number" name="semestre" min="1" max="6"
value="<?= $alumno['semestre'] ?>" required>
<br><br>
<label>Especialidad:</label>
<br>
<input type="text" name="especialidad"
value="<?= htmlspecialchars($alumno['especialidad']) ?>" required>
<br><br>
<button type="submit">Actualizar</button>
</form>
<br>
<a href="index.php">Regresar</a>
</body>
</html>
Partes importantes
$_GET['id']recibe el identificador que viaja en la URL (editar.php?id=3).- La consulta usa un marcador
:id: aunque el valor venga de la URL (controlado por el usuario), la consulta preparada lo trata siempre como dato, nunca como SQL. fetch()(singular) devuelve una sola fila. Si no existe el registro retornafalse, y el script termina condie().- El campo oculto
<input type="hidden" name="id">es fundamental: es la forma de decirle aactualizar.phpqué registro debe modificar. - Los
value="<?= ... ?>"precargan el formulario con los datos actuales. Nuevamente se usahtmlspecialchars()para no romper el HTML si el valor contiene comillas.
5.2 Actualizar en la base de datos — actualizar.php
<?php
require_once "config/conexion.php";
$id = $_POST['id'];
$matricula = $_POST['matricula'];
$nombre = $_POST['nombre'];
$apellidos = $_POST['apellidos'];
$semestre = $_POST['semestre'];
$especialidad = $_POST['especialidad'];
$sql = "UPDATE alumnos SET
matricula = :matricula,
nombre = :nombre,
apellidos = :apellidos,
semestre = :semestre,
especialidad = :especialidad
WHERE id = :id";
$stmt = $pdo->prepare($sql);
$stmt->execute([
':matricula' => $matricula,
':nombre' => $nombre,
':apellidos' => $apellidos,
':semestre' => $semestre,
':especialidad' => $especialidad,
':id' => $id
]);
header("Location: index.php");
exit;
Partes importantes
- El
WHERE id = :ides crítico: sin él, elUPDATEmodificaría todos los registros de la tabla. - El
idllega por$_POSTgracias al campo oculto del formulario. - La estructura es idéntica a
guardar.php: preparar → ejecutar → redirigir.
Paso 6 — Eliminar alumno (Delete)
Crear eliminar.php. No necesita formulario: recibe el id por la URL y
borra el registro directamente (la confirmación ya la hizo el confirm()
de JavaScript en index.php).
<?php
require_once "config/conexion.php";
$id = $_GET['id'];
$sql = "DELETE FROM alumnos WHERE id = :id";
$stmt = $pdo->prepare($sql);
$stmt->execute([
':id' => $id
]);
header("Location: index.php");
exit;
Partes importantes
- Igual que en el
UPDATE, elWHERE id = :iddelimita un solo registro. - Nota de diseño: en aplicaciones reales el borrado por
GETes riesgoso (un rastreador o pre-carga del navegador podría ejecutarlo). Lo correcto es hacerlo con un formularioPOST. Aquí se usaGETpor simplicidad didáctica, pero conviene mencionarlo en clase.
Paso 7 — Estilos (opcional)
El proyecto incluye css/estilos.css con estilos para el contenedor, la
tabla, los botones (.btn-primary, .btn-danger, etc.) y formularios.
También define clases pensadas para los retos (.buscador, .container,
.form-group), por lo que al aplicar class="container" al <body> o un
<div> principal, la apariencia mejora sin tocar más CSS.
Flujo completo de la aplicación
index.php ──Registrar──▶ crear.php ──POST──▶ guardar.php ──▶ index.php
│
├──Editar──▶ editar.php ──POST──▶ actualizar.php ──▶ index.php
│
└──Eliminar──▶ eliminar.php ──▶ index.php
Conceptos aprendidos
| Concepto | Dónde se usa |
|---|---|
| Conexión PDO y DSN | config/conexion.php |
query() vs prepare()/execute() |
index.php vs resto |
Marcadores con nombre (:campo) |
guardar.php, editar.php, actualizar.php, eliminar.php |
$_GET y $_POST |
Enlaces con ?id=, formularios |
fetch() / fetchAll() |
editar.php / index.php |
htmlspecialchars() contra XSS |
Salidas HTML |
| Post/Redirect/Get | guardar.php, actualizar.php, eliminar.php |
Retos adicionales
Los siguientes retos deberás implementarlos desde cero, modificando los archivos del proyecto. Solo se indican los requisitos y algunas pistas.
Reto 1 — Búsqueda
Agregar en index.php un formulario de búsqueda que permita encontrar
alumnos por nombre, apellidos o matrícula.
- El formulario envía el término por
GET(index.php?q=texto). - La consulta SQL debe usar
WHERE ... LIKEcon comodines%. - Pista: el comodín va en el valor, no en el SQL:
'%' . $q . '%'pasado como parámetro del marcador. - Pista: para buscar en varias columnas se puede usar
ORoCONCAT(nombre, ' ', apellidos) LIKE :q. - Si no hay término de búsqueda, se muestra la lista completa.
Reto 2 — Filtrar por especialidad
Agregar en index.php un <select> con las especialidades disponibles que
filtre la tabla al elegir una opción.
- El filtro puede combinarse con la búsqueda del Reto 1
(dos condiciones en el
WHEREunidas conAND). - Pista: la consulta se arma dinámicamente; solo agrega el
AND especialidad = :especialidadcuando el filtro tenga valor. - El
<select>debe recordar la opción elegida (selectedcuando coincide con$_GET). - Incluir una opción "Todas" que quite el filtro.
Reto 3 — Validar matrícula duplicada
Evitar que se registren dos alumnos con la misma matrícula.
- Antes del
INSERTenguardar.php, hacer unSELECTpara comprobar si la matrícula ya existe. - Si existe, redirigir a
crear.phpmostrando un mensaje de error y sin perder los datos que el usuario ya escribió. - Repetir la validación en
actualizar.php, pero excluyendo al propio alumno:WHERE matricula = :matricula AND id != :id. - Pista (defensa en profundidad): además de la validación en PHP, agrega
una restricción en la base de datos:
ALTER TABLE alumnos ADD UNIQUE (matricula);
Reto 4 — Mensajes mediante $_GET
Mostrar mensajes de éxito/error en la interfaz después de cada operación.
- En lugar de redirigir a
index.php"a secas", hacerlo con un parámetro:header("Location: index.php?msg=creado"),?msg=actualizado,?msg=eliminado,?msg=error, etc. - En
index.php, leer$_GET['msg']y mostrar un aviso (<div class="alerta">) con el texto correspondiente. - Usar una estructura tipo
switcho arreglo asociativo para mapear el código de mensaje a su texto (evita imprimir directamente el valor de$_GET). - Aplicar el mismo mecanismo para el error del Reto 3
(por ejemplo
crear.php?error=duplicada). - Pista: los mensajes por
$_GETson una versión sencilla del patrón flash messages que usan los frameworks con sesiones.
Rúbrica sugerida
| Criterio | Puntos |
|---|---|
| Base de datos creada correctamente | 10 |
| Conexión PDO funcional | 10 |
| Listado de alumnos | 15 |
| Registro de alumnos | 15 |
| Edición y actualización | 15 |
| Eliminación con confirmación | 10 |
Consultas preparadas y htmlspecialchars() |
10 |
| Retos adicionales (5 c/u) | 20 |
| Total | 100 |