Symfony 2.4, el libro oficial

7.4. Etiquetas y helpers

Después de estudiar las secciones anteriores, ya conoces los conceptos básicos de las plantillas, la sintaxis de su nomenclatura y hasta cómo aplicar la herencia de plantillas. Esta sección explica cómo realizar las tareas más habituales en las plantillas, como por ejemplo incluir unas plantillas en otras, crear enlaces entre páginas y mostrar imágenes.

Symfony2 incluye varias etiquetas especiales de Twig y funciones que facilitan el trabajo del diseñador de las plantillas. El sistema de plantillas también incluye varios helpers para aquellos diseñadores que todavía utilizan PHP para diseñar las plantillas.

Aunque ya hemos visto algunas etiquetas de Twig ({% block %} y {% extends %}) y algunos helpers de PHP, ($view['slot']), a continuación aprenderás unos cuantos más.

7.4.1. Incluyendo otras plantillas

Resulta habitual querer incluir la misma plantilla o fragmento de código en varias páginas diferentes. Si la aplicación tiene por ejemplo un listado de artículos, el código de la plantilla que muestra un artículo se puede utilizar en la página de detalle del artículo, en una página que muestra los artículos más populares, o en una lista de los artículos más recientes.

En PHP, cuando necesitas reutilizar un trozo de código, normalmente mueves el código a una nueva clase o función. En las plantillas se aplica la misma idea. Después de mover una parte del código de la plantilla a su propia plantilla, este se puede incluir en cualquier otra plantilla. En primer lugar, crea la nueva plantilla reutilizable.

