Lab 06 — Formularios, validación, PRG y flash messages¶
| Campo | Valor |
|---|---|
| Módulo | Implantación de Aplicaciones Web (IMW) |
| Ciclo | Administración de Sistemas Informáticos en Red (ASIR) |
| Unidad | UT4 — PHP en servidor |
| Criterios de evaluación (CE) | f (formularios y entrada de datos), g (procesamiento de datos en el servidor) |
| Requisitos | Labs 01, 02, 03, 04 y 05 completados |
| Agrupación | Individual |
| Herramientas | Navegador + editor de texto + terminal |
| Apuntes | https://ichigar.codeberg.page/imw/recursos/ut4_php_06_formularios_y_validacion/ |
| Tiempo estimado | 120–150 minutos |
Antes de empezar¶
Entorno de trabajo
Este lab continúa el proyecto ~/gestor/ que vienes ampliando desde el Lab 01. Al final del Lab 05, la estructura quedó así:
gestor/
├── public/
│ ├── index.php, info_servidor.php (Lab 01)
│ ├── variables.php, resumen.php (Lab 02)
│ ├── arrays.php, listado.php, catalogo.php (Lab 03)
│ ├── estado.php (Lab 04)
│ └── css/estilos.css (Lab 05)
├── templates/
│ ├── _header.php (Lab 05)
│ └── _footer.php (Lab 05)
└── src/
Si http://gestor.local/listado.php no se ve con la cabecera azul, el menú y el pie en el año actual, revisa el Lab 05 antes de continuar: vamos a partir del listado.php con plantillas.
Qué vamos a hacer
Hasta ahora todas tus páginas solo mostraban información: arrays, condiciones, plantillas. Hoy el navegador empezará a enviar datos al servidor. Vamos a:
- Añadir un formulario de búsqueda GET a
listado.phppara filtrar incidencias por título desde la URL. - Crear
public/nueva.phpcon un formulario POST para registrar incidencias nuevas. - Implementar validación en el servidor (campos requeridos, longitud, valor en una lista cerrada) que muestre los errores junto a cada campo y conserve lo que el usuario ya había escrito.
- Aplicar el patrón PRG (Post-Redirect-Get) para que recargar la página no reenvíe el formulario.
- Mostrar un flash message de confirmación en
listado.phpcuando se acabe de crear una incidencia. - En el paso "aplica lo aprendido": añadir un selector de filtro por estado al
listado.php.
GET y POST en este lab
Hoy verás los dos métodos HTTP que usan los formularios. El Paso 1 trabaja con GET (buscar y filtrar, datos en la URL); a partir del Paso 2 entra POST (crear datos, en el cuerpo de la petición). La regla de cuándo usar cada uno la repaso en cada paso.
Cómo se entrega este lab¶
Al terminar, empaqueta tu trabajo en un ZIP con esta estructura exacta:
apellido_nombre_lab06/
├── gestor/
│ ├── public/
│ │ ├── listado.php
│ │ └── nueva.php
│ └── templates/
│ └── _header.php
├── capturas/
│ ├── paso1_busqueda_get.png
│ ├── paso2_var_dump_post.png
│ ├── paso3_validacion_errores.png
│ ├── paso4_devtools_prg.png
│ ├── paso5_flash_exito.png
│ └── paso6_filtro_estado.png
└── RESPUESTAS.md
Nombres prescritos
Los nombres de los ficheros y carpetas son obligatorios y exactos. No uses mayúsculas, espacios ni acentos donde no los haya. El script de corrección busca estos nombres concretos: si los cambias, tu entrega aparecerá como incompleta.
Solo los ficheros que toca este lab
En el ZIP solo se incluyen los ficheros nuevos o modificados en este lab: listado.php actualizado (Pasos 1, 5 y 6), nueva.php nuevo (Pasos 2, 3, 4 y 5) y _header.php con session_start() añadido (Paso 5). El resto de tu gestor/ (incluidos _footer.php, estilos.css, estado.php, etc.) se queda en tu servidor y no se entrega.
Cómo preparar la entrega:
- Crea una carpeta en tu equipo llamada
apellido_nombre_lab06(sustituye por tu apellido y nombre reales, en minúsculas y sin espacios ni acentos). - Dentro, reproduce la estructura de arriba con los tres ficheros PHP en sus rutas exactas y la subcarpeta
capturas/con los seis PNG renombrados como se indica en cada paso. - Añade un fichero
RESPUESTAS.mden la raíz con una sección## Paso Npor cada pregunta del lab (la plantilla está en el anexo). - Comprime la carpeta completa:
- Windows: clic derecho sobre la carpeta → Enviar a → Carpeta comprimida (en zip). El archivo resultante debe llamarse
apellido_nombre_lab06.zip. - Linux/macOS:
zip -r apellido_nombre_lab06.zip apellido_nombre_lab06/.
- Windows: clic derecho sobre la carpeta → Enviar a → Carpeta comprimida (en zip). El archivo resultante debe llamarse
- Abre el ZIP y comprueba que al descomprimirlo aparece una sola carpeta raíz
apellido_nombre_lab06/.
Comprueba la estructura del ZIP antes de subirlo
En Windows es habitual comprimir los ficheros de dentro de la carpeta en lugar de la carpeta completa. Eso genera un ZIP sin carpeta raíz, y el script de corrección lo rechaza. Comprime la carpeta apellido_nombre_lab06/, no su contenido.
Paso 1 — Búsqueda con GET en listado.php¶
Conceptos
- Un formulario HTML con
method="get"envía sus campos como querystring en la URL:listado.php?busqueda=VPN. El servidor los lee en$_GET. - GET es apropiado para acciones que no modifican datos: buscar, filtrar, navegar. La URL con los parámetros se puede compartir o marcar como favorito.
- Cuando un mismo fichero sirve la página vacía y la página con resultados, distinguimos los dos casos según si
$_GET['busqueda']viene con valor o no. value="<?= htmlspecialchars($busqueda) ?>"en el<input>repuebla el campo de búsqueda con lo que el usuario escribió: así no tiene que volver a teclearlo si quiere refinar la búsqueda.- Funciones nuevas de PHP que aparecen en este paso:
trim($cadena)devuelve la cadena sin espacios al principio ni al final. Sin él, una búsqueda con un espacio accidental ('VPN ') se trataría como distinta de'VPN'.stripos($paja, $aguja)busca la subcadena$agujadentro de$pajasin distinguir mayúsculas y minúsculas (la "i" del nombre viene de insensitive). Devuelve la posición donde la encuentra (un número) ofalsesi no aparece. Por eso comparamos con!== falsey no con> 0: la posición0(encontrado al principio) es un valor válido.
Objetivo. Añadir un formulario de búsqueda en listado.php que filtre las incidencias por título. La URL final debe quedar tipo http://gestor.local/listado.php?busqueda=VPN.
Tarea. Reescribe ~/gestor/public/listado.php para que quede así (mantén el array $incidencias que ya tenías; en el lab anterior eran 3, aquí lo ampliamos a 5 para que la búsqueda tenga algo que filtrar):
El patrón "construyo un array vacío, recorro con foreach y meto con $incidencias[] = ... solo lo que cumple la condición" es el mismo que ya usaste en A3 al construir listas. La condición aquí es la de stripos(), que ya tienes glosada arriba.
Recarga http://gestor.local/listado.php y prueba:
- Busca VPN → la URL pasa a
listado.php?busqueda=VPNy la tabla deja una sola fila. - Busca serv en minúsculas → encuentra "Servidor web caído". Aunque la 'S' del título es mayúscula y tu búsqueda está en minúsculas, aparece igual: ahí ves que
stripos()ignora mayúsculas/minúsculas. - Busca xxx → la tabla queda vacía y el contador dice "0 incidencia(s)".
- Pulsa Limpiar → la URL vuelve a
listado.php(sin parámetro) y reaparecen las 5.
Responde en RESPUESTAS.md, sección ## Paso 1.
- Cuando buscas "VPN", copia la URL completa que aparece en la barra de direcciones del navegador, tal cual (incluyendo
http://y todo).- ¿Por qué es importante el
value="<?= htmlspecialchars($busqueda) ?>"del<input>? ¿Qué pasaría si lo quitaras y buscaras "VPN"?
Captura requerida. Visita http://gestor.local/listado.php?busqueda=VPN y guarda una captura en capturas/paso1_busqueda_get.png. Debe verse:
- La URL completa con
?busqueda=VPNen la barra de direcciones. - El campo de búsqueda con "VPN" dentro.
- La tabla mostrando solo la incidencia "VPN no conecta".
- El enlace "Limpiar" visible.
Verificación antes de pasar al Paso 2.
-
listado.phplee$_GET['busqueda']contrim()y?? ''. - El formulario tiene
method="get"y un único camponame="busqueda". - El campo conserva el texto buscado con
htmlspecialchars()en elvalue. - La búsqueda es case-insensitive (encuentra "VPN" buscando "vpn").
- El enlace "Limpiar" solo aparece cuando hay una búsqueda activa.
-
php -l ~/gestor/public/listado.phpno da errores. - He guardado
capturas/paso1_busqueda_get.png.
Paso 2 — Crear nueva.php con un formulario POST¶
Conceptos
- Con
method="post", los datos del formulario viajan en el cuerpo de la petición HTTP, no en la URL. PHP los pone en$_POST. - POST se usa para acciones que modifican datos: crear, editar, borrar. La URL no cambia, así que el formulario no se puede guardar como favorito ni compartir.
- Antes de validar nada, conviene ver qué llega.
var_dump($_POST)imprime el contenido del array tal cual lo recibe PHP: nombres de campo, valores, tipos. Es una herramienta de diagnóstico, no algo que se quede en el código final. $_SERVER['REQUEST_METHOD']vale'GET'o'POST'. Lo usamos para distinguir si la página se está cargando vacía (GET) o si el usuario acaba de enviar el formulario (POST).exit;termina el script PHP ahí mismo: no se ejecuta nada del código que va después, ni se pinta nada de HTML adicional. Aquí lo usamos para parar tras imprimir elvar_dump; en el Paso 4 lo veremos en su segundo uso típico (justo después deheader('Location: ...')).
Etiquetas HTML nuevas del formulario
Las páginas anteriores eran solo de salida. Hoy aparecen por primera vez las etiquetas que el navegador usa para enviar datos:
| Etiqueta | Para qué sirve | Detalle clave |
|---|---|---|
<form action="..." method="..."> |
Contenedor del formulario. | action es la URL a la que se envían los datos; method es get o post. |
<label for="id">Texto</label> |
Etiqueta de un campo. | El for debe coincidir con el id del campo, así el clic en la etiqueta enfoca el campo. |
<input type="text" name="..." id="..."> |
Campo de texto de una línea. | El name es la clave que aparecerá en $_POST. El id es para el <label>. |
<select name="..."><option value="..."></option>...</select> |
Lista desplegable. | El valor enviado es el value del <option> elegido, no el texto visible. |
<textarea name="..." rows="..."></textarea> |
Campo de texto multilínea. | Su valor no va en value, va entre las etiquetas: <textarea>aquí va el valor</textarea>. |
<button type="submit">Texto</button> |
Botón de envío. | Al pulsarlo, el navegador envía el formulario padre. |
Objetivo. Crear public/nueva.php con un formulario POST de tres campos (título, prioridad y descripción) y comprobar qué llega a $_POST cuando el formulario se envía.
Tarea. Crea ~/gestor/public/nueva.php con este contenido:
Recarga http://gestor.local/nueva.php: debe aparecer el formulario con cabecera, los tres campos y el botón. Rellena el título con un texto cualquiera, elige una prioridad, escribe algo en la descripción y pulsa Crear incidencia.
Lo que verás no es el listado: aún no hemos puesto ninguna lógica detrás. Verás la página de diagnóstico con el contenido de $_POST impreso con var_dump(). Por ejemplo:
array(3) {
["titulo"]=>
string(11) "Prueba VPN"
["prioridad"]=>
string(4) "alta"
["descripcion"]=>
string(0) ""
}
Es exactamente lo que PHP recibe: tres entradas con los nombres que pusiste en los name="..." del HTML, y los valores que el usuario escribió o seleccionó.
Cuando termines de probar, borra el bloque de diagnóstico entero. En concreto, elimina las 7 líneas que van desde el comentario // Diagnóstico: ... hasta el } que cierra el if, ambas inclusive (de la línea 8 a la 14 del bloque que tecleaste). El require __DIR__ . '/../templates/_header.php'; de abajo se queda. Lo recuperaremos en el Paso 3 con validación de verdad: este bloque solo servía para que vieras qué llega en $_POST.
Responde en RESPUESTAS.md, sección ## Paso 2.
Envía el formulario dejando vacíos el textarea de descripción y sin seleccionar nada en el select de prioridad. Mira la salida de
var_dump($_POST):
- ¿Aparece la clave
"descripcion"en el array? Si aparece, ¿qué valor tiene?- ¿Y la clave
"prioridad"cuando dejas la opción-- Selecciona --? ¿Qué valor tiene?- Has visto que tras enviar el formulario,
$_POST['titulo'],$_POST['prioridad']y$_POST['descripcion']existen siempre, incluso vacíos. Ahora piensa en el escenario contrario: la primera vez que el usuario abrehttp://gestor.local/nueva.php(todavía no ha enviado nada, es una petición GET). ¿Crees que$_POST['titulo']existirá ya en ese momento, o no? ¿Qué pasaría si tu código intentara leerlo directamente sin precaución?
Captura requerida. Rellena los tres campos y envía el formulario. Cuando aparezca la página de diagnóstico, guarda una captura en capturas/paso2_var_dump_post.png. Debe verse:
- La URL
http://gestor.local/nueva.phpen la barra. - La salida de
var_dump($_POST)con las tres claves (titulo,prioridad,descripcion) y sus valores.
Verificación antes de pasar al Paso 3.
-
~/gestor/public/nueva.phpexiste y se ve correctamente con cabecera y pie. - El formulario tiene
method="post"y tres campos:titulo,prioridadydescripcion. - Al enviar el formulario aparece la salida de
var_dump($_POST). - He eliminado el bloque
if ($_SERVER['REQUEST_METHOD'] === 'POST') { ... exit; }tras la prueba. -
php -l ~/gestor/public/nueva.phpno da errores. - He guardado
capturas/paso2_var_dump_post.png.
Paso 3 — Validación en el servidor y errores junto a cada campo¶
Conceptos
- La validación del navegador (
required,maxlength) se puede saltar abriendo las DevTools y quitando el atributo, o enviando la petición concurl. La única validación que no se puede saltar es la del servidor. - El flujo estándar es recoger → sanear → validar → procesar o re-mostrar. Saneamos con
trim()para quitar espacios sobrantes. Si la validación falla, re-mostramos el formulario con los valores escritos y los mensajes de error junto a cada campo. - Acumulamos errores en un array
$erroresindexado por nombre de campo:$errores['titulo'] = 'El título es obligatorio.';. Después, en el HTML, comprobamos conisset($errores['titulo'])si hay que pintar el mensaje rojo bajo ese campo.isset()lo viste en A5 aplicado a variables simples (isset($titulo_pagina)); funciona igual con claves de array. in_array($valor, $lista, true)con eltrueactiva la comparación estricta (===). Sin él, PHP convierte tipos automáticamente yin_array('0', ['baja','media','alta'])daría sorpresas. Para validar que un valor está en una lista cerrada usamos siempretrue.
Sintaxis nueva: el operador ternario ? :
En el HTML del formulario vas a usar una forma corta del if/else llamada operador ternario:
Se lee: "si la condición isset(...) es cierta, el valor es 'rojo'; si no, es 'normal'". Equivale al if/else clásico que viste en A4, pero cabe en una línea. Lo usaremos sobre todo dentro de atributos HTML para pintar (o no) un trozo de estilo según haya error en un campo:
Mala práctica: imprimir $_POST['titulo'] directo en el value del input
value="<?= $_POST['titulo'] ?? '' ?>" parece más corto, pero el usuario puede haber tecleado caracteres especiales (incluso <script>) y los pegarías literalmente en el HTML. Pasa siempre la salida por htmlspecialchars().
Paso largo: hazlo en dos pasadas
Este es el paso con más código del lab: vas a reescribir dos bloques distintos de nueva.php (cabecera PHP en la Tarea 1, formulario HTML completo en la Tarea 2) y aparecen 3 conceptos nuevos a la vez ($val, $errores, operador ternario en atributos HTML). Hazlo despacio. Guarda y prueba con php -l después de la Tarea 1 antes de pasar a la Tarea 2 — así si rompes algo, sabes en cuál de las dos tareas fue.
Objetivo. Implementar validación completa en nueva.php que detecte campos vacíos, títulos demasiado largos y prioridades inválidas, mostrando los errores en rojo junto a cada campo y conservando lo que el usuario había escrito.
Tarea 1 — Reescribir el bloque PHP de cabecera de nueva.php. Sustituye la cabecera PHP (lo que hay antes del require de _header.php) por esto:
Comprobación intermedia tras la Tarea 1. Antes de tocar el HTML, comprueba que el PHP solo no rompe nada: ejecuta php -l ~/gestor/public/nueva.php (no debe dar errores) y recarga la página en el navegador. Como aún no has tocado el <form> del Paso 2, el formulario se ve igual que antes y al enviarlo con datos válidos verás el mensaje verde "Datos válidos. Título: ..." (sin estilo de errores, porque el HTML del Paso 2 no los pinta todavía). Eso es lo esperado: en la Tarea 2 le toca al HTML.
Tarea 2 — Reescribir el HTML del formulario. Sustituye el <form> que tenías por este, que conserva los valores y muestra los errores junto a cada campo:
Fíjate en dos detalles:
- En el
<textarea>, el valor va entre las etiquetas (no en un atributovalue):<textarea ...><?= htmlspecialchars(...) ?></textarea>. Es así como funcionan los textareas en HTML. - El
<select>mantiene la prioridad elegida poniendo el atributoselecteden el<option>cuya$opcoincide con$val['prioridad'].
Ahora prueba:
- Envía el formulario completamente vacío: deben aparecer dos errores rojos ("El título es obligatorio." y "Selecciona una prioridad válida.") junto a sus campos. El textarea sigue vacío, sin error.
- Escribe un título de más de 80 caracteres (puedes pegar
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaarepetido), elige prioridad y envía: aparece el error de longitud, pero la prioridad seleccionada se mantiene marcada. - Rellena el título, elige prioridad y deja la descripción vacía o llena: ahora sí entras al
if (empty($errores))y ves el mensaje verde "Datos válidos. Título: ...". Más adelante reemplazaremos eseechopor un guardado real, pero la validación ya funciona.
Pista si los errores aparecen pero el texto del input se borra
Es el síntoma de no haber puesto value="<?= htmlspecialchars($val['titulo']) ?>" en el <input>. PHP recoge el valor en $val, pero si el HTML no lo vuelca de nuevo en el campo, el navegador pinta el input vacío. Comprueba que cada campo del formulario consulta su entrada correspondiente de $val.
Responde en RESPUESTAS.md, sección ## Paso 3.
- ¿Por qué validamos en el servidor si el navegador ya ofrece
required,maxlengthy similares? Da un ejemplo concreto en el que la validación del navegador no sirva de nada.- ¿Por qué usamos
in_array($val['prioridad'], ['baja', 'media', 'alta'], true)con el tercer parámetrotrue? ¿Qué riesgo tendría omitirlo?
Captura requerida. Envía el formulario con el título relleno (por ejemplo, "Servidor lento") pero sin seleccionar prioridad (deja la opción "-- Selecciona --"). Guarda la captura en capturas/paso3_validacion_errores.png. Debe verse:
- El formulario re-pintado con el título "Servidor lento" todavía visible en el input.
- El campo de prioridad con
-- Selecciona --y el mensaje rojo "Selecciona una prioridad válida." debajo. - (Opcional) Ningún error junto al título, porque ese campo sí fue válido.
Verificación antes de pasar al Paso 4.
- La cabecera PHP de
nueva.phpdefine$valy$erroresantes de cualquier salida HTML. - Si el formulario se envía vacío, aparecen los dos errores correspondientes en rojo junto a sus campos.
- Si el título es válido pero la prioridad no, el título escrito se conserva en el input.
- Si el formulario es válido, aparece "Datos válidos. Título: ..." en verde.
- El bloque
var_dump($_POST)del Paso 2 ya no existe ennueva.php. -
php -l ~/gestor/public/nueva.phpno da errores. - He guardado
capturas/paso3_validacion_errores.png.
Paso 4 — Patrón PRG (Post-Redirect-Get)¶
Conceptos
- Cuando un POST se procesa con éxito y devuelve directamente el HTML del resultado, recargar la página hace que el navegador pregunte "¿Reenviar el formulario?". Si el usuario dice que sí, la acción se repite (en una página que crea incidencias, eso son duplicados silenciosos).
- PRG rompe el ciclo: tras procesar el POST con éxito, el servidor responde con una redirección HTTP 302 a otra URL. El navegador sigue la redirección con un GET, que sí es seguro de recargar.
header('Location: /listado.php')envía esa cabecera de redirección. Debe llamarse antes de cualquier salida: niecho, ni HTML, nirequirede la plantilla. Si ya salió algo, PHP avisa con "headers already sent" y la redirección falla.exit;justo después es obligatorio: sin él, PHP sigue ejecutando el script aunque el navegador ya esté redirigiendo, y eso puede ejecutar dos veces lógica que solo debía correr una.
Mala práctica: olvidar exit tras header('Location: ...')
El navegador redirige al ver la cabecera, pero el servidor sigue corriendo código y ejecuta cosas que ya no debían ejecutarse. La pareja header(...) + exit; se escribe siempre junta.
Objetivo. Cambiar el bloque "datos válidos" de nueva.php para que, en lugar de imprimir un mensaje verde, redirija al listado y comprobar con las DevTools la secuencia POST → 302 → GET.
Tarea 1 — Provoca el problema antes de resolverlo. Para ver con tus ojos el problema que PRG arregla, hazlo primero sin PRG:
- Visita
http://gestor.local/nueva.php, rellena el formulario con datos válidos (título "Prueba sin PRG", prioridad "alta") y pulsa Crear incidencia. Verás el mensaje verde "Datos válidos. Título: Prueba sin PRG" que dejaste en el Paso 3. - Pulsa F5 para recargar la página que está mostrando ese mensaje verde.
- El navegador te pregunta algo tipo "¿Quieres reenviar el formulario? Para reproducir esta página, el navegador tiene que reenviar los datos previamente enviados." (el texto exacto varía entre navegadores). Si dices que sí, el POST se repite: en una página real que guardara la incidencia, se crearía un duplicado sin que tú hubieras hecho nada raro.
Ese es el problema. Vuelve a la página nueva.php (no aceptes el reenvío) y a continuación lo arreglamos con PRG.
Tarea 2 — Implementa PRG. En ~/gestor/public/nueva.php, localiza el bloque que en el Paso 3 quedó así:
Sustitúyelo entero por:
Guarda y prueba el flujo completo desde las DevTools:
- Abre
http://gestor.local/nueva.php. - Pulsa F12 para abrir las DevTools y entra en la pestaña Network (o Red).
- Marca la casilla Preserve log / Conservar registro (en Firefox aparece como "Persistir registros"). Está en la barra de herramientas superior de la pestaña Network, junto al filtro de tipo. Importante: márcala antes de enviar el formulario, no después; si no, la lista se borra al cambiar de URL y solo verás la petición final.
- Rellena el formulario con datos válidos (título "Prueba PRG", prioridad "media") y envía.
- Observa la lista de peticiones. Deben aparecer dos seguidas:
POST /nueva.phpcon código 302.GET /listado.phpcon código 200.
- La URL del navegador termina en
/listado.php, no en/nueva.php. - Pulsa F5 para recargar. Ahora el navegador no pregunta nada: se limita a repetir el GET de
listado.php, sin reenvío de formulario. Compara con lo que pasaba en la Tarea 1: ese era el problema, esto es la solución.
Pista si no ves las dos peticiones en Network
Algunos navegadores ocultan por defecto las redirecciones. Asegúrate de:
- Tener marcada la casilla Preserve log / Conservar registro antes de enviar el formulario.
- Tener seleccionado el filtro All / Todo (no solo "XHR", "Fetch", "JS"...).
- Si aun así solo ves la petición final, mira la columna Status: si hay una fila marcada con 302 en gris, es la del POST.
Pista si recibes 'headers already sent'
Antes de seguir, asegúrate de que estás viendo de verdad ese aviso en el navegador. Si en lugar del mensaje "headers already sent" lo que ves es una página en blanco o un HTTP ERROR 500 sin texto, es que display_errors sigue apagado en php.ini y el mensaje de PHP no llega al navegador. Vuelve al Paso 5 del lab A1 — Depuración: ver los errores de PHP — y actívalo. Una vez activado, sirve para el resto de los labs.
Si ya ves el aviso, sigue aquí. PHP lo emite cuando header('Location: ...') se llama después de haber enviado algo al navegador. Comprueba que:
- El
header()está dentro del bloque PHP de cabecera denueva.php, antes delrequire __DIR__ . '/../templates/_header.php';. - El fichero
nueva.phpno tiene ningún espacio en blanco ni texto antes del primer<?php. Algunos editores meten un BOM invisible al inicio del fichero: en VS Code, mira la esquina inferior derecha de la ventana; si poneUTF-8 with BOM, pulsa ahí, elige "Save with Encoding" y seleccionaUTF-8(sin BOM).
Responde en RESPUESTAS.md, sección ## Paso 4.
- ¿Qué código HTTP devuelve el
POST /nueva.phpcuando los datos son válidos? ¿Y elGET /listado.phpsiguiente?- ¿Qué pasaría si quitaras el
exit;que va justo después deheader('Location: /listado.php');? Piensa en qué ejecuta el servidor después de enviar la cabecera de redirección.
Captura requerida. Repite el envío con datos válidos con las DevTools abiertas en Network y Preserve log activado, y guarda una captura en capturas/paso4_devtools_prg.png. Debe verse:
- La pestaña Network de las DevTools con las dos peticiones visibles:
POST /nueva.phpcon status 302, yGET /listado.phpcon status 200. - La URL del navegador en
http://gestor.local/listado.php.
Verificación antes de pasar al Paso 5.
- El bloque
if (empty($errores))denueva.phpredirige conheader('Location: /listado.php')yexit;. - Tras enviar un formulario válido, la URL termina en
/listado.php. - Al recargar
listado.phpcon F5, el navegador no pregunta por reenvío. - En la pestaña Network aparece el POST 302 + GET 200.
-
php -l ~/gestor/public/nueva.phpno da errores. - He guardado
capturas/paso4_devtools_prg.png.
Paso 5 — Flash messages: confirmar la acción al usuario¶
Conceptos
- Tras el PRG, el usuario llega al
listado.phpsin saber si la incidencia se creó. Necesitamos un mensaje breve que aparezca una vez y se vaya: lo que se llama un flash message. - Lo guardamos en
$_SESSIONjusto antes de redirigir, y lo leemos y borramos en la página destino. Si no lo borramos, se queda pegado a todas las páginas hasta que la sesión expire. $_SESSIONes una superglobal que persiste entre peticiones del mismo usuario. Para activarla hay que llamar asession_start()antes de cualquier salida: antes deecho, antes de HTML suelto, antes del primerrequirede plantilla.- Regla operativa: cualquier página que vaya a leer o escribir en
$_SESSIONantes delrequirede_header.phptiene que llamar ella misma asession_start()arriba del todo. Eso es lo que va a pasar ennueva.php(escribirá el flash antes del redirect) y enlistado.php(leerá el flash antes delrequire).
Cookies de sesión y por qué session_start() va arriba
Una cookie es un dato pequeño (clave=valor) que el servidor envía al navegador en una cabecera HTTP de la respuesta, y que el navegador devuelve automáticamente en cada petición posterior al mismo sitio.
La primera vez que llamas a session_start(), PHP:
- Genera un identificador único de sesión (una cadena hexadecimal aleatoria).
- Lo guarda en disco asociado a los datos de
$_SESSION. - Envía ese identificador al navegador como cookie llamada
PHPSESSID.
Esa cookie va en una cabecera HTTP, igual que el header('Location: ...') del Paso 4. Y las cabeceras se envían antes del HTML. Por eso session_start() tiene que ir arriba del todo: si ya salió HTML, las cabeceras están cerradas y PHP avisa con "Cannot send session cookie - headers already sent". Es la misma regla del Paso 4 aplicada a sesiones.
session_status() y la guarda de doble inicialización
Si session_start() se llama dos veces en la misma petición, PHP emite un aviso. En la Tarea 1 vas a meter session_start() en _header.php con una guarda:
session_status() devuelve uno de tres valores: PHP_SESSION_NONE (sesión no iniciada), PHP_SESSION_ACTIVE (sesión iniciada) o PHP_SESSION_DISABLED (sesiones desactivadas en el servidor). Las tres son constantes de PHP (van en mayúsculas y sin $, como E_ALL del error_reporting que vienes usando desde A1).
La guarda dice: "inicia sesión solo si no estaba iniciada ya". Es útil porque en este lab nueva.php y listado.php van a llamar a session_start() antes del require (Tarea 2 y 3), y sin la guarda, el require del _header.php lo llamaría una segunda vez y daría aviso.
Objetivo. Que tras crear una incidencia con éxito aparezca un mensaje verde de confirmación en listado.php que diga "Incidencia creada correctamente.", y que ese mensaje desaparezca al recargar la página.
Tarea 1 — Añadir session_start() al inicio de _header.php. Abre ~/gestor/templates/_header.php. Justo después del <?php inicial (la primera línea), añade el arranque de sesión con la guardia:
Así, cualquier página que haga require de _header.php tiene sesión disponible. Si una página la había arrancado antes (como van a hacer nueva.php y listado.php en las Tareas 2 y 3), session_status() evita la llamada doble.
Comprobación intermedia tras la Tarea 1. Recarga http://gestor.local/listado.php en el navegador: la página tiene que seguir funcionando exactamente igual que al final del Paso 4. Si ya no se ve la cabecera o sale un error, has roto _header.php y no lo verás en pasos posteriores. Corrige antes de seguir.
Tarea 2 — Guardar el flash en nueva.php antes de redirigir. En ~/gestor/public/nueva.php:
- Justo después del
<?phpinicial, antes de loserror_reporting, añadesession_start();. Ennueva.phpla sesión hay que iniciarla aquí (no esperar al_header.php) porque el flash se guarda antes delheader('Location: ...'), y eseheaderestá antes delrequirede la cabecera. - Localiza el bloque
if (empty($errores)) { header('Location: /listado.php'); exit; }del Paso 4 y sustitúyelo entero por:
La cabecera PHP de nueva.php debe empezar ahora así:
Por qué aquí concateno $val['titulo'] sin htmlspecialchars()
En el Paso 3 te insistí en pasar todo por htmlspecialchars(). Aquí estamos guardando el título en $_SESSION para usarlo luego, no estamos pintándolo todavía. En la Tarea 3 verás que el flash se pinta con <?= htmlspecialchars($flash['msg']) ?>: ahí es donde se escapa. La regla es se escapa al pintar, no al guardar: si lo escapas aquí y otra vez al pintar, los caracteres especiales se mostrarían dos veces escapados (&quot; en lugar de ").
Comprobación intermedia tras la Tarea 2. Sin haber tocado aún listado.php, crea una incidencia válida desde nueva.php. El redirect a listado.php debe seguir funcionando (URL acaba en /listado.php, no aparece error). Todavía no verás el flash porque listado.php aún no lo lee. Si en cambio recibes un error tipo "Cannot send session cookie - headers already sent", has metido el session_start() en el sitio equivocado: tiene que ser la primera línea ejecutable, justo después de <?php.
Tarea 3 — Leer y pintar el flash en listado.php. En ~/gestor/public/listado.php:
- Justo después del
<?phpinicial, antes deerror_reporting, añadesession_start();. - Justo después de la línea
$pagina_activa = 'listado';, añade la lectura y borrado del flash:
- En el HTML, justo después del
requirede_header.phpy antes del formulario de búsqueda, añade el bloque que pinta el flash si existe:
Guarda y prueba el ciclo completo:
- Visita
http://gestor.local/nueva.php. - Crea una incidencia válida (título "Prueba flash", prioridad "alta"). Envía.
- El navegador acaba en
http://gestor.local/listado.phpcon un recuadro verde que dice Incidencia "Prueba flash" creada correctamente. arriba. - Recarga la página con F5. El recuadro verde desaparece: solo se muestra una vez.
Pista si el flash no aparece
Errores típicos, por orden de probabilidad:
session_start()no es la primera instrucción denueva.phpolistado.php: comprueba que va justo después de<?php, sin nada delante.- Has llamado a
$_SESSION['flash'] = ...después delheader('Location: ...'): elheader()yexitse ejecutan antes de que llegue a guardar. El orden correcto es flash → header → exit. - Has guardado el flash pero no llamas a
session_start()enlistado.php: PHP no carga$_SESSIONy la variable está vacía.
Responde en RESPUESTAS.md, sección ## Paso 5.
- ¿Por qué
unset($_SESSION['flash'])enlistado.phpes importante? ¿Qué pasaría si lo quitaras y luego visitaras directamentelistado.phpdesde el menú varias veces seguidas?- ¿Por qué
session_start()debe ir antes de cualquier salida HTML? Pista: piensa en qué tiene que enviar al navegador la primera vez que se inicia una sesión.
Captura requerida. Crea una incidencia válida desde nueva.php y, cuando el navegador acabe en listado.php, guarda una captura en capturas/paso5_flash_exito.png. Debe verse:
- La URL
http://gestor.local/listado.phpen la barra. - El recuadro verde con el mensaje Incidencia "..." creada correctamente. en la parte superior.
- El formulario de búsqueda y la tabla del listado justo debajo.
Verificación antes de pasar al Paso 6.
-
_header.phpllama asession_start()con la guardiasession_status() === PHP_SESSION_NONE. -
nueva.phpempieza consession_start();justo después de<?php. -
nueva.phpguarda$_SESSION['flash']antes delheader('Location:...'). -
listado.phpempieza consession_start();y lee$_SESSION['flash']conunset()después. - El flash aparece tras crear una incidencia y desaparece al recargar.
- Los tres ficheros (
_header.php,nueva.php,listado.php) pasanphp -lsin errores. - He guardado
capturas/paso5_flash_exito.png.
Paso 6 — Aplica lo aprendido: filtro por estado en listado.php¶
Qué es este paso
Los pasos anteriores han sido guiados: te he dado el código y tú lo has tecleado, probado y entendido. En este sexto paso no hay código de referencia completo. Lo que hay son requisitos, y tú aplicas lo aprendido sobre $_GET, <select> con selected y filtrado de arrays para resolver un problema nuevo.
El reto: añadir un selector de estado al formulario de búsqueda de listado.php que filtre las incidencias por estado (todos / abierta / resuelta), combinándose con la búsqueda por título que ya tienes.
Esto es una modificación reversible de listado.php (regla de los labs: el paso "aplica" produce material extra y desechable). Si decides borrar el selector y dejarlo solo con la búsqueda del Paso 1, la página sigue funcionando: el filtro de estado es una capa adicional encima del sistema que ya tienes.
Objetivo. Ampliar el formulario de búsqueda de listado.php con un <select> de estado, de forma que se pueda filtrar por título, por estado, o por ambos a la vez.
Tarea. Modifica ~/gestor/public/listado.php cumpliendo los requisitos siguientes.
Requisitos obligatorios de contenido:
- En la cabecera PHP de
listado.php(donde lees$_GET['busqueda']), lee también un segundo parámetro$_GET['estado']con valor por defecto'todos'. Aplicatrim()igual que con la búsqueda. - En la lógica de filtrado, además del filtro por título que ya tienes, aplica un segundo filtro: si
$estado !== 'todos', deja solo las incidencias cuyo campoestadosea igual a$estado. Si vale'todos', no se aplica filtro de estado. Los dos filtros se aplican combinadamente: si el usuario busca "VPN" y selecciona "abierta", solo aparecen las incidencias que cumplan ambas cosas. - En el HTML del formulario, dentro del mismo
<form method="get">(junto al input de búsqueda y el botón "Buscar"), añade un<select name="estado">con tres opciones:todos,abierta,resuelta. La opción seleccionada por defecto debe ser la que coincida con el valor de$estado(igual que en el Paso 3 hicimos con la prioridad ennueva.php).
Requisitos de presentación HTML:
- Al cargar
http://gestor.local/listado.phpsin ningún parámetro, el<select>muestra "Todos" seleccionado y se ven las 5 incidencias. - Al seleccionar "Abierta" y pulsar Buscar, la URL pasa a
listado.php?busqueda=&estado=abiertay solo aparecen las 3 incidencias con estado abierta. Fíjate en quebusqueda=aparece vacío en la URL aunque el input estuviera vacío: por defecto, un<form method="get">envía todos los campos del formulario, incluso los que dejaste sin rellenar. - Al combinar: buscar "VPN" + seleccionar "Abierta", la URL es
?busqueda=VPN&estado=abiertay solo aparece "VPN no conecta". - Al pulsar Limpiar, vuelve a
listado.phppelado y se ven las 5.
Requisitos de calidad del código:
- Sigue el patrón de
trim($_GET['estado'] ?? 'todos')para leer el parámetro. - Usa el mismo patrón de filtrado que ya tienes en el Paso 1 (
foreachsobre$todaso sobre el resultado del filtro de título, con unifque decida si una incidencia entra o no en$incidencias). No hay sintaxis nueva. - Valida los valores admitidos: si alguien escribe
?estado=imposibledirectamente en la URL, no debe romper la página (basta con que el filtro no encuentre nada, no que aparezca un error). La forma natural de conseguirlo es no aplicar el filtro si$estadono está en la lista['abierta', 'resuelta']o vale'todos'.
Pista sobre el select con selected
Es el mismo patrón que viste con la prioridad en el Paso 3, pero ahora aplicado al estado:
Pista sobre el doble filtrado
Una idea de estructura: empieza con $incidencias = $todas; y aplícale dos filtros en cascada, cada uno con un foreach que reconstruye la lista. El primero (búsqueda por título) es básicamente el del Paso 1, pero filtrando sobre $incidencias en lugar de sobre $todas. El segundo (filtro de estado) tiene la misma forma, pero con la condición $inc['estado'] === $estado. Antes de aplicar el segundo filtro, comprueba con in_array($estado, ['abierta', 'resuelta'], true) que el valor es uno de los esperados: si vale 'todos' o cualquier cosa inventada, no apliques el filtro de estado y déjalo pasar.
Tienes en el Paso 1 el patrón exacto de cómo construir un foreach que filtra: clónalo y cámbiale la condición.
Responde en RESPUESTAS.md, sección ## Paso 6.
- Al hacer una búsqueda combinada (por ejemplo, "VPN" + estado "abierta"), ¿qué aparece en la URL del navegador? Copia la URL completa.
- ¿Qué ventaja tiene comprobar
in_array($estado, ['abierta', 'resuelta'], true)antes de aplicar el filtro de estado, en lugar de aplicarlo siempre? Piensa en qué pasa si alguien teclea?estado=cualquiercosadirectamente.
Captura requerida. Visita http://gestor.local/listado.php?busqueda=&estado=abierta (o usa el selector para llegar a esa URL) y guarda una captura en capturas/paso6_filtro_estado.png. Debe verse:
- La URL con
estado=abiertaen la barra de direcciones. - El
<select>mostrando "Abierta" seleccionado. - La tabla con las 3 incidencias de estado abierta (Servidor web caído, VPN no conecta, Backup no se ejecuta).
Verificación.
-
listado.phplee$_GET['estado']contrim()y valor por defecto'todos'. - El formulario tiene un
<select name="estado">con tres opciones. - La opción del selector que aparece marcada coincide con el valor de
$estado. - Búsqueda y filtro por estado se combinan: buscar "VPN" + "abierta" deja una sola fila.
-
?estado=cualquiercosano rompe la página. -
php -l ~/gestor/public/listado.phpno da errores. - He guardado
capturas/paso6_filtro_estado.png.
Si tu Paso 6 deja listado.php con error de sintaxis
Antes de hacer el ZIP, deshaz tus cambios del Paso 6 en listado.php y vuelve a la versión que tenías al final del Paso 5. Es preferible entregar el Paso 6 sin hacer (perdiendo solo los puntos de ese paso) que entregar un listado.php roto que rompa también lo del Paso 1 y el Paso 5. Comprueba con php -l ~/gestor/public/listado.php antes de empaquetar.
Checklist final de entrega¶
Antes de subir el ZIP al Campus, revisa uno a uno:
- He creado la carpeta
apellido_nombre_lab06/(con mi apellido y nombre reales). - Dentro hay
gestor/public/listado.phpcon la versión final del Paso 6. - Dentro hay
gestor/public/nueva.phpcon la versión final del Paso 5. - Dentro hay
gestor/templates/_header.phpconsession_start()añadido. - Dentro hay
capturas/con los 6 PNG:paso1_busqueda_get.png,paso2_var_dump_post.png,paso3_validacion_errores.png,paso4_devtools_prg.png,paso5_flash_exito.png,paso6_filtro_estado.png. - Dentro hay
RESPUESTAS.mdcon las 6 secciones rellenas. - Los tres ficheros PHP pasan
php -lsin errores:php -l ~/gestor/public/listado.phpphp -l ~/gestor/public/nueva.phpphp -l ~/gestor/templates/_header.php
- El ZIP se llama
apellido_nombre_lab06.zipy al descomprimirlo solo aparece una carpeta raíz con ese nombre. - He subido el ZIP a la tarea correspondiente en el Campus antes de la fecha límite.
Anexo — Plantilla de RESPUESTAS.md¶
Crea un fichero llamado RESPUESTAS.md dentro de apellido_nombre_lab06/ con esta plantilla y rellena cada sección:
# Respuestas — Lab 06 Formularios, validación, PRG y flash
**Nombre:** Apellido, Nombre
**Fecha:** YYYY-MM-DD
## Paso 1
1. (URL completa que aparece al buscar "VPN".)
2. (Por qué es importante `value="<?= htmlspecialchars($busqueda) ?>"` y qué pasaría sin él.)
## Paso 2
1. (¿Aparece la clave `descripcion` en `$_POST` cuando dejas el textarea vacío? ¿Con qué valor?)
2. (¿Qué valor tiene `prioridad` cuando dejas la opción `-- Selecciona --`?)
3. (¿Qué pasaría si tu PHP usara `$_POST['titulo']` directamente al cargar la página por primera vez?)
## Paso 3
1. (Por qué validar en el servidor si el navegador ya valida con `required` y similares. Ejemplo de caso en que la validación del navegador no sirve.)
2. (Por qué usamos `in_array(..., ..., true)` con `true` como tercer parámetro y qué riesgo tiene omitirlo.)
## Paso 4
1. (Código HTTP del POST y código HTTP del GET.)
2. (Qué pasaría si quitaras el `exit;` después de `header('Location: ...');`.)
## Paso 5
1. (Por qué `unset($_SESSION['flash'])` es importante y qué pasaría sin él al recargar varias veces.)
2. (Por qué `session_start()` debe ir antes de cualquier salida HTML.)
## Paso 6
1. (URL completa con búsqueda "VPN" y estado "abierta" combinados.)
2. (Qué ventaja tiene comprobar `in_array($estado, ['abierta', 'resuelta'], true)` antes de aplicar el filtro.)