Extendiendo Twig

Prudencia

Esta sección describe cómo extender Twig a partir de Twig 1.12. Si estás usando una versión anterior, en vez de esta lee el capítulo del legado.

Twig se puede extender en muchos aspectos; puedes añadir etiquetas adicionales, filtros, pruebas, operadores, variables globales y funciones. Incluso puedes extender el propio analizador con visitantes de nodo.

Nota

La primer sección de este capítulo describe la forma de extender Twig fácilmente. Si deseas volver a utilizar tus cambios en diferentes proyectos o si deseas compartirlos con los demás, entonces, debes crear una extensión tal como se describe en la siguiente sección.

Prudencia

Al extender Twig sin crear una extensión, Twig no será capaz de volver a compilar tus plantillas al actualizar el código de PHP. Para ver tus cambios en tiempo real, o bien desactiva la memorización de plantillas o empaca tu código en una extensión (ve la siguiente sección de este capítulo).

Antes de extender Twig, debes entender las diferencias entre todos los diferentes puntos de extensión posibles y cuándo utilizarlos.

En primer lugar, recuerda que el lenguaje de Twig tiene dos construcciones principales:

  • {{ }}: Utilizada para imprimir el resultado de la evaluación de la expresión;
  • {% %}: Utilizada para ejecutar instrucciones.

Para entender por qué Twig expone tantos puntos de extensión, vamos a ver cómo implementar un generador Lorem ipsum (este necesita saber el número de palabras a generar).

Puedes utilizar una etiqueta Lipsum:

{% lipsum 40 %}

Eso funciona, pero usar una etiqueta para lipsum no es una buena idea por al menos tres razones principales:

  • lipsum no es una construcción del lenguaje;

  • La etiqueta produce algo;

  • La etiqueta no es flexible ya que no la puedes utilizar en una expresión:

    {{ 'algún texto' ~ {% lipsum 40 %} ~ 'algo más de texto' }}

De hecho, rara vez es necesario crear etiquetas; y es una muy buena noticia porque las etiquetas son el punto de extensión más complejo de Twig.

Ahora, vamos a utilizar un filtro lipsum:

{{ 40|lipsum }}

Una vez más, funciona, pero se ve raro. Un filtro transforma el valor pasado a algo más pero aquí utilizamos el valor para indicar el número de palabras a generar (así que, 40 es un argumento del filtro, no el valor que se va a transformar).

En seguida, vamos a utilizar una función lipsum:

{{ lipsum(40) }}

Aquí vamos. Para este ejemplo concreto, la creación de una función es el punto de extensión a usar. Y la puedes usar en cualquier lugar en que se acepte una expresión:

{{ 'some text' ~ lipsum(40) ~ 'some more text' }}

{% set lipsum = lipsum(40) %}

Por último pero no menos importante, también puedes utilizar un objeto global con un método capaz de generar texto Lorem Ipsum:

{{ text.lipsum(40) }}

Como regla general, utiliza funciones para las características más utilizadas y objetos globales para todo lo demás.

Ten en cuenta lo siguiente cuando quieras extender Twig:

¿Qué? ¿dificultad para implementarlo? ¿Con qué frecuencia? ¿Cuándo?
macro trivial frecuente Generación de contenido
global trivial frecuente Objeto ayudante
function trivial frecuente Generación de contenido
filter trivial frecuente Transformación de valor
tag complejo raro Constructor del lenguaje DSL
test trivial raro Decisión booleana
operator trivial raro Transformación de valores

Globales

Una variable global es como cualquier otra variable de plantilla, excepto que está disponible en todas las plantillas y macros:

$twig = new Twig_Environment($loader);
$twig->addGlobal('text', new Text());

Entonces puedes utilizar la variable text en cualquier parte de una plantilla:

{{ text.lipsum(40) }}

Filtros

Crear un filtro es tan sencillo como asociar un nombre con un ejecutable PHP:

// una función anónima
$filter = new Twig_SimpleFilter('rot13', function ($string) {
    return str_rot13($string);
});

// o una simple función PHP
$filter = new Twig_SimpleFilter('rot13', 'str_rot13');

// o un método de clase
$filter = new Twig_SimpleFilter('rot13', array('SomeClass', 'rot13Filter'));

El primer argumento pasado al constructor de Twig_Filter_Function es el nombre del filtro que usarás en las plantillas y el segundo es el ejecutable PHP asociado.

Luego, añade el filtro a tu entorno Twig:

$twig = new Twig_Environment($loader);
$twig->addFilter($filter);

