Tema
Include y plantillas
Un sitio real tiene muchas páginas que comparten encabezado, menú y pie. Copiar y pegar ese HTML en cada archivo es una pesadilla de mantener. En esta lección aprenderás a dividir tu proyecto en piezas reutilizables con include y require, a crear un layout y a pasar datos a tus vistas.
Concepto
Piensa en un periódico: cada página tiene el mismo encabezado con el logo y el mismo pie con los datos de contacto; sólo cambia el contenido del centro. En PHP, guardas el encabezado y el pie en archivos aparte y los "pegas" en cada página con include o require. Si cambias el menú, lo cambias en un solo archivo.
Las cuatro instrucciones
| Instrucción | Si el archivo no existe | ¿Incluye dos veces? | Úsala para |
|---|---|---|---|
include | Warning y sigue | Sí | Partes opcionales de una vista |
require | Error fatal y se detiene | Sí | Vistas o partes imprescindibles |
include_once | Warning y sigue | No | Poco usada |
require_once | Error fatal | No | Archivos de funciones, configuración, clases |
php
<?php
require_once __DIR__ . '/config.php'; // Configuración: imprescindible, una sola vez
require_once __DIR__ . '/helpers.php'; // Funciones: una sola vez (evita "Cannot redeclare")
include __DIR__ . '/partials/banner.php'; // Banner promocional: si falta, la página sigueRutas: usa siempre __DIR__
__DIR__ es la carpeta del archivo actual. Si escribes rutas relativas como include 'header.php', PHP las resuelve según el directorio de trabajo, que puede ser otro cuando un archivo incluye a otro que incluye a otro. Resultado: "failed to open stream".
php
<?php
// ✗ Frágil: depende de desde dónde se ejecute
include 'partials/header.php';
// ✓ Robusto: siempre relativo a ESTE archivo
include __DIR__ . '/partials/header.php';
// Subir un nivel
require __DIR__ . '/../config.php';Ejemplo: sitio con partials
Estructura:
txt
mi-sitio/
├── config.php
├── helpers.php
├── partials/
│ ├── header.php
│ └── footer.php
├── index.php
├── servicios.php
└── contacto.phpphp
<?php
declare(strict_types=1);
const SITIO_NOMBRE = 'Estudio Creativo';
const SITIO_EMAIL = 'hola@estudiocreativo.mx';
// Menú centralizado: agrega una página aquí y aparece en todo el sitio
const MENU = [
'index.php' => 'Inicio',
'servicios.php' => 'Servicios',
'contacto.php' => 'Contacto',
];php
<?php
declare(strict_types=1);
function e(?string $texto): string
{
return htmlspecialchars($texto ?? '', ENT_QUOTES, 'UTF-8');
}
/** Devuelve el nombre del archivo que se está visitando, p. ej. "servicios.php" */
function paginaActual(): string
{
return basename($_SERVER['SCRIPT_NAME']);
}php
<?php
// Espera que la página defina $titulo antes de incluirlo.
$titulo = $titulo ?? SITIO_NOMBRE;
?>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= e($titulo) ?> | <?= e(SITIO_NOMBRE) ?></title>
<link rel="stylesheet" href="estilos.css">
</head>
<body>
<header>
<strong><?= e(SITIO_NOMBRE) ?></strong>
<nav>
<?php foreach (MENU as $archivo => $texto): ?>
<a href="<?= $archivo ?>"
class="<?= paginaActual() === $archivo ? 'activo' : '' ?>">
<?= e($texto) ?>
</a>
<?php endforeach; ?>
</nav>
</header>
<main>php
</main>
<footer>
<p>© <?= date('Y') ?> <?= e(SITIO_NOMBRE) ?> · <?= e(SITIO_EMAIL) ?></p>
</footer>
</body>
</html>php
<?php
require_once __DIR__ . '/config.php';
require_once __DIR__ . '/helpers.php';
$titulo = 'Servicios';
$servicios = [
['nombre' => 'Diseño web', 'precio' => 8500],
['nombre' => 'Tienda en línea', 'precio' => 18000],
['nombre' => 'Mantenimiento mensual', 'precio' => 1200],
];
require __DIR__ . '/partials/header.php';
?>
<h1>Nuestros servicios</h1>
<ul>
<?php foreach ($servicios as $s): ?>
<li><?= e($s['nombre']) ?> — desde $<?= number_format($s['precio']) ?> MXN</li>
<?php endforeach; ?>
</ul>
<?php require __DIR__ . '/partials/footer.php'; ?>Las variables definidas antes del require (como $titulo) están disponibles dentro del archivo incluido: include funciona como si pegaras el código ahí mismo.
Un paso más: función vista() con layout
El patrón anterior funciona, pero cada página repite el require del header y del footer, y comparte todas sus variables con los partials. Una solución más profesional es una función que renderiza una vista dentro de un layout, pasando sólo los datos que necesita.
Estructura:
txt
proyecto/
├── vistas/
│ ├── layout.php
│ ├── inicio.php
│ └── productos/
│ └── lista.php
├── helpers.php
└── index.phpphp
<?php
declare(strict_types=1);
function e(?string $texto): string
{
return htmlspecialchars($texto ?? '', ENT_QUOTES, 'UTF-8');
}
/**
* Renderiza una vista y devuelve el HTML como string.
* @param string $nombre Ruta de la vista sin extensión, p. ej. 'productos/lista'
* @param array $datos Variables que estarán disponibles en la vista
*/
function render(string $nombre, array $datos = []): string
{
$archivo = __DIR__ . "/vistas/$nombre.php";
if (!is_file($archivo)) {
throw new RuntimeException("La vista '$nombre' no existe");
}
extract($datos, EXTR_SKIP); // ['titulo' => 'x'] se convierte en $titulo = 'x'
ob_start(); // Empieza a "capturar" la salida en vez de enviarla
require $archivo;
return ob_get_clean(); // Devuelve lo capturado y limpia el buffer
}
/**
* Renderiza una vista dentro del layout principal.
*/
function vista(string $nombre, array $datos = []): void
{
$contenido = render($nombre, $datos);
echo render('layout', [...$datos, 'contenido' => $contenido]);
}php
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= e($titulo ?? 'Mi App') ?></title>
</head>
<body>
<nav><a href="?p=inicio">Inicio</a> | <a href="?p=productos">Productos</a></nav>
<main>
<?= $contenido /* Ya es HTML generado por nosotros: no se escapa */ ?>
</main>
<footer>© <?= date('Y') ?></footer>
</body>
</html>php
<h1>Bienvenido, <?= e($usuario) ?></h1>
<p>Tienes <?= $pendientes ?> pedidos pendientes.</p>php
<h1>Productos</h1>
<?php if (empty($productos)): ?>
<p>No hay productos.</p>
<?php else: ?>
<ul>
<?php foreach ($productos as $p): ?>
<li><?= e($p['nombre']) ?></li>
<?php endforeach; ?>
</ul>
<?php endif; ?>php
<?php
declare(strict_types=1);
require_once __DIR__ . '/helpers.php';
$pagina = $_GET['p'] ?? 'inicio';
match ($pagina) {
'inicio' => vista('inicio', [
'titulo' => 'Inicio',
'usuario' => 'Erick',
'pendientes' => 3,
]),
'productos' => vista('productos/lista', [
'titulo' => 'Productos',
'productos' => [['nombre' => 'Taza'], ['nombre' => 'Playera']],
]),
default => (function () {
http_response_code(404);
vista('inicio', ['titulo' => 'No encontrado', 'usuario' => 'visitante', 'pendientes' => 0]);
})(),
};Ventajas:
- Cada vista recibe sólo sus datos (no hereda variables sueltas).
- El layout se define una vez.
- Es exactamente la idea detrás de los motores de plantillas como Blade (Laravel) o Twig. Lo retomarás en MVC desde cero.
Buena práctica
Mantén la lógica (consultas, cálculos, validaciones) fuera de las vistas. Una vista sólo debería tener echo, if, foreach y llamadas a e(). Si ves una consulta SQL en una vista, muévela.
Nunca incluyas archivos según lo que mande el usuario
php
<?php
// ✗ PELIGRO: ?p=../../config o una URL remota
include $_GET['p'] . '.php';Esto permite a un atacante leer o ejecutar archivos que no deberían (Local/Remote File Inclusion). Usa siempre una lista blanca de páginas permitidas, como el match del ejemplo.
Error común
"Headers already sent": aparece si un archivo incluido imprime algo (aunque sea un espacio o un salto de línea después de ?>) antes de que llames a header() o session_start(). Por eso en archivos sólo-PHP no se cierra la etiqueta ?>.
Archivos que devuelven valores
include puede devolver un valor con return. Es la forma típica de archivos de configuración:
php
<?php
// config/app.php
return [
'nombre' => 'Mi App',
'debug' => true,
'db' => [
'host' => 'localhost',
'nombre' => 'mi_app',
],
];php
<?php
$config = require __DIR__ . '/config/app.php';
echo $config['db']['host']; // localhostEjercicios
- Crea un sitio de 3 páginas (Inicio, Nosotros, Contacto) con
header.phpyfooter.php, donde el enlace de la página actual tenga la claseactivo. - Modifica la función
render()para que lance una excepción si$nombrecontiene..(evita salir de la carpeta de vistas). - Crea un partial
tarjeta.phpque reciba$tituloy$texto, y úsalo 3 veces dentro de unforeachcon distintos datos.
Ver solución
Usa la estructura de "sitio con partials" de esta lección, con
MENUde tres entradas ypaginaActual()para la claseactivo.
php
<?php
function render(string $nombre, array $datos = []): string
{
if (str_contains($nombre, '..') || !preg_match('#^[a-z0-9_/-]+$#', $nombre)) {
throw new InvalidArgumentException('Nombre de vista no válido');
}
$archivo = __DIR__ . "/vistas/$nombre.php";
if (!is_file($archivo)) {
throw new RuntimeException("La vista '$nombre' no existe");
}
extract($datos, EXTR_SKIP);
ob_start();
require $archivo;
return ob_get_clean();
}php
<?php
// partials/tarjeta.php
?>
<article class="tarjeta">
<h3><?= e($titulo) ?></h3>
<p><?= e($texto) ?></p>
</article>php
<?php
// index.php
require_once __DIR__ . '/helpers.php';
$tarjetas = [
['titulo' => 'Rápido', 'texto' => 'Sitios que cargan en menos de 2 segundos.'],
['titulo' => 'Seguro', 'texto' => 'HTTPS y buenas prácticas incluidas.'],
['titulo' => 'Adaptable', 'texto' => 'Se ve perfecto en celular.'],
];
foreach ($tarjetas as ['titulo' => $titulo, 'texto' => $texto]) {
include __DIR__ . '/partials/tarjeta.php'; // Usa $titulo y $texto de esta vuelta
}Resumen
require_oncepara configuración, funciones y clases;requirepara vistas imprescindibles;includepara partes opcionales.- Construye rutas con
__DIR__para que no dependan del directorio de trabajo. - Separa header, footer y menú en partials para no repetir HTML.
- Una función
vista()conextract+ob_startte da layouts y vistas aisladas. - Los archivos incluidos pueden devolver valores con
return(ideal para configuración). - Nunca incluyas rutas que vengan del usuario: usa listas blancas.