1 Motor de Plantillas
Roberto edited this page 2025-10-23 17:58:50 +00:00

Motor de Plantillas Template Engine

Descripción General

El Template Engine es un motor de plantillas personalizado implementado con el patrón Singleton que permite crear vistas dinámicas con soporte para herencia de plantillas, inclusión de archivos, variables, condicionales y bucles.


Inicialización

Configuración Inicial

El motor debe ser inicializado una única vez proporcionando la ruta base donde se encuentran las plantillas:

use Conexvirt\View\Template;

// Primera inicialización (requerida)
$template = Template::getInstance('/path/to/views');

// Llamadas posteriores (sin parámetro)
$template = Template::getInstance();

Nota: Si no se proporciona la ruta base en la primera llamada, se lanzará una excepción.


Renderización de Plantillas

Método render()

Renderiza una plantilla procesando todas las directivas y retorna el contenido HTML final:

// Renderizar plantilla sin datos
$html = $template->render('home');

// Renderizar con datos
$html = $template->render('users/profile', [
    'username' => 'Roberto',
    'email' => 'roberto@example.com'
]);

Método assign()

Asigna datos globales que estarán disponibles en todas las plantillas:

$template->assign([
    'siteName' => 'Mi Aplicación',
    'year' => 2024
]);

Sintaxis del Motor de Plantillas

1. Variables {{ }}

Las variables se escriben entre llaves dobles y son escapadas automáticamente con htmlspecialchars():

{{ username }}
{{ email }}
{{ siteName }}

Ejemplo:

<h1>Bienvenido {{ username }}</h1>
<p>Tu correo es: {{ email }}</p>

2. Helper old()

Recupera valores previos de formularios (útil tras validaciones):

{{ old('email') }}
{{ old('nombre', 'Valor por defecto') }}

Ejemplo:

<input type="text" name="email" value="{{ old('email') }}">
<input type="text" name="nombre" value="{{ old('nombre', 'Anónimo') }}">

3. Helper errors()

Obtiene mensajes de error de validación:

{{ errors()['email'][0] }}
{{ errors()['password'][1] }}

Ejemplo:

<span class="error">{{ errors()['email'][0] }}</span>

4. Helper asset()

Genera rutas de recursos estáticos:

{{ asset('css/style.css') }}
{{ asset('images/logo.png') }}

Ejemplo:

<link rel="stylesheet" href="{{ asset('css/style.css') }}">
<img src="{{ asset('images/logo.png') }}" alt="Logo">

Estructuras de Control

Condicionales {% if %}

Evalúa condiciones con funciones PHP como empty(), !empty(), isset(), !isset():

{% if !empty(username) %}
    <p>Hola {{ username }}</p>
{% endif %}

{% if empty(errors()['email']) %}
    <p class="success">Email válido</p>
{% endif %}

{% if isset(isAdmin) %}
    <a href="/admin">Panel Admin</a>
{% endif %}

Funciones soportadas:

  • empty(variable) - Verifica si está vacío
  • !empty(variable) - Verifica si NO está vacío
  • isset(variable) - Verifica si existe
  • !isset(variable) - Verifica si NO existe

Ejemplo completo:

{% if !empty(errors()['email']) %}
    <div class="alert alert-danger">
        {{ errors()['email'][0] }}
    </div>
{% endif %}

{% if !empty(old('remember')) %}
    <input type="checkbox" name="remember" checked>
{% endif %}

Bucles

Estructura {% foreach %}

Itera sobre arrays de datos:

{% foreach users as user %}
    <li>{{ user }}</li>
{% endforeach %}

Ejemplo completo:

<ul>
    {% foreach productos as producto %}
        <li>{{ producto }}</li>
    {% endforeach %}
</ul>

<div class="usuarios">
    {% foreach nombres as nombre %}
        <span class="badge">{{ nombre }}</span>
    {% endforeach %}
</div>

Renderización:

$html = $template->render('lista', [
    'productos' => ['Laptop', 'Mouse', 'Teclado'],
    'nombres' => ['Ana', 'Juan', 'María']
]);

Herencia de Plantillas

Directiva {% extends %}

Permite que una plantilla herede de otra plantilla base:

Archivo: layouts/base.php

<!DOCTYPE html>
<html>
<head>
    <title>{{ title }}</title>
</head>
<body>
    <header>{{ header }}</header>
    
    <main>
        {{ content }}
    </main>
    
    <footer>{{ footer }}</footer>
</body>
</html>

Archivo: home.php

{% extends 'layouts/base' %}

{% section 'header' %}
    <h1>Mi Sitio Web</h1>
{% endsection %}

{% section 'content' %}
    <p>Bienvenido {{ username }}</p>
{% endsection %}

{% section 'footer' %}
    <p>&copy; 2024 - Todos los derechos reservados</p>
{% endsection %}

Uso:

$html = $template->render('home', [
    'title' => 'Inicio',
    'username' => 'Roberto'
]);

Inclusión de Archivos

Directiva {% include %}

