Saltar a contenido

Lab 05 — Plantillas con require: cabecera y pie reutilizables

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) e (modularidad: require y plantillas)
Requisitos Labs 01, 02, 03 y 04 completados
Agrupación Individual
Herramientas Navegador + editor de texto + terminal
Apuntes https://ichigar.codeberg.page/imw/recursos/ut4_php_05_funciones_y_plantillas/ (lee solo la sección Plantillas con require; las funciones llegarán en el Lab 07)
Tiempo estimado 90–120 minutos

Antes de empezar

Entorno de trabajo

Este lab continúa el proyecto ~/gestor/ que vienes ampliando desde el Lab 01. La estructura actual es:

gestor/
├── public/        <-- accesible desde el navegador
│   ├── 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)
├── src/
├── templates/     <-- vacío hasta hoy
└── data/

Si http://gestor.local no responde, revisa el Lab 01 Paso 3 antes de continuar.

Qué vamos a hacer

En los labs anteriores has creado varias páginas PHP (listado.php, estado.php, catalogo.php, etc.) y todas repiten el mismo HTML de estructura: <!DOCTYPE>, <head>, <title>, <header>, <footer>... Hoy vamos a extraer ese HTML repetido a dos plantillas (_header.php y _footer.php) y a aprender a incluirlas en una página con require.

Al final del lab, public/listado.php tendrá solo su contenido único (el <table> de incidencias), y el HTML que la envuelve vendrá de las plantillas. Y en el paso "aplica" añadirás un detalle final: que el menú destaque el enlace de la página activa.

Las demás páginas del proyecto (estado.php, catalogo.php, etc.) se quedan como están. A partir del Lab 06, las páginas nuevas se construirán ya con plantillas; las viejas las migraremos en el lab de integración final (A11) si toca.

Servidor vs cliente

require se ejecuta en el servidor, antes de que Apache envíe nada al navegador. PHP "pega" el contenido de la plantilla dentro de la página y manda al navegador el HTML resultante ya unido. El navegador nunca ve los require: solo recibe un <!DOCTYPE html>...</html> completo. Esto es importante: si abres "Ver código fuente" en el navegador, no encontrarás ningún <?php require ... ?> por ningún lado.


Cómo se entrega este lab

Al terminar, empaqueta tu trabajo en un ZIP con esta estructura exacta:

apellido_nombre_lab05/
├── gestor/
│   ├── public/
│   │   ├── css/
│   │   │   └── estilos.css
│   │   └── listado.php
│   └── templates/
│       ├── _header.php
│       └── _footer.php
├── capturas/
│   ├── paso1_html_duplicado.png
│   ├── paso2_estructura_templates.png
│   ├── paso3_listado_con_plantillas.png
│   ├── paso4_listado_con_css_externo.png
│   └── paso5_menu_activo.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: el listado.php refactorizado, el estilos.css nuevo y las dos plantillas. El resto de tu gestor/ (index.php, estado.php, arrays.php, etc.) se queda en tu servidor y no se entrega. Esa es la regla a partir de ahora: cada lab entrega solo lo suyo.

Cómo preparar la entrega (independientemente del sistema operativo):

  1. Crea una carpeta en tu equipo llamada apellido_nombre_lab05 (sustituye por tu apellido y nombre reales, en minúsculas y sin espacios ni acentos).
  2. Dentro, reproduce la estructura de arriba:
    • gestor/public/css/estilos.css con el CSS externo (Paso 4).
    • gestor/public/listado.php con la versión refactorizada que usa plantillas.
    • gestor/templates/_header.php y gestor/templates/_footer.php con las plantillas.
    • Una subcarpeta capturas/ con los 5 PNG, ya renombrados exactamente como se indica en cada paso.
    • Un fichero RESPUESTAS.md en la raíz de la carpeta, con una sección ## Paso N por cada pregunta del lab (la plantilla está en el anexo al final de este documento).
  3. Comprime esa carpeta completa en un archivo .zip:
    • Windows: clic derecho sobre la carpeta → Enviar aCarpeta comprimida (en zip). El archivo resultante debe llamarse apellido_nombre_lab05.zip.
    • Linux/macOS: desde la terminal, zip -r apellido_nombre_lab05.zip apellido_nombre_lab05/.
  4. Abre el ZIP para comprobar que al descomprimirlo aparece una sola carpeta raíz llamada apellido_nombre_lab05/, y que dentro están todos los ficheros esperados.

Comprueba la estructura del ZIP antes de subirlo

