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:
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 |
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) }}
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);
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));
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));
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')));
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:
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').
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.
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.
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:
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());
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()):
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.
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:
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.
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(),
);
}
// ...
}
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'),
);
}
// ...
}
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'),
);
}
// ...
}
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.
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
),
),
);
}
// ...
}
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'),
);
}
// ...
}
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.
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.
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.