Twig se puede extender de muchas maneras; puedes agregar etiquetas, filtros, tests, operadores, variables globales y funciones adicionales. Incluso puedes extender el propio parser con visitantes de nodos.
Note
La primera sección de este capítulo describe cómo extender Twig. Si quieres reutilizar tus cambios en diferentes proyectos o compartirlos con otros, entonces deberías crear una extensión como se describe en la siguiente sección.
Caution
Al extender Twig sin crear una extensión, Twig no podrá recompilar tus plantillas cuando se actualice el código PHP. Para ver tus cambios en tiempo real, desactiva el almacenamiento en caché de plantillas o empaqueta tu código en una extensión (consulta la siguiente sección de este capítulo).
Antes de extender Twig, debes comprender las diferencias entre todos los diferentes puntos de extensión posibles y cuándo usarlos.
Primero, recuerda que Twig tiene dos construcciones de lenguaje principales:
{{ }}: se utiliza para imprimir el resultado de una evaluación de expresión;{% %}: se utiliza para ejecutar declaraciones.Para entender por qué Twig expone tantos puntos de extensión, veamos cómo implementar un generador de Lorem ipsum (necesita saber la cantidad de palabras a generar).
Puedes usar 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;{{ 'some text' ~ {% lipsum 40 %} ~ 'some more text' }}
De hecho, rara vez necesitas crear etiquetas; y esa es una buena noticia porque las etiquetas son el punto de extensión más complejo.
Ahora, usemos un filtro lipsum:
{{ 40|lipsum }}
Nuevamente, funciona. Pero un filtro debería transformar el valor pasado en otra cosa. Aquí, usamos el valor para indicar la cantidad de palabras a generar (por lo tanto, 40 es un argumento del filtro, no el valor que queremos transformar).
A continuación, usemos una función lipsum:
{{ lipsum(40) }}
Ahí está. Para este ejemplo específico, la creación de una función es el punto de extensión a utilizar. Y puedes usarla en cualquier lugar donde se acepte una expresión:
{{ 'some text' ~ lipsum(40) ~ 'some more text' }}
{% set lipsum = lipsum(40) %}
Por último, también puedes usar un objeto global con un método capaz de generar texto lorem ipsum:
{{ text.lipsum(40) }}
Como regla general, usa funciones para características de uso frecuente y objetos globales para todo lo demás.
Ten en cuenta lo siguiente cuando quieras extender Twig:
| ¿Qué? | ¿Dificultad de implementación? | ¿Con qué frecuencia? | ¿Cuándo? |
|---|---|---|---|
| macro | simple | frecuente | Generación de contenido |
| global | simple | frecuente | Objeto auxiliar |
| function | simple | frecuente | Generación de contenido |
| filter | simple | frecuente | Transformación de valores |
| tag | complejo | raro | Construcción del lenguaje DSL |
| test | simple | raro | Decisión booleana |
| operator | simple | raro | Transformación de valores |
Las variables globales están disponibles en todas las plantillas y macros. Usa addGlobal() para agregar una variable global a un entorno Twig:
$twig = new \Twig\Environment($loader);
$twig->addGlobal('text', new Text());
Luego puedes usar la variable text en cualquier lugar de una plantilla:
{{ text.lipsum(40) }}
Crear un filtro consiste en asociar un nombre con un elemento invocable de PHP:
// una función anónima
$filter = new \Twig\TwigFilter('rot13', function ($string) {
return str_rot13($string);
});
// o una función PHP simple
$filter = new \Twig\TwigFilter('rot13', 'str_rot13');
// o un método estático de clase
$filter = new \Twig\TwigFilter('rot13', ['SomeClass', 'rot13Filter']);
$filter = new \Twig\TwigFilter('rot13', 'SomeClass::rot13Filter');
// o un método de clase
$filter = new \Twig\TwigFilter('rot13', [$this, 'rot13Filter']);
// el siguiente necesita una implementación en tiempo de ejecución (ver más abajo para más información)
$filter = new \Twig\TwigFilter('rot13', ['SomeClass', 'rot13Filter']);
El primer argumento pasado al constructor \Twig\TwigFilter es el nombre del filtro que usarás en las plantillas y el segundo es el elemento invocable de PHP a asociar con él.
Luego, agrega el filtro al entorno Twig:
$twig = new \Twig\Environment($loader);
$twig->addFilter($filter);
Y así es como se usa en una plantilla:
{{ 'Twig'|rot13 }}
{# mostrará Gjvt #}
Cuando Twig lo llama, el elemento invocable de PHP recibe el lado izquierdo del filtro (antes de la barra vertical |) como primer argumento y los argumentos adicionales pasados al filtro (entre paréntesis ()) como argumentos extra.
Por ejemplo, el siguiente código:
{{ 'TWIG'|lower }}
{{ now|date('d/m/Y') }}
se compila en algo como lo siguiente:
<?php echo strtolower('TWIG') ?>
<?php echo twig_date_format_filter($now, 'd/m/Y') ?>
La clase \Twig\TwigFilter toma un array de opciones como último argumento:
$filter = new \Twig\TwigFilter('rot13', 'str_rot13', $options);
Si quieres acceder al juego de caracteres predeterminado en tu filtro, establece la opción needs_charset en true; Twig pasará el juego de caracteres predeterminado como primer argumento a la llamada del filtro:
$filter = new \Twig\TwigFilter('rot13', function (string $charset, $string) {
return str_rot13($string);
}, ['needs_charset' => true]);
Si quieres acceder a la instancia del entorno actual en tu filtro, establece la opción needs_environment en true; Twig pasará el entorno actual como primer argumento a la llamada del filtro:
$filter = new \Twig\TwigFilter('rot13', function (\Twig\Environment $env, $string) {
// obtener el juego de caracteres actual, por ejemplo
$charset = $env->getCharset();
return str_rot13($string);
}, ['needs_environment' => true]);
Si quieres acceder al contexto actual en tu filtro, establece la opción needs_context en true; Twig pasará el contexto actual como primer argumento a la llamada del filtro (o el segundo si needs_environment también está establecido en true):
$filter = new \Twig\TwigFilter('rot13', function ($context, $string) {
// ...
}, ['needs_context' => true]);
$filter = new \Twig\TwigFilter('rot13', function (\Twig\Environment $env, $context, $string) {
// ...
}, ['needs_context' => true, 'needs_environment' => true]);
Si el escape automático está habilitado, la salida del filtro puede escaparse antes de imprimirse. Si tu filtro actúa como un escapador (o genera explícitamente código HTML o JavaScript), querrás que se imprima la salida sin procesar. En tal caso, establece la opción is_safe:
$filter = new \Twig\TwigFilter('nl2br', 'nl2br', ['is_safe' => ['html']]);
Algunos filtros pueden necesitar trabajar con entrada que ya está escapada o es segura, por ejemplo, cuando se agregan etiquetas HTML (seguras) a una salida originalmente insegura. En tal caso, establece la opción pre_escape para escapar los datos de entrada antes de que pasen por tu filtro:
$filter = new \Twig\TwigFilter('somefilter', 'somefilter', ['pre_escape' => 'html', 'is_safe' => ['html']]);
Cuando un filtro debe aceptar un número arbitrario de argumentos, establece la opción is_variadic en true; Twig pasará los argumentos adicionales como último argumento a la llamada del filtro en forma de array:
$filter = new \Twig\TwigFilter('thumbnail', function ($file, array $options = []) {
// ...
}, ['is_variadic' => true]);
Ten en cuenta que los argumentos con nombre pasados a un filtro variádico no pueden verificarse para validez, ya que automáticamente terminarán en el array de opciones.
Un nombre de filtro que contiene el carácter especial * es un filtro dinámico y la parte * coincidirá con cualquier cadena:
$filter = new \Twig\TwigFilter('*_path', function ($name, $arguments) {
// ...
});
Los siguientes filtros coinciden con el filtro dinámico definido anteriormente:
product_pathcategory_pathUn filtro dinámico puede definir más de una parte dinámica:
$filter = new \Twig\TwigFilter('*_path_*', function ($name, $suffix, $arguments) {
// ...
});
El filtro recibe todos los valores de las partes dinámicas antes de los argumentos normales del filtro, pero después del entorno y el contexto. Por ejemplo, una llamada a 'Paris'|a_path_b() dará como resultado que se pasen los siguientes argumentos al filtro: ('a', 'b', 'Paris').
Note
La opción deprecation_info se agregó en Twig 3.15.
Puedes marcar un filtro como obsoleto estableciendo la opción deprecation_info:
$filter = new \Twig\TwigFilter('obsolete', function () {
// ...
}, ['deprecation_info' => new DeprecatedCallableInfo('twig/twig', '3.11', 'new_one')]);
El constructor DeprecatedCallableInfo toma los siguientes parámetros:
Opcionalmente, también puedes proporcionar los siguientes parámetros sobre una alternativa:
Cuando un filtro es obsoleto, Twig emite un aviso de obsolescencia al compilar una plantilla que lo usa. Consulta Recetas para más información.
Note
Antes de Twig 3.15, puedes marcar un filtro como obsoleto estableciendo la opción deprecated en true. También puedes dar un filtro alternativo que reemplace al obsoleto cuando tenga sentido:
$filter = new \Twig\TwigFilter('obsolete', function () {
// ...
}, ['deprecated' => true, 'alternative' => 'new_one']);
Note
La opción deprecating_package se agregó en Twig 3.11.
También puedes establecer la opción deprecating_package para especificar el paquete que está marcando el filtro como obsoleto, y deprecated se puede establecer en la versión del paquete cuando el filtro se volvió obsoleto:
$filter = new \Twig\TwigFilter('obsolete', function () {
// ...
}, ['deprecated' => '1.1', 'deprecating_package' => 'twig/some-package']);
Las funciones se definen exactamente de la misma manera que los filtros, pero necesitas crear una instancia de \Twig\TwigFunction:
$twig = new \Twig\Environment($loader);
$function = new \Twig\TwigFunction('function_name', function () {
// ...
});
$twig->addFunction($function);
Las funciones admiten las mismas características que los filtros, excepto las opciones pre_escape y preserves_safety.
Los tests se definen exactamente de la misma manera que los filtros y funciones, pero necesitas crear una instancia de \Twig\TwigTest:
$twig = new \Twig\Environment($loader);
$test = new \Twig\TwigTest('test_name', function () {
// ...
});
$twig->addTest($test);
Los tests te permiten crear lógica específica de la aplicación para evaluar condiciones booleanas. Como ejemplo simple, creemos un test de Twig que verifique si los objetos son 'rojos':
$twig = new \Twig\Environment($loader);
$test = new \Twig\TwigTest('red', function ($value) {
if (isset($value->color) && $value->color == 'red') {
return true;
}
if (isset($value->paint) && $value->paint == 'red') {
return true;
}
return false;
});
$twig->addTest($test);
Las funciones de test siempre deben devolver true/false.
Al crear tests, puedes usar la opción node_class para proporcionar una compilación de test personalizada. Esto es útil si tu test se puede compilar en primitivas PHP. Esto es utilizado por muchos de los tests integrados en Twig:
namespace App;
use Twig\Environment;
use Twig\Node\Expression\TestExpression;
use Twig\TwigTest;
$twig = new Environment($loader);
$test = new TwigTest(
'odd',
null,
['node_class' => OddTestExpression::class]);
$twig->addTest($test);
class OddTestExpression extends TestExpression
{
public function compile(\Twig\Compiler $compiler)
{
$compiler
->raw('(')
->subcompile($this->getNode('node'))
->raw(' % 2 != 0')
->raw(')')
;
}
}
El ejemplo anterior muestra cómo puedes crear tests que usan una clase de nodo. La clase de nodo tiene acceso a un subnodo llamado node. Este subnodo contiene el valor que se está probando. Cuando el filtro odd se usa en código como:
{% if my_value is odd %}
El subnodo node contendrá una expresión de my_value. Los tests basados en nodos también tienen acceso al nodo arguments. Este nodo contendrá los otros diversos argumentos que se han proporcionado a tu test.
Si quieres pasar un número variable de argumentos posicionales o con nombre al test, establece la opción is_variadic en true. Los tests admiten nombres dinámicos (consulta los filtros dinámicos para la sintaxis).
Una de las características más emocionantes de un motor de plantillas como Twig es la posibilidad de definir nuevas construcciones de lenguaje. Esta es también la característica más compleja, ya que necesitas entender cómo funcionan las partes internas de Twig.
Sin embargo, la mayoría de las veces no se necesita una etiqueta:
Si tu etiqueta genera alguna salida, usa una función en su lugar.
Si tu etiqueta modifica algún contenido y lo devuelve, usa un filtro en su lugar.
Por ejemplo, si quieres crear una etiqueta que convierta texto formateado en Markdown a HTML, crea un filtro markdown en su lugar:
{{ '**markdown** text'|markdown }}
Si quieres usar este filtro en grandes cantidades de texto, envuélvelo con la etiqueta apply:
{% apply markdown %}
Title
=====
Much better than creating a tag as you can **compose** filters.
{% endapply %}
Si tu etiqueta no genera nada, sino que solo existe por un efecto secundario, crea una función que no devuelva nada y llámala mediante la etiqueta do.
Por ejemplo, si quieres crear una etiqueta que registre texto, crea una función log en su lugar y llámala mediante la etiqueta do:
{% do log('Log some things') %}
Si aún quieres crear una etiqueta para una nueva construcción de lenguaje, ¡genial!
Vamos a crear una etiqueta set que permita la definición de variables simples desde dentro de una plantilla. La etiqueta se puede usar de la siguiente manera:
{% set name = "value" %}
{{ name }}
{# debería mostrar value #}
Note
La etiqueta set es parte de la extensión Core y, como tal, siempre está disponible. La versión integrada es ligeramente más poderosa y admite múltiples asignaciones de forma predeterminada.
Se necesitan tres pasos para definir una nueva etiqueta:
Agrega una etiqueta llamando al método addTokenParser en la instancia \Twig\Environment:
$twig = new \Twig\Environment($loader);
$twig->addTokenParser(new CustomSetTokenParser());
Ahora, veamos el código real de esta clase:
class CustomSetTokenParser extends \Twig\TokenParser\AbstractTokenParser
{
public function parse(\Twig\Token $token)
{
$parser = $this->parser;
$lineno = $token->getLine();
$stream = $parser->getStream();
$name = $stream->expect(\Twig\Token::NAME_TYPE)->getValue();
$stream->expect(\Twig\Token::OPERATOR_TYPE, '=');
$value = $parser->getExpressionParser()->parseExpression();
$stream->expect(\Twig\Token::BLOCK_END_TYPE);
return new CustomSetNode($name, $value, $lineno);
}
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. Debe devolver una instancia \Twig\Node\Node que represente el nodo (la creación de CustomSetNode se explica en la siguiente sección).
El proceso de análisis se simplifica gracias a un montón de métodos que puedes llamar desde el flujo de tokens ($this->parser->getStream()):
getCurrent(): Obtiene el token actual en el flujo.next(): Se mueve al siguiente token en el flujo, pero devuelve el anterior.test($type), test($value) o test($type, $value): Determina si el token actual es de un tipo o valor particular (o ambos). El valor puede ser un array de varios valores posibles.expect($type[, $value[, $message]]): Si el token actual no es del tipo/valor dado, se lanza un error de sintaxis. De lo contrario, si el tipo y el valor son correctos, se devuelve el token y el flujo se mueve al siguiente token.look(): Mira el siguiente token sin consumirlo.El análisis de expresiones se realiza llamando a parseExpression() como lo hicimos para la etiqueta set.
Al encontrar un error de sintaxis durante el análisis, lanza una excepción:
throw new SyntaxError('Some error message.', $stream->getCurrent()->getLine(), $stream->getSourceContext());
Para una mejor informes de errores al usuario, sigue estas recomendaciones:
- Usa
\Twig\Error\SyntaxError;- Siempre pasa el número de línea del nodo y el contexto de origen;
- Termina el mensaje de excepción con un punto.
[!TIP] Leer las clases
TokenParserexistentes es la mejor manera de aprender todos los detalles del proceso de análisis.
La clase CustomSetNode en sí es bastante corta:
class CustomSetNode extends \Twig\Node\Node
{
public function __construct($name, \Twig\Node\Expression\AbstractExpression $value, $line)
{
parent::__construct(['value' => $value], ['name' => $name], $line);
}
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 al desarrollador a generar código PHP hermoso y legible:
subcompile(): Compila un nodo.raw(): Escribe la cadena dada tal cual.write(): Escribe la cadena dada agregando 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\ForNode para un ejemplo de uso).addDebugInfo(): Agrega la línea del archivo de plantilla original relacionado con el nodo actual como comentario. Es muy recomendable llamar a este método al implementar nodos personalizados.indent(): Sangra el código generado (consulta \Twig\Node\BlockNode para un ejemplo de uso).outdent(): Quita la sangría del código generado (consulta \Twig\Node\BlockNode para un ejemplo de uso).Para nodos estructurales, siempre llama a addDebugInfo() al principio del proceso de compilación para mejorar la informes de errores al usuario en caso de que el código lance una excepción.
La motivación principal para escribir una extensión es mover código de uso frecuente a una clase reutilizable, como agregar soporte para internacionalización. Una extensión puede definir etiquetas, filtros, tests, operadores, funciones y visitantes de nodos.
La mayoría de las veces, es útil crear una sola extensión para tu proyecto, para albergar todas las etiquetas y filtros específicos que quieres agregar a Twig.
Tip
Al empaquetar tu código en una extensión, Twig es lo suficientemente inteligente como para recompilar tus plantillas cada vez que realizas un cambio en ella (cuando auto_reload está habilitado).
Una extensión es una clase que implementa la siguiente interfaz:
interface \Twig\Extension\ExtensionInterface
{
/**
* Returns the token parser instances to add to the existing list.
*
* @return \Twig\TokenParser\TokenParserInterface[]
*/
public function getTokenParsers();
/**
* Returns the node visitor instances to add to the existing list.
*
* @return \Twig\NodeVisitor\NodeVisitorInterface[]
*/
public function getNodeVisitors();
/**
* Returns a list of filters to add to the existing list.
*
* @return \Twig\TwigFilter[]
*/
public function getFilters();
/**
* Returns a list of tests to add to the existing list.
*
* @return \Twig\TwigTest[]
*/
public function getTests();
/**
* Returns a list of functions to add to the existing list.
*
* @return \Twig\TwigFunction[]
*/
public function getFunctions();
/**
* Returns a list of operators to add to the existing list.
*
* @return array<array> First array of unary operators, second array of binary operators
*/
public function getOperators();
}
Para mantener tu clase de extensión limpia y ligera, hereda de la clase incorporada \Twig\Extension\AbstractExtension en lugar de implementar la interfaz, ya que proporciona implementaciones vacías para todos los métodos:
class CustomTwigExtension extends \Twig\Extension\AbstractExtension
{
}
Esta extensión no hace nada por ahora. La personalizaremos en las siguientes secciones.
Puedes guardar tu extensión en cualquier lugar del sistema de archivos, ya que todas las extensiones deben registrarse explícitamente para estar disponibles en tus plantillas.
Puedes registrar una extensión usando el método addExtension() en tu objeto Environment principal:
$twig = new \Twig\Environment($loader);
$twig->addExtension(new CustomTwigExtension());
Tip
Las extensiones principales de Twig son grandes ejemplos de cómo funcionan las extensiones.
Las variables globales se pueden registrar en una extensión mediante el método getGlobals():
class CustomTwigExtension extends \Twig\Extension\AbstractExtension implements \Twig\Extension\GlobalsInterface
{
public function getGlobals(): array
{
return [
'text' => new Text(),
];
}
// ...
}
Caution
Las globales se obtienen una vez de las extensiones y luego se almacenan en caché durante la vida útil del entorno Twig. Significa que las globales no deben usarse para almacenar valores que puedan cambiar durante la vida útil del entorno Twig. Por ejemplo, si estás utilizando un servidor de aplicaciones como RoadRunner o FrankenPHP, no debes almacenar valores relacionados con el contexto actual (como la solicitud HTTP). Si lo haces, no olvides restablecer la caché entre solicitudes llamando a Environment::resetGlobals().
Las funciones se pueden registrar en una extensión mediante el método getFunctions():
class CustomTwigExtension extends \Twig\Extension\AbstractExtension
{
public function getFunctions()
{
return [
new \Twig\TwigFunction('lipsum', 'generate_lipsum'),
];
}
// ...
}
Para agregar un filtro a una extensión, necesitas anular el método getFilters(). Este método debe devolver un array de filtros para agregar al entorno Twig:
class CustomTwigExtension extends \Twig\Extension\AbstractExtension
{
public function getFilters()
{
return [
new \Twig\TwigFilter('rot13', 'str_rot13'),
];
}
// ...
}
Agregar una etiqueta en una extensión se puede hacer anulando el método getTokenParsers(). Este método debe devolver un array de etiquetas para agregar al entorno Twig:
class CustomTwigExtension extends \Twig\Extension\AbstractExtension
{
public function getTokenParsers()
{
return [new CustomSetTokenParser()];
}
// ...
}
En el código anterior, hemos agregado una sola etiqueta nueva, definida por la clase CustomSetTokenParser. La clase CustomSetTokenParser es responsable de analizar la etiqueta y compilarla a PHP.
El método getOperators() te permite agregar nuevos operadores. Para implementar uno nuevo, echa un vistazo a los operadores predeterminados proporcionados por Twig\Extension\CoreExtension.
El método getTests() te permite agregar nuevas funciones de test:
class CustomTwigExtension extends \Twig\Extension\AbstractExtension
{
public function getTests()
{
return [
new \Twig\TwigTest('even', 'twig_test_even'),
];
}
// ...
}
Note
Las clases de atributos se agregaron en Twig 3.21.
Puedes agregar los atributos #[AsTwigFilter], #[AsTwigFunction] y #[AsTwigTest] a métodos públicos de cualquier clase para definir filtros, funciones y tests.
Crea una clase usando estos atributos:
use Twig\Attribute\AsTwigFilter;
use Twig\Attribute\AsTwigFunction;
use Twig\Attribute\AsTwigTest;
class ProjectExtension
{
#[AsTwigFilter('rot13')]
public static function rot13(string $string): string
{
// ...
}
#[AsTwigFunction('lipsum')]
public static function lipsum(int $count): string
{
// ...
}
#[AsTwigTest('even')]
public static function isEven(int $number): bool
{
// ...
}
}
Luego registra la Twig\Extension\AttributeExtension con el nombre de la clase:
$twig = new \Twig\Environment($loader);
$twig->addExtension(new \Twig\Extension\AttributeExtension(ProjectExtension::class));
Si todos los métodos son estáticos, ya está listo. La clase ProjectExtension nunca se instanciará y los atributos de clase se escanearán solo cuando se compile una plantilla.
De lo contrario, si algunos métodos no son estáticos, necesitas registrar la clase como una extensión en tiempo de ejecución usando uno de los cargadores de tiempo de ejecución:
use Twig\Attribute\AsTwigFunction;
class ProjectExtension
{
// Inyectar dependencias hipotéticas
public function __construct(private LipsumProvider $lipsumProvider) {}
#[AsTwigFunction('lipsum')]
public function lipsum(int $count): string
{
return $this->lipsumProvider->lipsum($count);
}
}
$twig = new \Twig\Environment($loader);
$twig->addExtension(new \Twig\Extension\AttributeExtension(ProjectExtension::class);
$twig->addRuntimeLoader(new \Twig\RuntimeLoader\FactoryLoader([
ProjectExtension::class => function () use ($lipsumProvider) {
return new ProjectExtension($lipsumProvider);
},
]));
Si quieres acceder a la instancia del entorno actual en tu filtro o función, agrega el tipo Twig\Environment al primer argumento del método:
class ProjectExtension
{
#[AsTwigFunction('lipsum')]
public function lipsum(\Twig\Environment $env, int $count): string
{
// ...
}
}
#[AsTwigFilter] y #[AsTwigFunction] admiten argumentos variádicos automáticamente cuando se aplican a métodos variádicos:
class ProjectExtension
{
#[AsTwigFilter('thumbnail')]
public function thumbnail(string $file, mixed ...$options): string
{
// ...
}
}
Los atributos admiten otras opciones utilizadas para configurar los elementos invocables de Twig:
AsTwigFilter:needsCharset,needsEnvironment,needsContext,isSafe,isSafeCallback,preEscape,preservesSafety,deprecationInfoAsTwigFunction:needsCharset,needsEnvironment,needsContext,isSafe,isSafeCallback,deprecationInfoAsTwigTest:needsCharset,needsEnvironment,needsContext,deprecationInfo
Las implementaciones en tiempo de ejecución de filtros, funciones y tests de Twig se pueden definir como cualquier elemento invocable de PHP válido:
La forma más simple de usar métodos es definirlos en la propia extensión:
class CustomTwigExtension extends \Twig\Extension\AbstractExtension
{
private $rot13Provider;
public function __construct($rot13Provider)
{
$this->rot13Provider = $rot13Provider;
}
public function getFunctions()
{
return [
new \Twig\TwigFunction('rot13', [$this, 'rot13']),
];
}
public function rot13($value)
{
return $this->rot13Provider->rot13($value);
}
}
Esto es muy conveniente pero no recomendado, ya que hace que la compilación de plantillas dependa de dependencias de tiempo de ejecución incluso si no se necesitan (piensa, por ejemplo, en una dependencia que se conecta a un motor de base de datos).
Puedes desacoplar las definiciones de extensión de sus implementaciones en tiempo de ejecución registrando una instancia \Twig\RuntimeLoader\RuntimeLoaderInterface en el entorno que sepa cómo instanciar dichas clases de tiempo de ejecución (las clases de tiempo de ejecución deben ser autocargables):
class RuntimeLoader implements \Twig\RuntimeLoader\RuntimeLoaderInterface
{
public function load($class)
{
// implementar la lógica para crear una instancia de $class
// e inyectar sus dependencias
// la mayoría de las veces, significa usar tu contenedor de inyección de dependencias
if ('CustomTwigRuntime' === $class) {
return new $class(new Rot13Provider());
} else {
// ...
}
}
}
$twig->addRuntimeLoader(new RuntimeLoader());
Note
Twig viene con un cargador de tiempo de ejecución compatible con PSR-11 (\Twig\RuntimeLoader\ContainerRuntimeLoader).
Ahora es posible mover la lógica de tiempo de ejecución a una nueva clase CustomTwigRuntime y usarla directamente en la extensión:
class CustomTwigRuntime
{
private $rot13Provider;
public function __construct($rot13Provider)
{
$this->rot13Provider = $rot13Provider;
}
public function rot13($value)
{
return $this->rot13Provider->rot13($value);
}
}
class CustomTwigExtension extends \Twig\Extension\AbstractExtension
{
public function getFunctions()
{
return [
new \Twig\TwigFunction('rot13', ['CustomTwigRuntime', 'rot13']),
// o
new \Twig\TwigFunction('rot13', 'CustomTwigRuntime::rot13'),
];
}
}
Note
La clase de extensión debe implementar la interfaz Twig\Extension\LastModifiedExtensionInterface para invalidar la caché de plantillas cuando se modifica la clase de tiempo de ejecución. La clase AbstractExtension implementa esta interfaz y rastrea la clase de tiempo de ejecución si su nombre es el mismo que el de la clase de extensión pero termina con Runtime en lugar de Extension.
Puedes crear tests funcionales para extensiones creando la siguiente estructura de archivos en tu directorio de tests:
Fixtures/
filters/
lower.test
upper.test
functions/
date.test
format.test
tags/
for.test
if.test
IntegrationTest.php
El archivo IntegrationTest.php debería verse así:
namespace Project\Tests;
use Twig\Test\IntegrationTestCase;
class IntegrationTest extends IntegrationTestCase
{
public function getExtensions()
{
return [
new CustomTwigExtension1(),
new CustomTwigExtension2(),
];
}
public function getFixturesDir()
{
return __DIR__.'/Fixtures/';
}
}
Puedes encontrar ejemplos de fixtures en el directorio tests/Twig/Fixtures del repositorio de Twig.
Probar los visitantes de nodos puede ser complejo, así que extiende tus casos de prueba desde \Twig\Test\NodeTestCase. Puedes encontrar ejemplos en el directorio tests/Twig/Node del repositorio de Twig.