Y aquí tienes cómo utilizarlo en una plantilla:

{{ 'Twig'|rot13 }}

{# producirá Gjvt #}

Cuando es llamado por Twig, el ejecutable PHP en el lado izquierdo recibe el filtro (antes de la barra vertical |) como primer argumento y los argumentos extras pasados al filtro (dentro de paréntesis ()) como argumentos extra.

Por ejemplo, el siguiente código:

{{ 'TWIG'|lower }}
{{ now|date('d/m/Y') }}

se compila a algo como lo siguiente:

<?php echo strtolower('TWIG') ?>
<?php echo twig_date_format_filter($now, 'd/m/Y') ?>

La clase Twig_SimpleFilter toma un arreglo de opciones como último argumento:

$filter = new Twig_SimpleFilter('rot13', 'str_rot13', $options);

Entorno consciente de filtros

Si en tu filtro quieres acceder a la instancia del entorno actual, pon a true la opción needs_environment; Twig pasará el entorno actual como primer argumento al llamar al filtro:

$filter = new Twig_SimpleFilter('rot13', function (Twig_Environment $env, $string) {
    // obtiene el juego de caracteres actual, por ejemplo
    $charset = $env->getCharset();

    return str_rot13($string);
}, array('needs_environment' => true));

Filtros conscientes del contexto

Si quieres acceder al contexto actual en tu filtro, pon a true la opción needs_context; Twig pasará el contexto actual como primer argumento al llamar al filtro (o el segundo si needs_environment también es true):

$filter = new Twig_SimpleFilter('rot13', function ($context, $string) {
    // ...
}, array('needs_context' => true));

$filter = new Twig_SimpleFilter('rot13', function (Twig_Environment $env, $context, $string) {
    // ...
}, array('needs_context' => true, 'needs_environment' => true));

Escapando automáticamente

Si está habilitado el escape automático, puedes escapar la salida del filtro antes de imprimir. Si tu filtro actúa como un escapista (o explícitamente produce código html o javascript), desearás que se imprima la salida sin procesar. En tal caso, establece la opción is_safe:

$filter = new Twig_SimpleFilter('nl2br', 'nl2br', array('is_safe' => array('html')));

Algunos filtros posiblemente tengan que trabajar en entradas que ya se escaparon o son seguras, por ejemplo, al agregar etiquetas HTML (seguras) inicialmente inseguras para salida. En tal caso, establece la opción pre_escape para escapar los datos entrantes antes de pasarlos por tu filtro:

$filter = new Twig_SimpleFilter('somefilter', 'somefilter', array('pre_escape' => 'html', 'is_safe' => array('html')));

Filtros dinámicos

Un nombre de filtro que contiene el carácter especial * es un filtro dinámico debido a que el * puede ser cualquier cadena:

$filter = new Twig_SimpleFilter('*_path', function ($name, $arguments) {
    // ...
});

Los siguientes filtros deben corresponder con el filtro dinámico definido anteriormente:

  • product_path
  • category_path

Un filtro dinámico puede definir más de una parte dinámica:

$filter = new Twig_SimpleFilter('*_path', function ($name, $suffix, $arguments) {
    // ...
});

El filtro recibirá todas las partes dinámicas de los valores antes de los argumentos de los filtros normales, pero después del entorno y el contexto. Por ejemplo, una llamada a 'foo'|a_path_b() resultará en que se pasarán los siguientes argumentos al filtro: ('a', 'b', 'foo').

Funciones

Las funciones están definidas exactamente de la misma manera que los filtros, pero necesitas crear una instancia de Twig_SimpleFunction:

$twig = new Twig_Environment($loader);
$function = new Twig_SimpleFunction('function_name', function () {
    // ...
});
$twig->addFunction($function);

Las funciones apoyan las mismas características que los filtros, excepto por las opciones pre_escapr y preserves_safety.

Pruebas

Las pruebas se definen exactamente de la misma manera que los filtros y funciones, pero necesitas crear una instancia de Twig_SimpleTest:

$twig = new Twig_Environment($loader);
$test = new Twig_SimpleTest('test_name', function () {
    // ...
});
$twig->addTest($test);

Las pruebas no apoyan algunas opciones.

Etiquetas

Una de las características más interesantes de un motor de plantillas como Twig es la posibilidad de definir nuevas construcciones del lenguaje. Esta también es la característica más compleja que necesitas comprender de cómo trabaja Twig internamente.

Vamos a crear una simple etiqueta set que te permita definir variables simples dentro de una plantilla. Puedes utilizar la etiqueta de la siguiente manera:

{% set name = "value" %}

{{ name }}

{# debe producir value #}

Nota

La etiqueta set es parte de la extensión core y como tal siempre está disponible. La versión integrada es un poco más potente y de manera predeterminada es compatible con múltiples asignaciones (consulta el capítulo Twig para diseñadores de plantillas para más información).

para definir una nueva etiqueta son necesarios tres pasos:

  • Definir una clase para analizar segmentos (responsable de analizar el código de la plantilla);
  • Definir una clase Nodo (responsable de convertir el código analizado a PHP);
  • Registrar la etiqueta.

Registrando una nueva etiqueta

Agregar una etiqueta es tan simple como una llamada al método addTokenParser en la instancia de Twig_Environment:

$twig = new Twig_Environment($loader);
$twig->addTokenParser(new Project_Set_TokenParser());

Definiendo un analizador de fragmentos

Ahora, vamos a ver el código real de esta clase:

class Project_Set_TokenParser extends Twig_TokenParser
{
    public function parse(Twig_Token $token)
    {
        $lineno = $token->getLine();
        $name = $this->parser
                      ->getStream()
                      ->expect(Twig_Token::NAME_TYPE)
                      ->getValue();
        $this->parser->getExpressionParser()
                     ->expect(Twig_Token::OPERATOR_TYPE, '=');
        $value = $this->parser
                      ->getExpressionParser()
                      ->parseExpression();

        $this->parser->getStream()
                     ->expect(Twig_Token::BLOCK_END_TYPE);

        return new Project_Set_Node(  $name,
                                      $value,
                                      $lineno,
                                      $this->getTag()
                                   );
    }

    public function getTag()
    {
        return 'set';
    }
}

El método getTag() debe devolver la etiqueta que queremos analizar, aquí set.

El método parse() se invoca cada vez que el analizador encuentra una etiqueta set. Este debe devolver una instancia de Twig_Node que representa el nodo (la llamada para la creación del Project_Set_Node se explica en la siguiente sección).

El proceso de análisis se simplifica gracias a un montón de métodos que se pueden llamar desde el fragmento del flujo ($this->parser->getStream()):

  • getCurrent(): Obtiene el segmento actual del flujo.
  • next(): Mueve al siguiente segmento en la secuencia, pero devuelve el antiguo.
  • test($type), test($value) o test($type, $value): Determina si el segmento actual es de un tipo o valor particular (o ambos). El valor puede ser un arreglo de varios posibles valores.
  • expect($type[, $value[, $message]]): Si el segmento actual no es del tipo/valor dado lanza un error de sintaxis. De lo contrario, si el tipo y valor son correctos, devuelve el segmento y mueve el flujo al siguiente segmento.
  • look(): Busca el siguiente segmento sin consumirlo.

Las expresiones de análisis se llevan a cabo llamando a parseExpression() como lo hicimos para la etiqueta set.

Truco

Leer las clases TokenParser existentes es la mejor manera de aprender todos los detalles esenciales del proceso de análisis.

Definiendo un nodo

La clase Project_Set_Node en sí misma es bastante simple:

class Project_Set_Node extends Twig_Node
{
    public function __construct(  $name,
                                  Twig_Node_Expression $value,
                                  $lineno,
                                  $tag = null
                               )
    {
        parent::__construct(  array( 'value' => $value ),
                              array( 'name'  => $name  ),
                              $lineno,
                              $tag
                           );
    }

    public function compile(Twig_Compiler $compiler)
    {
        $compiler
            ->addDebugInfo($this)
            ->write('$context[\''.$this->getAttribute('name').'\'] = ')
            ->subcompile($this->getNode('value'))
            ->raw(";\n")
        ;
    }
}

El compilador implementa una interfaz fluida y proporciona métodos que ayudan a los desarrolladores a generar código PHP hermoso y fácil de leer:

  • subcompile(): Compila un nodo.
  • raw(): Escribe tal cual la cadena dada.
  • write(): Escribe la cadena dada añadiendo sangría al principio de cada línea.
  • string(): Escribe una cadena entre comillas.
  • repr(): Escribe una representación PHP de un valor dado (consulta Twig_Node_For para un ejemplo real).
  • addDebugInfo(): Agrega como comentario la línea del archivo de plantilla original relacionado con el nodo actual.
  • indent(): Aplica sangrías al código generado (consulta Twig_Node_Block para un ejemplo real).
  • outdent(): Quita la sangría al código generado (consulta Twig_Node_Block para un ejemplo real).

Creando una extensión

La principal motivación para escribir una extensión es mover el código usado frecuentemente a una clase reutilizable como agregar apoyo para la internacionalización. Una extensión puede definir etiquetas, filtros, pruebas, operadores, variables globales, funciones y visitantes de nodo.

La creación de una extensión también hace una mejor separación del código que se ejecuta en tiempo de compilación y el código necesario en tiempo de ejecución. Por lo tanto, hace que tu código sea más rápido.

La mayoría de las veces, es útil crear una extensión para tu proyecto, para acoger todas las etiquetas y filtros específicos que deseas agregar a Twig.

Truco

Al empacar tu código en una extensión, Twig es lo suficientemente inteligente como para volver a compilar tus plantillas cada vez que les hagas algún cambio (cuando auto_reload está habilitado).

Nota

Antes de escribir tus propias extensiones, échale un vistazo al repositorio de extensiones oficial de Twig: http://github.com/fabpot/Twig-extensions.

Una extensión es una clase que implementa la siguiente interfaz:

interface Twig_ExtensionInterface
{
    /**
     * Inicia el entorno en tiempo de ejecución.
     *
     * Aquí es donde puedes cargar algún archivo que contenga funciones
     * de filtro, por ejemplo.
     *
     * @param Twig_Environment $environment La instancia actual de
     *                                      Twig_Environment
     */
    function initRuntime(Twig_Environment $environment);

    /**
     * Devuelve instancias del analizador de segmentos para añadirlos a
     * la lista existente.
     *
     * @return array Un arreglo de instancias Twig_TokenParserInterface
     *               o Twig_TokenParserBrokerInterface
     */
    function getTokenParsers();

    /**
     * Devuelve instancias del visitante de nodos para añadirlas a la
     * lista existente.
     *
     * @return array Un arreglo de instancias de
     *                Twig_NodeVisitorInterface
     */
    function getNodeVisitors();

    /**
     * Devuelve una lista de filtros para añadirla a la lista
     * existente.
     *
     * @return array Un arreglo de filtros
     */
    function getFilters();

    /**
     * Devuelve una lista de pruebas para añadirla a la lista
     *  existente.
     *
     * @return array Un arreglo de pruebas
     */
    function getTests();

    /**
     * Devuelve una lista de funciones para añadirla a la lista
     * existente.
     *
     * @return array Un arreglo de funciones
     */
    function getFunctions();

    /**
     * Devuelve una lista de operadores para añadirla a la lista
     * existente.
     *
     * @return array Un arreglo de operadores
     */
    function getOperators();

    /**
     * Devuelve una lista de variables globales para añadirla a la
     * lista existente.
     *
     * @return array Un arreglo de variables globales
     */
    function getGlobals();

    /**
     * Devuelve el nombre de la extensión.
     *
     * @return string El nombre de la extensión
     */
    function getName();
}

Para mantener tu clase de extensión limpia y ordenada, puedes heredar de la clase Twig_Extension incorporada en lugar de implementar toda la interfaz. De esta forma, sólo tienes que implementar el método getName() como el que proporcionan las implementaciones vacías de Twig_Extension para todos los otros métodos.

El método getName() debe devolver un identificador único para tu extensión.

Ahora, con esta información en mente, vamos a crear la extensión más básica posible:

class Project_Twig_Extension extends Twig_Extension
{
    public function getName()
    {
        return 'project';
    }
}

Nota

Por supuesto, esta extensión no hace nada por ahora. Vamos a personalizarla en las siguientes secciones.

A Twig no le importa dónde guardas tu extensión en el sistema de archivos, puesto que todas las extensiones se deben registrar explícitamente para estar disponibles en tus plantillas.

Puedes registrar una extensión con el método addExtension() en tu objeto Environment principal:

$twig = new Twig_Environment($loader);
$twig->addExtension(new Project_Twig_Extension());

Por supuesto, tienes que cargar primero el archivo de la extensión, ya sea utilizando require_once() o con un cargador automático (consulta la sección spl_autoload_register()).

Truco

Las extensiones integradas son grandes ejemplos de cómo trabajan las extensiones.

Globales

Puedes registrar las variables globales en una extensión vía el método getGlobals():

class Project_Twig_Extension extends Twig_Extension
{
    public function getGlobals()
    {
        return array(
            'text' => new Text(),
        );
    }

    // ...
}

Funciones

Puedes registrar funciones en una extensión vía el método getFunctions():

class Project_Twig_Extension extends Twig_Extension
{
    public function getFunctions()
    {
        return array(
            new Twig_SimpleFunction('lipsum', 'generate_lipsum'),
        );
    }

    // ...
}

Filtros

Para agregar un filtro a una extensión, es necesario sustituir el método getFilters(). Este método debe devolver un arreglo de filtros para añadir al entorno Twig:

class Project_Twig_Extension extends Twig_Extension
{
    public function getFilters()
    {
        return array(
            new Twig_SimpleFilter('rot13', 'str_rot13'),
        );
    }

    // ...
}

Etiquetas

Puedes agregar una etiqueta en una extensión reemplazando el método getTokenParsers(). Este método debe devolver un arreglo de etiquetas para añadir al entorno Twig:

class Project_Twig_Extension extends Twig_Extension
{
    public function getTokenParsers()
    {
        return array(new Project_Set_TokenParser());
    }

    // ...
}

En el código anterior, hemos añadido una sola etiqueta nueva, definida por la clase Project_Set_TokenParser. La clase Project_Set_TokenParser es responsable de analizar la etiqueta y compilarla a PHP.

Operadores

El método getOperators() te permite añadir nuevos operadores. Aquí tienes cómo añadir los operadores !, || y &&:

class Project_Twig_Extension extends Twig_Extension
{
    public function getOperators()
    {
        return array(
            array(
                '!' => array(  'precedence' => 50,
                               'class'
                               => 'Twig_Node_Expression_Unary_Not'
                       ),
            ),
            array(
                '||' => array(  'precedence' => 10,
                                'class'
                                => 'Twig_Node_Expression_Binary_Or',
                                'associativity'
                                => Twig_ExpressionParser::OPERATOR_LEFT
                        ),
                '&&' => array(  'precedence' => 15,
                                'class'
                                => 'Twig_Node_Expression_Binary_And',
                                'associativity'
                                => Twig_ExpressionParser::OPERATOR_LEFT
                             ),
            ),
        );
    }

    // ...
}

Pruebas

El método getTests() te permite añadir funciones de prueba:

class Project_Twig_Extension extends Twig_Extension
{
    public function getTests()
    {
        return array(
            new Twig_SimpleTest('even', 'twig_test_even'),
        );
    }

    // ...
}

Sobrecargando

Para sobrecargar un filtro, prueba, operador, variable global o función existente, defínelo de nuevo tan tarde como sea posible:

$twig = new Twig_Environment($loader);
$twig->addFilter(new Twig_SimpleFilter('date', function ($timestamp, $format = 'F j, Y H:i') {
    // hace algo diferente que el filtro date integrado
}));

Aquí, se sobrecargó el filtro date integrado con uno personalizado.

Esto también trabaja con una extensión:

class MyCoreExtension extends Twig_Extension
{
    public function getFilters()
    {
        return array(
            new Twig_SimpleFilter('date', array($this, 'dateFilter')),
        );
    }

    public function dateFilter($timestamp, $format = 'F j, Y H:i')
    {
        // hace algo diferente que el filtro date integrado
    }

    public function getName()
    {
        return 'project';
    }
}

$twig = new Twig_Environment($loader);
$twig->addExtension(new MyCoreExtension());

Prudencia

Ten en cuenta que no es recomendable sobrecargar elementos integrados en Twig puesto que puede ser confuso.

Probando una extensión

Pruebas funcionales

Puedes crear pruebas funcionales para extensiones simplemente creando la siguiente estructura de archivos en tu directorio de pruebas (test):

Fixtures/
    filters/
        foo.test
        bar.test
    functions/
        foo.test
        bar.test
    tags/
        foo.test
        bar.test
IntegrationTest.php

El archivo IntegrationTest.php debe tener la siguiente apariencia:

class Project_Tests_IntegrationTest extends Twig_Test_IntegrationTestCase
{
    public function getExtensions()
    {
        return array(
            new Project_Twig_Extension1(),
            new Project_Twig_Extension2(),
        );
    }

    public function getFixturesDir()
    {
        return dirname(__FILE__).'/Fixtures/';
    }
}

Los accesorios de ejemplo se pueden encontrar dentro del directorio del repositorio de Twig tests/Twig/Fixtures.

Pruebas de nodo

Probar los visitantes de nodo puede ser complejo, así que extiende tus casos de prueba de Twig_Test_NodeTestCase. Puedes encontrar ejemplos en el directorio del repositorio de Twig tests/Twig/Node.

Bifúrcame en GitHub