{# src/Acme/ArticleBundle/Resources/views/Article/articleDetails.html.twig #}
<h2>{{ article.title }}</h2>
<h3 class="byline">by {{ article.authorName }}</h3>

<p>
    {{ article.body }}
</p>
<!-- src/Acme/ArticleBundle/Resources/views/Article/articleDetails.html.php -->
<h2><?php echo $article->getTitle() ?></h2>
<h3 class="byline">by <?php echo $article->getAuthorName() ?></h3>

<p>
    <?php echo $article->getBody() ?>
</p>

Ahora ya puedes incluir fácilmente esta plantilla en cualquier otra:

{# src/Acme/ArticleBundle/Resources/views/Article/list.html.twig #}
{% extends 'AcmeArticleBundle::layout.html.twig' %}

{% block body %}
    <h1>Recent Articles<h1>

    {% for article in articles %}
        {{ include(
            'AcmeArticleBundle:Article:articleDetails.html.twig',
            {'article': article}
        ) }}
    {% endfor %}
{% endblock %}
<!-- src/Acme/ArticleBundle/Resources/Article/list.html.php -->
<?php $view->extend('AcmeArticleBundle::layout.html.php') ?>

<?php $view['slots']->start('body') ?>
    <h1>Recent Articles</h1>

    <?php foreach ($articles as $article): ?>
        <?php echo $view->render('AcmeArticleBundle:Article:articleDetails.html.php', array('article' => $article)) ?>
    <?php endforeach; ?>
<?php $view['slots']->stop() ?>

La plantilla se incluye utilizando la función {{ include() }}. Observa que el nombre de la plantilla utiliza la misma nomenclatura especial explicada anteriormente. Como la plantilla articleDetails.html.twig necesita una variable llamada article, la plantilla list.html.twig se la pasa utilizando la palabra reservada with.

Truco La sintaxis {'article': article} es la forma de crear un objeto o hash en Twig (es decir, como un array pero sin claves numéricas). Si quieres pasar varios elementos, utiliza la sintaxis {'variable1': 'valor1', 'variable2': 'valor2'}.

Nota La función include() es una nueva característica de Twig que está disponible a partir de la versión 2.2 de Symfony. Antes se utilizaba la etiqueta {% include %}.

7.4.2. Integrando controladores

En ocasiones es necesario hacer algo más que incluir una simple plantilla estática. Imagina que en tu sitio web tienes una barra lateral que muestra los tres artículos más recientes. Para obtener esos tres artículos es necesario realizar una consulta a la base de datos o alguna otra operación similar que no se puede incluir en la propia plantilla.

La solución consiste en insertar en la plantilla el resultado devuelto por un controlador de la aplicación. En primer lugar, crea un controlador que renderice el listado de los artículos recientes:

// src/Acme/ArticleBundle/Controller/ArticleController.php
class ArticleController extends Controller
{
    public function recentArticlesAction($max = 3)
    {
        // hace una llamada a la base de datos u otra lógica
        // para obtener los "$max" artículos más recientes
        $articles = ...;

        return $this->render(
            'AcmeArticleBundle:Article:recentList.html.twig',
            array('articles' => $articles)
        );
    }
}

La plantilla recentList asociada al controlador anterior es muy sencilla, ya que sólo incluye el bucle for necesario para crear el listado de artículos:

{# src/Acme/ArticleBundle/Resources/views/Article/recentList.html.twig #}
{% for article in articles %}
    <a href="/article/{{ article.slug }}">
        {{ article.title }}
        </a>
{% endfor %}
<!-- src/Acme/ArticleBundle/Resources/views/Article/recentList.html.php -->
<?php foreach ($articles as $article): ?>
    <a href="/article/<?php echo $article->getSlug() ?>">
        <?php echo $article->getTitle() ?>
        </a>
<?php endforeach; ?>

Nota Observa que en este ejemplo la URL de cada artículo se ha escrito directamente como /article/*slug*. Ela siguiente sección verás que esto es una mala práctica y descubrirás cómo hacerlo correctamente.

Por último, para mostrar en cualquier plantilla el resultado de ejecutar este controlador, utiliza la función render e identifica al controlador utilizando la notación especial bundle:controlador:acción):

{# app/Resources/views/base.html.twig #}

{# ... #}
<div id="sidebar">
    {{ render(controller('AcmeArticleBundle:Article:recentArticles', {
        'max': 3
    })) }}
</div>
<!-- app/Resources/views/base.html.php -->

<!-- ... -->
<div id="sidebar">
    <?php echo $view['actions']->render(
        new ControllerReference(
            'AcmeArticleBundle:Article:recentArticles',
            array('max' => 3)
        )
    ) ?>
</div>

Siempre que necesites una variable o alguna otra información dinámica a la que no puedas acceder directamente desde la plantilla, deberías renderizar un controlador con la función render(). Los controladores se ejecutan muy rápidamente y permiten organizar mejor el código de la aplicación.

7.4.3. Contenido asíncrono con hinclude.js

Los controladores se pueden incluir asíncronamente en las plantillas usando la librería hinclude.js de JavaScript. Symfony2 utiliza la función render() estándar para configurar las etiquetas hinclude:

{% render url('...') with {}, {'standalone': 'js'} %}
<?php echo $view['actions']->render(
    new ControllerReference('...'),
    array('strategy' => 'hinclude')
) ?>

<?php echo $view['actions']->render(
    $view['router']->generate('...'),
    array('strategy' => 'hinclude')
) ?>

Nota Para que el código anterior funcione, asegúrate de incluir primero la librería hinclude.js en la página.

Nota Si utilizas un controlador en vez de una URL, no olvides activar primero la configuración fragments de Symfony:

# app/config/config.yml
framework:
    # ...
    fragments: { path: /_fragment }
<!-- app/config/config.xml -->
<framework:config>
    <framework:fragments path="/_fragment" />
</framework:config>
// app/config/config.php
$container->loadFromExtension('framework', array(
    // ...
    'fragments' => array('path' => '/_fragment'),
));

El contenido por defecto que se muestra mientras se carga el contenido o cuando JavaScript está desactivado se puede configurar globalmente en el archivo config.yml de la aplicación:

# app/config/config.yml
framework:
    # ...
    templating:
        hinclude_default_template: AcmeDemoBundle::hinclude.html.twig
<!-- app/config/config.xml -->
<framework:config>
    <framework:templating
        hinclude-default-template="AcmeDemoBundle::hinclude.html.twig" />
</framework:config>
// app/config/config.php
$container->loadFromExtension('framework', array(
    // ...
    'templating'      => array(
        'hinclude_default_template' => array(
            'AcmeDemoBundle::hinclude.html.twig'
        ),
    ),
));

Nota Las plantillas por defecto de la función render_hinclude() se incluyeron a partir de la versión 2.2 de Symfony.

Además de la plantilla por defecto global, puedes definir para cada render_hinclude() su propia plantilla por defecto que se muestra si cualquier error impide cargar el contenido solicitado:

{{ render_hinclude(controller('...'), {
    'default': 'AcmeDemoBundle:Default:content.html.twig'
}) }}
<?php echo $view['actions']->render(
    new ControllerReference('...'),
    array(
        'strategy' => 'hinclude',
        'default' => 'AcmeDemoBundle:Default:content.html.twig',
    )
) ?>

La última opción consiste en definir directamente el contenido por defecto en la propia llamada a render_hinclude(), lo que evita que tengas que definir otra plantilla:

{{ render_hinclude(controller('...'), {'default': 'Cargando...'}) }}
<?php echo $view['actions']->render(
    new ControllerReference('...'),
    array(
        'strategy' => 'hinclude',
        'default'  => 'Cargando...',
    )
) ?>

7.4.4. Contenido asíncrono con hinclude.js

Los controladores se pueden embeber asíncronamente mediante la librería hinclude.js de JavaScript. Como el contenido incluído proviene de otra página (de otro controlador) Symfony2 utiliza el habitual helper render para configurar las etiquetas hinclude:

{% render url('...') with {}, {'standalone': 'js'} %}
<?php echo $view['actions']->render(
    $view['router']->generate('...'),
    array('standalone' => 'js')
) ?>

Nota Para que este código funcione, debes incluir la librería hinclude.js en tus páginas.

El contenido por defecto que se muestra mientras se cargan los contenidos o cuando JavaScript está desactivado se puede configurar globalmente en la configuración de la aplicación:

# app/config/config.yml
framework:
    # ...
    templating:
        hinclude_default_template: AcmeDemoBundle::hinclude.html.twig
<!-- app/config/config.xml -->
<framework:config>
    <framework:templating hinclude-default-template="AcmeDemoBundle::hinclude.html.twig" />
</framework:config>
// app/config/config.php
$container->loadFromExtension('framework', array(
    // ...
    'templating'      => array(
        'hinclude_default_template' => array('AcmeDemoBundle::hinclude.html.twig'),
    ),
));

7.4.5. Enlazando páginas

Crear enlaces a otras páginas de la aplicación es una de las tareas más comunes de una plantilla. En lugar de generar a mano las URL dentro de la plantilla, utiliza la función path de Twig (o el helper router en PHP) para generar las URL utilizando la configuración del sistema de enrutamiento. De esta manera, si más adelante quieres cambiar el aspecto de cualquier URL, sólo tienes que actualizar un archivo de configuración y todas las URL de las plantillas se actualizarán instantáneamente.

El siguiente ejemplo crea un enlace a la página _welcome, cuya configuración de enrutamiento se muestra a continuación:

_welcome:
    path:     /
    defaults: { _controller: AcmeDemoBundle:Welcome:index }
<route id="_welcome" path="/">
    <default key="_controller">AcmeDemoBundle:Welcome:index</default>
</route>
$collection = new RouteCollection();
$collection->add('_welcome', new Route('/', array(
    '_controller' => 'AcmeDemoBundle:Welcome:index',
)));

return $collection;

Para enlazar a la página, utiliza la función path de Twig pasándole el nombre de la página a la que quieres ir al pinchar sobre el enlace:

<a href="{{ path('_welcome') }}">Home</a>
<a href="<?php echo $view['router']->generate('_welcome') ?>">Home</a>

Como era de esperar, este código hace que la URL generada sea /. Si ahora utilizas una ruta más avanzada:

article_show:
    path:     /article/{slug}
    defaults: { _controller: AcmeArticleBundle:Article:show }
<route id="article_show" path="/article/{slug}">
    <default key="_controller">AcmeArticleBundle:Article:show</default>
</route>
$collection = new RouteCollection();
$collection->add('article_show', new Route('/article/{slug}', array(
    '_controller' => 'AcmeArticleBundle:Article:show',
)));

return $collection;

Como la ruta anterior tiene alguna parte variable, además de especificar el nombre de la ruta (article_show) también es necesario asignar un valor al parámetro {slug}. Haciendo uso de esta ruta puedes mejorar la plantilla recentList que se utilizó en los ejemplos de las secciones anteriores:

{# src/Acme/ArticleBundle/Resources/views/Article/recentList.html.twig #}
{% for article in articles %}
    <a href="{{ path('article_show', {'slug': article.slug}) }}">
        {{ article.title }}
        </a>
{% endfor %}
<!-- src/Acme/ArticleBundle/Resources/views/Article/recentList.html.php -->
<?php foreach ($articles in $article): ?>
    <a href="<?php echo $view['router']->generate('article_show', array(
        'slug' => $article->getSlug(),
    )) ?>">
        <?php echo $article->getTitle() ?>
    </a>
<?php endforeach; ?>

Truco La función path() siempre genera URL relativas. Para generar URL absolutas, utiliza la función url() de Twig:

<a href="{{ url('_welcome') }}">Home</a>

Para las plantillas PHP utiliza el método generate():

<a href="<?php echo $view['router']->generate(
    '_welcome',
    array(),
    true
) ?>">Home</a>

7.4.6. Enlazando archivos web

Las plantillas de las aplicaciones web suelen enlazar con los web assets o archivos web, tales como imágenes, hojas de estilo CSS, archivos JavaScript, etc. De nuevo no es aconsejable generar a mano las URL de este tipo de archivos, ya que Symfony2 ofrece una solución mejor y mucho más flexible mediante la función asset de Twig:

<img src="{{ asset('images/logo.png') }}" alt="Symfony!" />

<link href="{{ asset('css/blog.css') }}" rel="stylesheet" type="text/css" />
<img src="<?php echo $view['assets']->getUrl('images/logo.png') ?>" alt="Symfony!" />

<link href="<?php echo $view['assets']->getUrl('css/blog.css') ?>" rel="stylesheet" type="text/css" />

Al utilizar la función asset tu aplicación es más flexible y fácil de trasladar de un servidor a otro. Si tu aplicación está instalada en la raíz del servidor (http://ejemplo.com), entonces las rutas de los archivos web serán del tipo /images/logo.png. Pero si tu aplicación se encuentra en un subdirectorio (http://ejemplo.com/aplicacion), las rutas ahora son del tipo /aplicacion/images/logo.png. La función asset se encarga de tener esto en cuenta automáticamente, por lo que siempre genera las rutas correctas.

Además, si utilizas la función asset, puedes hacer que Symfony añada una cadena al final de la URL del archivo web. De esta manera, se garantiza que los archivos estáticos que han sido actualizados no se almacenen en la caché. De esta manera, en vez de utilizar la URL simple /images/logo.png Symfony podría generar la URL /images/logo.png?v2.