Un error habitual en Windows es seleccionar los ficheros de dentro de la carpeta en lugar de la carpeta completa, y comprimirlos. Eso genera un ZIP que al abrirse no tiene carpeta raíz, y el script de corrección lo rechaza. Comprime la carpeta apellido_nombre_lab05/, no su contenido.


Paso 1 — Diagnóstico: el HTML que se repite en cada página

Conceptos

  • Sin plantillas, cada .php de tu public/ repite el mismo <head>, <header> y <footer>. Si quisiéramos cambiar el título del sitio o el menú, tendríamos que editar todos los ficheros.
  • DRY (Don't Repeat Yourself): si escribes lo mismo dos veces, extráelo y reutilízalo. Vale para HTML, igual que para cualquier código.
  • require "fichero.php" incluye el contenido de ese fichero como si lo hubieras escrito en el sitio donde aparece el require. La diferencia con include es que require lanza un error fatal si el fichero no existe (la página se rompe), mientras que include solo muestra un aviso y sigue. Para plantillas obligatorias usa siempre require: si la plantilla falta, queremos enterarnos en seguida, no pintar media página.

Pequeño aviso de coherencia: si esta comparación de hoy te da pocas líneas repetidas (porque tu listado.php de A3 y tu estado.php de A4 son cortos), no pasa nada. El argumento DRY se mantiene incluso con 8-10 líneas duplicadas: imagínate el día que tengamos 10 páginas.

Objetivo. Identificar el bloque de HTML que se repite en las páginas que ya tienes, para entender por qué require es la solución natural.

Tarea. Abre en tu editor (no hace falta tocar nada todavía) dos ficheros de los labs anteriores: ~/gestor/public/listado.php (del Lab 03) y ~/gestor/public/estado.php (del Lab 04). Compara las dos partes:

  • Desde <!DOCTYPE html> hasta el primer <h1> o <h2> → ¿qué hay igual en ambos ficheros? ¿qué cambia?
  • Desde el final del contenido propio (la tabla, la lista de incidencias) hasta </html> → ¿es exactamente el mismo cierre?

Esto que estás detectando "a ojo" es lo que vamos a extraer a plantillas. La meta no escrita: si mañana queremos añadir un <nav> con enlaces a las páginas del proyecto, deberíamos poder hacerlo en un solo sitio.

Responde en RESPUESTAS.md, sección ## Paso 1.

Cuenta cuántas líneas de HTML "de estructura" (desde <!DOCTYPE html> hasta el cierre </html>, excluyendo el contenido propio de cada página) se repiten entre listado.php y estado.php. No tiene que ser exacto a la línea: una aproximación razonable basta. ¿Qué pasaría si tuvieras 10 páginas y quisieras cambiar el texto del <title> global del sitio?

Captura requerida. Abre los dos ficheros en tu editor con vista de pantalla dividida (o uno al lado del otro) y guarda una captura en capturas/paso1_html_duplicado.png. Debe verse:

  • Los dos ficheros (listado.php y estado.php) abiertos en paralelo.
  • Las primeras líneas (<!DOCTYPE html>, <head>, <title>) visibles en ambos para que se aprecie la duplicación.
Pista si usas VS Code

Abre los dos ficheros. En el segundo, haz clic derecho en la pestaña → Split Right (o Dividir a la derecha). Te quedan los dos en paralelo.

Verificación antes de pasar al Paso 2.

  • He localizado el bloque de HTML repetido en listado.php y estado.php.
  • He guardado capturas/paso1_html_duplicado.png.
  • Entiendo por qué require (y no include) es lo correcto para incluir una plantilla obligatoria.

Paso 2 — Crear _header.php y _footer.php

Conceptos

  • Las plantillas van en templates/ (fuera de public/), porque no deben ser accesibles directamente desde el navegador. Si alguien escribiera http://gestor.local/_header.php, vería un HTML roto (solo la cabecera, sin contenido ni cierre). Y en labs futuros, ahí guardaremos plantillas con datos sensibles que tampoco queremos servir sueltas.
  • El prefijo _ en _header.php es una convención para indicar "esto es un fragmento, no una página completa".
  • Una plantilla puede usar variables definidas antes del require en la página que la incluye. Anticipo del patrón completo que verás aplicado en el Paso 3:

    <?php
    $titulo_pagina = 'Listado de incidencias';   // 1. defines la variable
    require __DIR__ . '/../templates/_header.php';  // 2. incluyes la plantilla
    ?>
    ...contenido único de la página...
    

    Dentro de _header.php, $titulo_pagina está disponible "como si la hubieras escrito allí". Esto es lo que permite que una sola plantilla sirva para muchas páginas, cada una con su título.

  • Si una página olvida definir $titulo_pagina, _header.php la usaría sin valor y daría aviso. Por eso la plantilla pone un valor por defecto con isset() al inicio. isset($var) es una función de PHP que devuelve true si la variable existe (y no es null), y false si no. Es el equivalente "largo" del operador ?? que viste en A4: $x = $x ?? 'default'; y if (!isset($x)) { $x = 'default'; } hacen lo mismo.

Mala práctica: poner _header.php dentro de public/

Si ponen las plantillas en public/templates/ o directamente en public/, Apache las sirve por URL como cualquier otra página: cualquiera podría abrir http://gestor.local/_header.php y vería un HTML incompleto. Peor: en el Lab 09 las plantillas mostrarán nombres de usuario, y servirlas sueltas filtraría información. Los ficheros que no son páginas finales se guardan fuera de public/. Esta regla vale para todo el proyecto y para todas las páginas que escriban a partir de hoy.

Objetivo. Crear las dos plantillas (_header.php y _footer.php) en ~/gestor/templates/, con el HTML que ahora está duplicado en las páginas.

Tarea 1 — Crear _header.php. Crea el fichero ~/gestor/templates/_header.php con este contenido:

<?php
// $titulo_pagina debe estar definida antes del require.
// Si no lo está, usamos un valor por defecto para evitar el aviso.
if (!isset($titulo_pagina)) {
    $titulo_pagina = 'Gestor de incidencias';
}
?>
<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title><?= htmlspecialchars($titulo_pagina) ?> — Gestor</title>
  <style>
    body { font-family: Arial, sans-serif; margin: 0; background: #f5f7fa; }
    header { background: #003865; color: white; padding: 1rem 2rem;
             display: flex; justify-content: space-between; align-items: center; }
    header h1 { margin: 0; font-size: 1.2rem; }
    nav a { color: #90caf9; text-decoration: none; margin-left: 1rem; }
    nav a:hover { color: white; }
    main { max-width: 900px; margin: 2rem auto; padding: 0 1rem; }
  </style>
</head>
<body>
  <header>
    <h1>Gestor de incidencias</h1>
    <nav>
      <a href="/listado.php">Listado</a>
    </nav>
  </header>
  <main>
    <h2><?= htmlspecialchars($titulo_pagina) ?></h2>

Fíjate en que _header.php abre <main> pero no lo cierra: el cierre lo pondrá _footer.php. Entre ambos va el contenido único de cada página.

Tarea 2 — Crear _footer.php. Crea el fichero ~/gestor/templates/_footer.php con este contenido:

1
2
3
4
5
6
7
  </main>
  <footer style="text-align:center; padding:1rem; color:#666;
                 border-top:1px solid #ddd; margin-top:2rem; font-size:0.85rem;">
    Gestor de incidencias — ASIR IMW — <?= date('Y') ?>
  </footer>
</body>
</html>

_footer.php cierra </main>, pinta el pie y cierra </body> y </html>. El año del pie se calcula en el servidor con la función date('Y'), así que cuando cambie el año cambia solo.

Responde en RESPUESTAS.md, sección ## Paso 2.

Fíjate en que _header.php abre <main> pero no lo cierra, y _footer.php cierra </main> pero no lo abre. ¿Por qué está hecho así? Pista: piensa en qué pasaría si cada plantilla cerrara sus propias etiquetas — ¿dónde meterías entonces el contenido específico de cada página (la tabla del listado.php, el bloque de estado.php...)?

Captura requerida. En tu terminal, ejecuta ls -la ~/gestor/templates/ y guarda una captura en capturas/paso2_estructura_templates.png. Debe verse:

  • El listado del directorio ~/gestor/templates/.
  • Los dos ficheros _header.php y _footer.php con sus tamaños.

Verificación antes de pasar al Paso 3.

  • ~/gestor/templates/_header.php existe y contiene el HTML hasta <h2>...</h2>.
  • ~/gestor/templates/_footer.php existe y cierra </main>, <footer>, </body> y </html>.
  • Los dos ficheros están en templates/, NO en public/.
  • php -l ~/gestor/templates/_header.php y php -l ~/gestor/templates/_footer.php no dan errores.
  • He guardado capturas/paso2_estructura_templates.png.

Paso 3 — Refactorizar listado.php con require y __DIR__

Conceptos

  • Refactorizar = mejorar la estructura interna de un código sin cambiar lo que se ve por fuera. El navegador debe mostrar lo mismo antes y después de la refactorización; lo que cambia es cómo está organizado el código en el servidor.
  • __DIR__ es una constante de PHP que vale "la ruta absoluta del directorio donde está el fichero actual". Si listado.php vive en /home/alumno/gestor/public/, dentro de listado.php la constante __DIR__ vale literalmente /home/alumno/gestor/public.
  • El punto . concatena cadenas en PHP. Por eso __DIR__ . "/../templates/_header.php" significa "el directorio actual, sube un nivel, entra en templates, coge _header.php".
  • El patrón completo de cada página queda así: define las variables ($titulo_pagina, datos a mostrar) → require cabecera → contenido único → require pie.

¿Por qué __DIR__ y no una ruta sin más?

Tienes tres formas de escribir la ruta a la plantilla. Compara qué pasa con cada una:

Forma Ejemplo Problema
Ruta absoluta require "/home/alumno/gestor/templates/_header.php"; Solo funciona en tu equipo. Si entregas el ZIP y el profesor lo descomprime en otra ruta, no funciona.
Ruta relativa require "../templates/_header.php"; Funciona si el directorio de trabajo es public/, pero Apache no siempre garantiza eso. Si la página se incluyera desde otro contexto, la ruta se rompe.
Con __DIR__ require __DIR__ . "/../templates/_header.php"; Siempre funciona, porque parte del directorio del fichero actual, que PHP conoce con seguridad.

Usa siempre la tercera. Es lo que hacen todos los frameworks PHP del mundo.

Cómo se "lee" esa línea, paso a paso. Si listado.php vive en /home/alumno/gestor/public/, entonces dentro de ese fichero __DIR__ vale literalmente /home/alumno/gestor/public. La expresión __DIR__ . "/../templates/_header.php" se convierte en la cadena /home/alumno/gestor/public/../templates/_header.php, que el sistema operativo resuelve a /home/alumno/gestor/templates/_header.php. Es decir: sube un nivel desde public/, entra en templates/, abre _header.php.

Objetivo. Reescribir ~/gestor/public/listado.php para que use las dos plantillas, eliminando todo el HTML duplicado.

Tarea. Reescribe ~/gestor/public/listado.php completo (sustituye lo que tenías por esto):

<?php
error_reporting(E_ALL);
ini_set('display_errors', '1');

// 1. DATOS: toda la lógica PHP va aquí arriba.
$titulo_pagina = 'Listado de incidencias';

$incidencias = [
    ['id' => 1, 'titulo' => 'Servidor web caído',   'prioridad' => 'alta',  'estado' => 'abierta'],
    ['id' => 2, 'titulo' => 'Impresora sin tóner',  'prioridad' => 'baja',  'estado' => 'resuelta'],
    ['id' => 3, 'titulo' => 'VPN no conecta',       'prioridad' => 'media', 'estado' => 'abierta'],
];

// 2. CABECERA: incluir la plantilla (usa $titulo_pagina).
require __DIR__ . '/../templates/_header.php';
?>

<!-- 3. CONTENIDO ÚNICO DE ESTA PÁGINA -->
<p><?= count($incidencias) ?> incidencias registradas.</p>

<table style="width:100%; border-collapse:collapse;">
  <tr style="background:#003865; color:white;">
    <th style="padding:8px">ID</th>
    <th style="padding:8px">Título</th>
    <th style="padding:8px">Prioridad</th>
    <th style="padding:8px">Estado</th>
  </tr>
  <?php foreach ($incidencias as $inc): ?>
    <tr style="border-bottom:1px solid #ddd;">
      <td style="padding:8px"><?= $inc['id'] ?></td>
      <td style="padding:8px"><?= htmlspecialchars($inc['titulo']) ?></td>
      <td style="padding:8px"><?= htmlspecialchars($inc['prioridad']) ?></td>
      <td style="padding:8px"><?= htmlspecialchars($inc['estado']) ?></td>
    </tr>
  <?php endforeach ?>
</table>

<?php
// 4. PIE: incluir la plantilla.
require __DIR__ . '/../templates/_footer.php';
?>

Accede a http://gestor.local/listado.php y comprueba que la página muestra:

  • Cabecera azul con "Gestor de incidencias" y el enlace "Listado" en el menú.
  • Título Listado de incidencias debajo de la cabecera.
  • La tabla de 3 incidencias (todavía sin filas alternas — eso vendrá con el CSS externo del Paso 4).
  • Pie centrado con "Gestor de incidencias — ASIR IMW — 2026" (o el año actual).

Y muy importante: el <title> de la pestaña del navegador debe decir "Listado de incidencias — Gestor".

Pista si ves los require literales en la página

Si en el navegador aparece texto como <?php require __DIR__ ... en lugar de la página renderizada, Apache no está interpretando PHP en listado.php. Eso suele significar que el módulo PHP está deshabilitado: sudo a2enmod php8.3 && sudo systemctl reload apache2. Si ves una página en blanco o un error 500, mira el log con sudo tail /var/log/apache2/error.log: ahí aparecerá si la ruta del require está mal.

Si la pantalla en blanco / HTTP 500 son habituales en este lab y te toca abrir el log cada vez, comprueba que activaste display_errors = On en el Paso 5 del lab A1 — Depuración: ver los errores de PHP. Con esa directiva activa, los errores fatales tipo "Failed opening required" aparecen directamente en el navegador y no necesitas mirar el log para verlos. Una vez activado, sirve para el resto de los labs.

Pista si la página dice 'Failed opening required ...'

Es el error fatal de require cuando no encuentra el fichero. Tres cosas a comprobar, en este orden:

  1. Que existen ~/gestor/templates/_header.php y ~/gestor/templates/_footer.php con esos nombres exactos (cuidado con el _ inicial).
  2. Que la ruta __DIR__ . '/../templates/_header.php' parte de public/: las plantillas tienen que estar un nivel por encima.
  3. Que Apache (usuario www-data) puede leer la carpeta templates/. Compruébalo con sudo -u www-data cat ~/gestor/templates/_header.php: si te muestra el contenido, ok; si dice "Permission denied", hay un problema de permisos en el directorio del usuario.

Responde en RESPUESTAS.md, sección ## Paso 3.

Si moviéramos _header.php dentro de public/ (por ejemplo, a public/_header.php) y un usuario curioso abriera la URL http://gestor.local/_header.php en su navegador, ¿qué vería? ¿Por qué es esto un problema, y por qué guardar las plantillas en templates/ (fuera de public/) lo evita?

Captura requerida. Visita http://gestor.local/listado.php y guarda una captura en capturas/paso3_listado_con_plantillas.png. Debe verse:

  • La URL http://gestor.local/listado.php en la barra de direcciones.
  • La pestaña del navegador con el título "Listado de incidencias — Gestor".
  • La cabecera azul con "Gestor de incidencias" y el menú con "Listado".
  • La tabla de las 3 incidencias.
  • El pie con el año actual.

Verificación antes de pasar al Paso 4.

  • listado.php usa require __DIR__ . '/../templates/_header.php' para la cabecera.
  • listado.php usa require __DIR__ . '/../templates/_footer.php' para el pie.
  • listado.php ya no contiene <!DOCTYPE html>, <head>, <header> ni <footer> propios.
  • La pestaña del navegador muestra "Listado de incidencias — Gestor".
  • La página se ve correctamente con la cabecera azul y la tabla.
  • php -l ~/gestor/public/listado.php no da errores.
  • He guardado capturas/paso3_listado_con_plantillas.png.

Paso 4 — CSS externo en public/css/estilos.css

Conceptos

  • El CSS, las imágenes, los iconos y los .js van en public/ porque sí necesitan ser accesibles desde el navegador (el navegador los pide directamente por URL).
  • Convención habitual: public/css/, public/js/, public/img/.
  • Cómo funciona el <link> (importante, distinto del require): cuando Apache envía el HTML al navegador, el navegador ve la línea <link rel="stylesheet" href="/css/estilos.css"> y hace una segunda petición HTTP a http://gestor.local/css/estilos.css. Apache le responde con el fichero CSS y el navegador lo aplica. Es decir: el CSS no lo "pega" el servidor dentro de la página como hace require con las plantillas; el servidor manda un HTML que menciona la ruta del CSS, y es el navegador quien lo va a buscar. Por eso el fichero CSS tiene que estar dentro de public/: el navegador lo va a pedir directamente. Las plantillas, en cambio, pueden quedarse fuera de public/ porque el navegador no las pide nunca.
  • La ruta en el <link> se escribe relativa a la raíz del servidor: href="/css/estilos.css". La barra inicial significa "desde la raíz del DocumentRoot de Apache", que en nuestro caso es ~/gestor/public/. Por eso /css/estilos.css apunta a ~/gestor/public/css/estilos.css.
  • Centralizar el CSS en un fichero externo tiene dos ventajas: un cambio de color o fuente afecta a todas las páginas a la vez, y el navegador puede cachear ese fichero en lugar de descargarlo en cada página.

Objetivo. Extraer el CSS que ahora está dentro del <style> de _header.php a un fichero externo public/css/estilos.css, y enlazarlo desde la plantilla.

Tarea 1 — Crear el directorio y el fichero CSS. Desde la terminal:

mkdir -p ~/gestor/public/css

Después, crea ~/gestor/public/css/estilos.css con exactamente el mismo contenido que tenías en el <style> de _header.php, más unas reglas extra para la tabla:

/* public/css/estilos.css */
body { font-family: Arial, sans-serif; margin: 0; background: #f5f7fa; color: #1a1a2e; }
header { background: #003865; color: white; padding: 1rem 2rem;
         display: flex; justify-content: space-between; align-items: center; }
header h1 { margin: 0; font-size: 1.2rem; }
nav a { color: #90caf9; text-decoration: none; margin-left: 1rem; }
nav a:hover { color: white; }
main { max-width: 900px; margin: 2rem auto; padding: 0 1rem; }
footer { text-align: center; padding: 1rem; color: #666;
         border-top: 1px solid #ddd; margin-top: 2rem; font-size: 0.85rem; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 8px 12px; border-bottom: 1px solid #ddd; text-align: left; }
th { background: #003865; color: white; }
tr:nth-child(even) { background: #f0f4f8; }

Tarea 2 — Sustituir el <style> por un <link> en _header.php. Abre ~/gestor/templates/_header.php y elimina el bloque entero <style> ... </style> que va dentro del <head>. En su lugar, añade esta línea (debajo del <title>):

<link rel="stylesheet" href="/css/estilos.css">

El bloque <head> debe quedar así:

1
2
3
4
5
6
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title><?= htmlspecialchars($titulo_pagina) ?> — Gestor</title>
  <link rel="stylesheet" href="/css/estilos.css">
</head>

Tarea 3 — Quitar los estilos inline de listado.php. En listado.php, los style="..." que pusimos en el <table>, <tr>, <th> y <td> ya están cubiertos por el CSS externo: bórralos. La tabla queda así de limpia:

<table>
  <tr>
    <th>ID</th>
    <th>Título</th>
    <th>Prioridad</th>
    <th>Estado</th>
  </tr>
  <?php foreach ($incidencias as $inc): ?>
    <tr>
      <td><?= $inc['id'] ?></td>
      <td><?= htmlspecialchars($inc['titulo']) ?></td>
      <td><?= htmlspecialchars($inc['prioridad']) ?></td>
      <td><?= htmlspecialchars($inc['estado']) ?></td>
    </tr>
  <?php endforeach ?>
</table>

Tarea 4 — Quitar el style inline del <footer> de _footer.php. El CSS externo ya define una regla footer { ... } con los mismos colores y bordes que el style="..." que pusimos en el Paso 2. Abre ~/gestor/templates/_footer.php y deja el <footer> sin atributo style:

1
2
3
4
5
6
  </main>
  <footer>
    Gestor de incidencias — ASIR IMW — <?= date('Y') ?>
  </footer>
</body>
</html>

Guarda los cuatro ficheros y recarga http://gestor.local/listado.php. La página debe verse igual que antes (mismos colores, mismas filas alternas), pero ahora todo el CSS viene del fichero externo: ni <style> en el <head> ni style="..." en ninguna etiqueta.

Pista para comprobar que el CSS lo sirve Apache

En el navegador, pulsa F12 para abrir las DevTools y ve a la pestaña Network (o Red). Recarga la página. Deberías ver entre las peticiones una a /css/estilos.css con código de estado 200. Si ves un 404, la ruta del <link> está mal o el fichero no está en ~/gestor/public/css/.

Responde en RESPUESTAS.md, sección ## Paso 4.

¿Por qué el CSS se carga con <link rel="stylesheet" href="/css/estilos.css"> (una etiqueta HTML que descarga el navegador) y no con require en PHP como las plantillas? Pista: piensa en quién interpreta cada cosa (servidor o navegador) y cuándo.

Captura requerida. Recarga http://gestor.local/listado.php con las DevTools abiertas en la pestaña Network y guarda una captura en capturas/paso4_listado_con_css_externo.png. Debe verse:

  • La página renderizada con los mismos colores que antes (cabecera azul, tabla con filas alternas).
  • La pestaña Network de las DevTools mostrando la petición a /css/estilos.css con código 200.

Verificación antes de pasar al Paso 5.

  • ~/gestor/public/css/estilos.css existe y contiene los estilos.
  • _header.php ya no tiene el bloque <style> y sí tiene el <link rel="stylesheet" ...>.
  • listado.php ya no tiene atributos style="..." en la tabla.
  • La página se ve igual que en el Paso 3, pero el CSS viene del fichero externo.
  • En la pestaña Network del navegador aparece la petición a /css/estilos.css con código 200.
  • He guardado capturas/paso4_listado_con_css_externo.png.

Paso 5 — Aplica lo aprendido: enlace activo en el menú

Qué es este paso

Los cuatro pasos anteriores han sido guiados: te he dado el código y tú lo has tecleado, probado y respondido. En este quinto paso no hay código de referencia completo. Lo que hay son requisitos, y tú aplicas lo aprendido sobre paso de variables a plantillas y sobre isset() para resolver un problema nuevo.

El reto: cuando un usuario está en listado.php, el enlace "Listado" del menú debe aparecer destacado (subrayado y en negrita). El sistema tiene que funcionar con cualquier futura página: si mañana añadimos panel.php con un enlace "Panel" en el menú, debe poder destacarse igual sin reescribir la plantilla.

Aquí no creamos un fichero nuevo: vamos a ampliar los ficheros del Paso 3 (_header.php y listado.php) y a añadir una clase nueva al CSS externo. Si después de hacerlo decides borrar esta ampliación, los ficheros vuelven a la versión del Paso 4 y siguen funcionando: el "enlace activo" es una capa decorativa encima del sistema de plantillas, no una pieza imprescindible.

Objetivo. Hacer que _header.php destaque el enlace del menú correspondiente a la página actual, usando una variable $pagina_activa que cada página define antes del require.

Tarea. Modifica tres ficheros cumpliendo los requisitos siguientes.

Requisitos obligatorios de contenido:

  1. En ~/gestor/templates/_header.php:
    • Al inicio del bloque PHP de cabecera (donde ya pones el valor por defecto de $titulo_pagina), añade un valor por defecto para $pagina_activa con isset(): si la página no la define, vale cadena vacía.
    • En el <nav>, sustituye el único <a href="/listado.php">Listado</a> actual por un bloque if/else que pinte dos <a> distintos según la condición $pagina_activa === 'listado': si es verdad, el <a> lleva class="activo"; si no, va sin clase. Es el mismo patrón if/else con sintaxis alternativa (if (...): ... else: ... endif) que viste en A4, pero ahora la "salida" de cada rama es una etiqueta HTML completa, no texto.
  2. En ~/gestor/public/css/estilos.css: añade al final una regla nueva para nav a.activo que destaque el enlace activo. Como mínimo debe cambiar el color a blanco, ponerlo en negrita y añadirle un borde inferior (border-bottom) para que se vea subrayado.
  3. En ~/gestor/public/listado.php: justo después de la línea $titulo_pagina = 'Listado de incidencias'; añade la línea que define $pagina_activa = 'listado';.

Requisitos de presentación HTML:

  • Al cargar http://gestor.local/listado.php, el enlace "Listado" del menú debe aparecer destacado (blanco, en negrita, subrayado).
  • Si quitas la línea $pagina_activa = 'listado'; de listado.php y recargas, el menú sigue funcionando pero ningún enlace queda marcado: no debe salir ningún error en pantalla.

Requisitos de calidad del código:

  • La plantilla no asume que $pagina_activa siempre esté definida: usa isset() (o el operador ??) para darle un valor por defecto.
  • La clase activo se añade desde PHP de forma dinámica, no escribiendo class="activo" hardcoded en el HTML.
Pista sobre cómo pintar dos <a> distintos según la condición

El bloque dentro del <nav> puede quedar así:

<?php if ($pagina_activa === 'listado'): ?>
  <a href="/listado.php" class="activo">Listado</a>
<?php else: ?>
  <a href="/listado.php">Listado</a>
<?php endif ?>

Es repetitivo (el href y el texto aparecen dos veces), pero a cambio es muy claro de leer y muy fácil de verificar: si pulsas "Ver código fuente" en el navegador después de cargar, verás un solo <a> impreso, el que corresponda. Es exactamente la misma estructura if/else con sintaxis alternativa que has usado en A4 — lo único nuevo es que aquí cada rama imprime una etiqueta HTML diferente.

Pista sobre el valor por defecto de $pagina_activa

Igual que en _header.php ya proteges $titulo_pagina con isset(), haz lo mismo con $pagina_activa:

if (!isset($pagina_activa)) {
    $pagina_activa = '';
}

Con la cadena vacía como valor por defecto, la condición $pagina_activa === 'listado' siempre da false si la página no la define, y ningún enlace sale marcado. Sin error.

Responde en RESPUESTAS.md, sección ## Paso 5.

  1. ¿Qué pasa exactamente si una página olvida definir $pagina_activa antes del require __DIR__ . '/../templates/_header.php';? ¿Sale un error en pantalla, sale un aviso, o sale la página normal sin enlace marcado? Explica por qué.
  2. Si mañana queremos añadir al menú un enlace "Panel" (a un futuro panel.php), ¿qué dos cambios habría que hacer en _header.php y qué línea habría que añadir en panel.php para que el enlace se destaque al estar en esa página? No hace falta escribir el código, basta con describir los cambios.

Captura requerida. Visita http://gestor.local/listado.php y guarda una captura en capturas/paso5_menu_activo.png. Debe verse:

  • La URL http://gestor.local/listado.php en la barra de direcciones.
  • La cabecera azul con el menú.
  • El enlace "Listado" del menú destacado: en blanco, en negrita y con borde inferior.

Verificación.

  • _header.php define $pagina_activa con isset() (valor por defecto cadena vacía).
  • El enlace "Listado" del <nav> recibe class="activo" solo cuando $pagina_activa === 'listado'.
  • estilos.css tiene una regla nav a.activo con color blanco, font-weight: bold y border-bottom.
  • listado.php define $pagina_activa = 'listado'; antes del require del header.
  • Al cargar listado.php, el enlace "Listado" sale destacado.
  • Si quitas temporalmente la línea de $pagina_activa en listado.php, no sale error y ningún enlace queda marcado.
  • He guardado capturas/paso5_menu_activo.png.

Si tu Paso 5 deja _header.php o listado.php con error de sintaxis

Antes de hacer el ZIP, deshaz tus cambios del Paso 5 en _header.php y en listado.php y vuelve a la versión que tenías al final del Paso 4. Es preferible entregar el Paso 5 vacío (perdiendo solo los puntos de ese paso) que entregar un _header.php roto que rompa también lo del Paso 3 y el Paso 4. Comprueba con php -l ~/gestor/templates/_header.php y 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_lab05/ (con mi apellido y nombre reales).
  • Dentro hay gestor/public/listado.php con el código final del Paso 5.
  • Dentro hay gestor/public/css/estilos.css con la regla nav a.activo incluida.
  • Dentro hay gestor/templates/_header.php y gestor/templates/_footer.php.
  • Dentro hay capturas/ con los 5 PNG: paso1_html_duplicado.png, paso2_estructura_templates.png, paso3_listado_con_plantillas.png, paso4_listado_con_css_externo.png, paso5_menu_activo.png.
  • Dentro hay RESPUESTAS.md con las 5 secciones rellenas.
  • Los tres ficheros PHP pasan php -l sin errores:
    • php -l ~/gestor/public/listado.php
    • php -l ~/gestor/templates/_header.php
    • php -l ~/gestor/templates/_footer.php
  • El ZIP se llama apellido_nombre_lab05.zip y 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_lab05/ con esta plantilla y rellena cada sección:

# Respuestas — Lab 05 Plantillas con `require`

**Nombre:** Apellido, Nombre
**Fecha:** YYYY-MM-DD

## Paso 1

(Tu respuesta: cuántas líneas de HTML estructural se repiten entre `listado.php` y `estado.php`, y qué pasaría con 10 páginas si cambias el `<title>` global.)

## Paso 2

(Tu respuesta: por qué `_header.php` abre `<main>` pero no lo cierra, y qué va en el hueco entre los dos `require`.)

## Paso 3

(Tu respuesta: qué vería un usuario que abriera `http://gestor.local/_header.php` si las plantillas estuvieran en `public/`, y por qué es un problema.)

## Paso 4

(Tu respuesta: por qué el CSS se carga con `<link>` y no con `require`, pensando en quién interpreta cada cosa.)

## Paso 5

1. (Tu respuesta: qué pasa si una página olvida definir `$pagina_activa` y por qué.)
2. (Tu respuesta: qué cambios harían falta en `_header.php` y en `panel.php` para destacar un futuro enlace "Panel".)