Skip to content

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ónSi el archivo no existe¿Incluye dos veces?Úsala para
includeWarning y sigueSíPartes opcionales de una vista
requireError fatal y se detieneSíVistas o partes imprescindibles
include_onceWarning y sigueNoPoco usada
require_onceError fatalNoArchivos 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 sigue

Rutas: 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.php
php
<?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>&copy; <?= 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.php
php
<?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>&copy; <?= 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']; // localhost

Ejercicios ​

  1. Crea un sitio de 3 páginas (Inicio, Nosotros, Contacto) con header.php y footer.php, donde el enlace de la página actual tenga la clase activo.
  2. Modifica la función render() para que lance una excepción si $nombre contiene .. (evita salir de la carpeta de vistas).
  3. Crea un partial tarjeta.php que reciba $titulo y $texto, y úsalo 3 veces dentro de un foreach con distintos datos.
Ver solución
  1. Usa la estructura de "sitio con partials" de esta lección, con MENU de tres entradas y paginaActual() para la clase activo.

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_once para configuración, funciones y clases; require para vistas imprescindibles; include para 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() con extract + ob_start te 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.

Academia Web — aprende, copia y construye.