Incluye el contenido de otra plantilla dentro de la actual:

Archivo: partials/menu.php

<nav>
    <a href="/">Inicio</a>
    <a href="/about">Acerca de</a>
    <a href="/contact">Contacto</a>
</nav>

Archivo: page.php

<!DOCTYPE html>
<html>
<head>
    <title>{{ title }}</title>
</head>
<body>
    {% include 'partials/menu' %}
    
    <main>
        {{ content }}
    </main>
    
    {% include 'partials/footer' %}
</body>
</html>

Nota: Los includes soportan recursividad, es decir, un archivo incluido puede incluir otros archivos.


Orden de Procesamiento

El motor procesa las directivas en el siguiente orden:

  1. Extends - Herencia de plantillas
  2. Includes - Inclusión de archivos
  3. Variables - Reemplazo de {{ var }}
  4. Condicionales - Procesamiento de {% if %}
  5. Bucles - Procesamiento de {% foreach %}

Ejemplo Completo de Uso

Estructura de Archivos

views/
├── layouts/
│   └── app.php
├── partials/
│   ├── header.php
│   └── footer.php
└── users/
    └── profile.php

Archivo: layouts/app.php

<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <title>{{ pageTitle }}</title>
    <link rel="stylesheet" href="{{ asset('css/app.css') }}">
</head>
<body>
    {% include 'partials/header' %}
    
    <main class="container">
        {{ content }}
    </main>
    
    {% include 'partials/footer' %}
</body>
</html>

Archivo: users/profile.php

{% extends 'layouts/app' %}

{% section 'content' %}
    <div class="profile">
        <h1>Perfil de {{ username }}</h1>
        
        {% if !empty(bio) %}
            <p>{{ bio }}</p>
        {% endif %}
        
        <h3>Lenguajes de Programación:</h3>
        <ul>
            {% foreach languages as language %}
                <li>{{ language }}</li>
            {% endforeach %}
        </ul>
        
        {% if !empty(errors()['email']) %}
            <div class="alert">{{ errors()['email'][0] }}</div>
        {% endif %}
    </div>
{% endsection %}

Renderización en PHP

use Conexvirt\View\Template;

// Inicializar motor
$template = Template::getInstance(__DIR__ . '/views');

// Asignar datos globales
$template->assign([
    'siteName' => 'Mi Aplicación'
]);

// Renderizar vista
$html = $template->render('users/profile', [
    'pageTitle' => 'Perfil de Usuario',
    'username' => 'Roberto Calel',
    'bio' => 'Desarrollador Full Stack',
    'languages' => ['PHP', 'JavaScript', 'Python']
]);

echo $html;

Características de Seguridad

Escapado Automático

Todas las variables son escapadas automáticamente con htmlspecialchars() para prevenir ataques XSS:

// Si username contiene: <script>alert('XSS')</script>
{{ username }}
// Salida: &lt;script&gt;alert('XSS')&lt;/script&gt;

Valores por Defecto

Si una variable no existe, el motor retorna una cadena vacía en lugar de generar un error:

{{ variableInexistente }}
// Salida: (cadena vacía)

Manejo de Errores

Plantilla No Encontrada

// Lanza Exception: Template not found: /path/to/views/inexistente.php
$html = $template->render('inexistente');

Include No Encontrado

{% include 'archivo/inexistente' %}
<!-- Salida: <!-- Include not found: archivo/inexistente --> -->

Buenas Prácticas

1. Organización de Archivos

views/
├── layouts/          # Plantillas base
├── partials/         # Componentes reutilizables
├── pages/            # Páginas completas
└── components/       # Componentes específicos

2. Nomenclatura

  • Usar nombres descriptivos para plantillas
  • Usar snake_case para archivos: user_profile.php
  • Usar camelCase para variables: {{ userName }}

3. Separación de Lógica

// ❌ Evitar lógica compleja en plantillas
{% if user.role === 'admin' && user.active === true %}

// ✅ Procesar datos antes de renderizar
$template->render('admin', [
    'showAdminPanel' => $user->isActiveAdmin()
]);

4. Reutilización con Includes

// ✅ Componentes reutilizables
{% include 'partials/alert' %}
{% include 'partials/pagination' %}

Limitaciones Actuales

  • No soporta else o elseif en condicionales
  • Los bucles solo soportan valores escalares simples
  • No soporta operadores complejos en condiciones (&&, ||, ==)
  • No soporta acceso a propiedades de objetos en plantillas

Resumen de Sintaxis

Directiva Sintaxis Descripción
Variable {{ var }} Muestra variable escapada
Old {{ old('field') }} Valor previo de formulario
Errors {{ errors()['field'][0] }} Mensaje de error
Asset {{ asset('path') }} Ruta de recurso
If {% if condition %} ... {% endif %} Condicional
Foreach {% foreach array as item %} ... {% endforeach %} Bucle
Extends {% extends 'template' %} Herencia
Section {% section 'name' %} ... {% endsection %} Define sección
Include {% include 'template' %} Incluye archivo