Creación de etiquetas personalizadas
Esta página ofrece una guía completa para crear etiquetas propias en Latte. Cubriremos desde etiquetas sencillas hasta escenarios más complejos, con contenido anidado y necesidades de análisis específicas, apoyándonos en lo que ya sabe sobre cómo compila Latte las plantillas.
Las etiquetas personalizadas ofrecen el máximo control sobre la sintaxis de las plantillas y la lógica de renderizado, pero son también el punto de extensión más complejo. Antes de decidirse a crear una etiqueta propia, valore siempre si existe una solución más sencilla o si ya hay una etiqueta adecuada en el conjunto estándar. Use etiquetas personalizadas solo cuando las alternativas más simples no basten para sus necesidades.
Entender el proceso de compilación
Para crear etiquetas propias con eficacia conviene explicar cómo procesa Latte las plantillas. Entender este proceso aclara por qué las etiquetas están estructuradas como están y cómo encajan en el conjunto.
La compilación de una plantilla en Latte comprende, de forma simplificada, estos pasos clave:
- Análisis léxico: el lexer lee el código fuente de la plantilla (el archivo
.latte) y lo descompone en una secuencia de piezas pequeñas y bien diferenciadas llamadas tokens (por ejemplo,{,foreach,$variable,}, texto HTML, etc.). - Análisis sintáctico: el parser toma ese flujo de tokens y construye una estructura de árbol con sentido que representa la lógica y el contenido de la plantilla. Ese árbol es el árbol de sintaxis abstracta (AST).
- Pases del compilador: antes de generar el código PHP, Latte ejecuta los pases del compilador. Son funciones que recorren todo el AST y pueden modificarlo o recopilar información. Este paso es crucial para funciones como la seguridad (Sandbox) o las optimizaciones.
- Generación del código: por último, el compilador recorre el AST (posiblemente modificado) y genera el código de la clase PHP correspondiente. Ese código PHP es el que realmente renderiza la plantilla al ejecutarse.
- Caché: el código PHP generado se guarda en caché en disco, lo que hace muy rápidos los renderizados siguientes, porque se saltan los pasos 1 a 4.
En realidad, la compilación es algo más complicada. Latte tiene dos lexers y parsers: uno para la plantilla HTML y otro para el código con aspecto de PHP que hay dentro de las etiquetas. Además, el análisis sintáctico no se ejecuta después de la tokenización, sino que el lexer y el parser corren en paralelo en dos “hilos” y se coordinan. Créame, soy David Grudl: programar esto se sintió como ciencia espacial :-)
Todo el proceso, desde la carga del contenido de la plantilla hasta la generación del archivo resultante, pasando por el análisis, se puede secuenciar con este código, con el que puede experimentar y volcar los resultados intermedios:
$latte = new Latte\Engine;
$source = $latte->getLoader()->getContent($file);
$ast = $latte->parse($source);
$latte->applyPasses($ast);
$code = $latte->generate($ast, $file);
La anatomía de una etiqueta
Crear en Latte una etiqueta propia plenamente funcional implica varias partes interconectadas. Antes de meternos en la implementación, entendamos los conceptos y la terminología básicos, con una analogía con HTML y el Document Object Model (DOM).
Etiquetas frente a nodos (analogía con HTML)
En HTML escribimos etiquetas como <p> o <div>...</div>. Esas etiquetas son
sintaxis del código fuente. Cuando un navegador analiza ese HTML, crea en memoria una representación llamada Document Object
Model (DOM). En el DOM, las etiquetas HTML están representadas por nodos (en concreto, nodos Element en
la terminología del DOM de JavaScript). Con esos nodos interactuamos mediante programación (por ejemplo,
document.getElementById(...) en JavaScript devuelve un nodo Element). La etiqueta es solo la representación textual
en el archivo fuente; el nodo es la representación como objeto dentro del árbol lógico.
Latte funciona de forma parecida:
- En un archivo de plantilla
.latteusted escribe etiquetas de Latte, como{foreach ...}y{/foreach}. Esa es la sintaxis con la que interactúa como autor de la plantilla. - Cuando Latte analiza la plantilla, construye un árbol de sintaxis abstracta (AST). Ese árbol se compone de nodos. Cada etiqueta de Latte, elemento HTML, fragmento de texto o expresión de la plantilla se convierte en uno o varios nodos del árbol.
- La clase base de todos los nodos del AST es
Latte\Compiler\Node. Igual que el DOM tiene distintos tipos de nodo (Element, Text, Comment), el AST de Latte tiene varios tipos de nodo. Se encontrará conLatte\Compiler\Nodes\TextNodepara el texto estático,Latte\Compiler\Nodes\Html\ElementNodepara los elementos HTML,Latte\Compiler\Nodes\Php\ExpressionNodepara las expresiones dentro de las etiquetas y, lo más importante para las etiquetas propias, nodos que heredan deLatte\Compiler\Nodes\StatementNode.
¿Por qué StatementNode?
Los elementos HTML (Html\ElementNode) representan sobre todo estructura y contenido. Las expresiones de PHP
(Php\ExpressionNode) representan valores o cálculos. ¿Y qué pasa con etiquetas de Latte como {if},
{foreach} o nuestra {datetime} propia? Esas etiquetas ejecutan acciones, controlan el flujo
del programa o generan salida a partir de una lógica. Son las unidades funcionales que hacen de Latte un potente motor de
plantillas y no un simple lenguaje de marcado.
En programación, esas unidades que ejecutan acciones se suelen llamar “sentencias” (statements). Por eso, los nodos que
representan estas etiquetas funcionales de Latte suelen heredar de Latte\Compiler\Nodes\StatementNode. Esto los
distingue de los nodos puramente estructurales (como los elementos HTML) o de los que representan valores (como las
expresiones).
Los componentes clave
Repasemos los componentes principales necesarios para crear una etiqueta propia:
Función de análisis de la etiqueta
- Este callable de PHP analiza la sintaxis de la etiqueta de Latte (
{...}) en el código fuente de la plantilla. - Recibe información sobre la etiqueta (su nombre, su posición y si es un n:atributo) mediante un objeto Latte\Compiler\Tag, y el Latte\Compiler\TemplateParser principal como
segundo argumento. Su firma completa es
callable(Tag, TemplateParser): (Node|\Generator|void). - Su herramienta principal para analizar argumentos y expresiones dentro de los delimitadores de la etiqueta es el objeto Latte\Compiler\TagParser, accesible mediante
$tag->parser(es un parser distinto del que analiza toda la plantilla). - En las etiquetas pares, usa
yieldpara indicar a Latte que analice el contenido interior entre la etiqueta de apertura y la de cierre. - El objetivo último de la función de análisis es crear y devolver una instancia de la clase de nodo, que se añade al AST.
- Es costumbre (aunque no obligatorio) implementar la función de análisis como un método estático (a menudo llamado
create) directamente dentro de la clase de nodo correspondiente. Así, la lógica de análisis y la representación del nodo quedan bien agrupadas, se puede acceder a elementos privados o protegidos de la clase si hace falta y mejora la organización.
Clase de nodo
- Representa la función lógica de su etiqueta dentro del árbol de sintaxis abstracta (AST).
- Guarda la información analizada (argumentos o contenido) en propiedades públicas. Esas propiedades suelen contener otras
instancias de
Node(por ejemplo,ExpressionNodepara los argumentos analizados oAreaNodepara el contenido analizado). - El método
print(PrintContext $context): stringgenera el código PHP (una sentencia o una serie de sentencias) que ejecuta la acción de la etiqueta durante el renderizado de la plantilla. - El método
getIterator(): \Generatorhace accesibles los nodos hijos (argumentos, contenido) para que los recorran los pases del compilador. Debe devolver referencias (&) para que los pases puedan modificar o sustituir los subnodos. - Una vez analizada toda la plantilla en un AST, Latte ejecuta una serie de pases del compilador. Esos pases recorren todo el AST mediante el
método
getIterator()que proporciona cada nodo. Pueden inspeccionar nodos, recopilar información e incluso modificar el árbol (por ejemplo, cambiando las propiedades públicas de los nodos o sustituyendo nodos enteros). Este diseño, que exige ungetIterator()completo, es crucial. Permite que funciones potentes como el Sandbox analicen y, en su caso, alteren el comportamiento de cualquier parte de la plantilla, incluidas sus etiquetas propias, lo que garantiza seguridad y coherencia.
Registro mediante una extensión
- Debe informar a Latte de su nueva etiqueta y de qué función de análisis usar para ella. Esto ocurre dentro de una extensión de Latte.
- Dentro de su clase de extensión implementa el método
getTags(): array. Este método devuelve un array asociativo donde las claves son los nombres de las etiquetas (por ejemplo,'mytag','n:myattribute') y los valores son los callables de PHP que representan sus respectivas funciones de análisis (por ejemplo,MyNamespace\DatetimeNode::create(...)).
En resumen: la función de análisis de la etiqueta convierte el código fuente de la plantilla correspondiente
a su etiqueta en un nodo del AST. La clase de nodo sabe después cómo convertirse a sí misma en
código PHP ejecutable para la plantilla compilada y pone sus subnodos a disposición de los pases del compilador
mediante getIterator(). El registro mediante una extensión conecta el nombre de la etiqueta con la función
de análisis y se lo da a conocer a Latte.
Veamos ahora cómo implementar estos componentes paso a paso.
Crear una etiqueta sencilla
Metámonos en la creación de su primera etiqueta propia de Latte. Empezaremos por un ejemplo muy sencillo: una etiqueta
llamada {datetime} que imprime la fecha y la hora actuales. De entrada, esta etiqueta no aceptará ningún
argumento, pero la mejoraremos más adelante en la sección Analizar
los argumentos de una etiqueta. Tampoco tiene contenido interior.
Este ejemplo le guiará por los pasos esenciales: definir la clase de nodo, implementar sus métodos print() y
getIterator(), crear la función de análisis y, por último, registrar la etiqueta.
Objetivo: implementar {datetime} para que imprima la fecha y la hora actuales con la función
date() de PHP.
Creación de la clase de nodo
Primero necesitamos una clase que represente nuestra etiqueta en el árbol de sintaxis abstracta (AST). Como se ha comentado
antes, heredamos de Latte\Compiler\Nodes\StatementNode.
Cree un archivo (por ejemplo, DatetimeNode.php) y defina la clase:
<?php
namespace App\Templating;
use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\PrintContext;
use Latte\Compiler\Tag;
class DatetimeNode extends StatementNode
{
/**
* Tag parsing function, called when {datetime} is found.
*/
public static function create(Tag $tag): self
{
// Nuestra etiqueta produce contenido, así que conservamos la indentación circundante
$tag->outputMode = $tag::OutputKeepIndentation;
// Nuestra etiqueta simple todavía no acepta argumentos, así que no tenemos que parsear nada
$node = $tag->node = new self;
return $node;
}
/**
* Generates the PHP code that will be executed when the template is rendered.
*/
public function print(PrintContext $context): string
{
return $context->format(
'echo date(\'Y-m-d H:i:s\') %line;',
$this->position,
);
}
/**
* Provides access to child nodes for Latte's compiler passes.
*/
public function &getIterator(): \Generator
{
false && yield;
}
}
Cuando Latte encuentra {datetime} en una plantilla, llama a la función de análisis create(). Su
tarea es devolver una instancia de DatetimeNode. Además ponemos $tag->outputMode en
OutputKeepIndentation: como una etiqueta se ejecuta en el modo predeterminado OutputNone (explicado en
Modos de salida de las etiquetas), una etiqueta colocada antes del primer
texto de la plantilla podría emitir su salida en el método prepare() generado en lugar de en main().
Fijar este modo garantiza que la salida caiga donde está la etiqueta.
El método print() genera el código PHP que se ejecutará al renderizar la plantilla. Llamamos al método
$context->format(), que compone la cadena de código PHP resultante para la plantilla compilada. El primer
argumento, 'echo date('Y-m-d H:i:s') %line;', es la máscara en la que se sustituyen los parámetros siguientes. El
marcador %line indica al método format() que tome el argumento que viene a continuación, que es
$this->position, e inserte un comentario como /* pos 15:1 */ que enlaza el código PHP generado con
la línea original de la plantilla, algo crucial para depurar.
La propiedad $this->position se hereda de la clase base Node y la establece automáticamente el
parser de Latte. Contiene un objeto Latte\Compiler\Range (una subclase de
Position ampliada con una length en bytes) que indica dónde se encuentra la etiqueta en el archivo
.latte de origen. En las etiquetas pares, el rango va de la etiqueta de apertura a la de cierre, y los descendientes
de StatementNode exponen además $this->tagRanges, que lista el Range de cada etiqueta
que las compone (apertura, intermedias como {else}/{case} y cierre).
El método getIterator() es vital para los pases del compilador. Debe devolver todos los nodos hijos, pero nuestro
sencillo DatetimeNode no tiene por ahora argumentos ni contenido, y por tanto tampoco nodos hijos. Aun así, el
método debe existir y ser un generador, es decir, la palabra clave yield debe aparecer de algún modo en el cuerpo
del método.
Registro mediante una extensión
Por último, informe a Latte de la nueva etiqueta. Cree una clase de extensión (por ejemplo,
MyLatteExtension.php) y registre la etiqueta en su método getTags().
<?php
namespace App\Templating;
use Latte\Extension;
class MyLatteExtension extends Extension
{
/**
* Returns the list of tags provided by this extension.
* @return array<string, callable> Map: 'tag-name' => parsing-function
*/
public function getTags(): array
{
return [
'datetime' => DatetimeNode::create(...),
// Registre aquí más etiquetas más adelante
];
}
}
Después, registre esta extensión en el Latte Engine:
$latte = new Latte\Engine;
$latte->addExtension(new App\Templating\MyLatteExtension);
Cree la plantilla:
<p>Page generated on: {datetime}</p>
Salida esperada: <p>Page generated on: 2023-10-27 11:00:00</p>
Resumen de esta fase
Hemos creado con éxito una etiqueta propia básica, {datetime}. Hemos definido su representación en el AST
(DatetimeNode), nos hemos ocupado de su análisis (create()), hemos indicado cómo debe generar el
código PHP (print()), hemos garantizado que sus hijos sean recorribles (getIterator()) y la hemos
registrado en Latte.
En la siguiente sección mejoraremos esta etiqueta para que acepte argumentos, lo que nos mostrará cómo analizar expresiones y gestionar nodos hijos.
Analizar los argumentos de una etiqueta
Nuestra sencilla etiqueta {datetime} funciona, pero no es muy flexible. Mejorémosla para que acepte un argumento
opcional: una cadena de formato para la función date(). La sintaxis deseada será
{datetime $format}.
Objetivo: modificar {datetime} para que acepte como argumento una expresión PHP opcional, que se usará
como cadena de formato de date().
Presentación de TagParser
Antes de modificar el código conviene entender la herramienta que vamos a usar, Latte\Compiler\TagParser. Cuando el parser principal
de Latte (TemplateParser) encuentra una etiqueta como {datetime ...} o un n:atributo, delega el
análisis del contenido interior de la etiqueta (la parte entre { y }, o el valor del atributo)
en un TagParser especializado.
Este TagParser opera únicamente sobre los argumentos de la etiqueta. Su tarea es consumir los tokens que
representan esos argumentos. Y algo crucial: debe analizar todo el contenido que se le entrega. Si su función de análisis
termina y el TagParser no ha llegado al final de los argumentos (se comprueba con
$tag->parser->isEnd()), Latte lanzará una excepción, porque eso indica que han quedado tokens inesperados
dentro de la etiqueta. A la inversa, si una etiqueta requiere argumentos, debería llamar a
$tag->expectArguments() al principio de su función de análisis. Este método comprueba si hay argumentos y lanza
una excepción útil si la etiqueta se usó sin ninguno.
TagParser ofrece métodos prácticos para analizar distintos tipos de argumentos:
parseExpression(): ExpressionNode: analiza una expresión con aspecto de PHP (variables, literales, operadores, llamadas a funciones o métodos, etc.). Se ocupa del azúcar sintáctico de Latte, como tratar las cadenas alfanuméricas simples como cadenas entrecomilladas (por ejemplo,foose analiza como si fuera'foo').parseUnquotedStringOrExpression(): ExpressionNode: analiza o bien una expresión estándar, o bien una cadena sin comillas. Las cadenas sin comillas son secuencias que Latte permite sin comillas, usadas a menudo para cosas como rutas de archivo (por ejemplo,{include ../file.latte}). Si analiza una cadena sin comillas, devuelve unStringNode.parseArguments(): ArrayNode: analiza argumentos separados por comas, eventualmente con claves, como10, name: 'John', true.parseModifier(): ModifierNode: analiza filtros como|upper|truncate:10.parseType(): ?SuperiorTypeNode: analiza declaraciones de tipo de PHP, comoint,?string,array|Foo.
Para necesidades de análisis más complejas o de bajo nivel, puede interactuar directamente con el flujo de tokens mediante
$tag->parser->stream. Este objeto ofrece métodos para inspeccionar y consumir tokens sueltos:
$tag->parser->stream->is(...): bool: comprueba si el token actual coincide con alguno de los tipos indicados (por ejemplo,Token::Php_Variable) o con valores literales (por ejemplo,'as') sin consumirlo. Útil para mirar hacia delante.$tag->parser->stream->consume(...): Token: consume el token actual y avanza la posición del flujo. Si se indican tipos o valores de token esperados como argumentos y el token actual no coincide, lanza unaCompileException. Úselo cuando espere un token concreto.$tag->parser->stream->tryConsume(...): ?Token: intenta consumir el token actual solo si coincide con alguno de los tipos o valores indicados. Si coincide, lo consume y lo devuelve. Si no, deja la posición del flujo intacta y devuelvenull. Úselo para tokens opcionales o al elegir entre distintas ramas sintácticas.
Actualizar la función de análisis create()
Con esto claro, modifiquemos el método create() de DatetimeNode para analizar el argumento de
formato opcional con $tag->parser.
<?php
namespace App\Templating;
use Latte\Compiler\Nodes\Php\ExpressionNode;
use Latte\Compiler\Nodes\Php\Scalar\StringNode;
use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\PrintContext;
use Latte\Compiler\Tag;
class DatetimeNode extends StatementNode
{
// Añade una propiedad pública para guardar el nodo de la expresión de formato parseada
public ?ExpressionNode $format = null;
public static function create(Tag $tag): self
{
$node = $tag->node = new self;
// Comprueba si hay algún token
if (!$tag->parser->isEnd()) {
// Parsea el argumento como una expresión al estilo de PHP con el TagParser.
$node->format = $tag->parser->parseExpression();
}
return $node;
}
// ... los métodos print() y getIterator() se actualizarán a continuación ...
}
Hemos añadido la propiedad pública $format. En create() usamos ahora
$tag->parser->isEnd() para comprobar si hay argumentos. Si los hay,
$tag->parser->parseExpression() consume los tokens de la expresión. Como el TagParser debe
consumir todos sus tokens de entrada, Latte lanzará automáticamente un error si el usuario escribe algo inesperado después de
la expresión de formato (por ejemplo, {datetime 'Y-m-d', unexpected}).
Actualizar el método print()
Modifiquemos ahora el método print() para usar la expresión de formato analizada y guardada en
$this->format. Si no se indicó formato ($this->format es null), deberíamos usar una
cadena de formato predeterminada, por ejemplo 'Y-m-d H:i:s'.
public function print(PrintContext $context): string
{
$formatNode = $this->format ?? new StringNode('Y-m-d H:i:s');
// %node imprime la representación en código PHP de $formatNode.
return $context->format(
'echo date(%node) %line;',
$formatNode,
$this->position
);
}
En la variable $formatNode guardamos el nodo del AST que representa la cadena de formato para la función
date() de PHP. Aquí usamos el operador de fusión de nulos (??). Si el usuario indicó un argumento en
la plantilla (por ejemplo, {datetime 'd.m.Y'}), la propiedad $this->format contiene el nodo
correspondiente (en este caso, un StringNode con el valor 'd.m.Y') y se usa ese nodo. Si el usuario no
indicó ningún argumento (escribió solo {datetime}), la propiedad $this->format es
null y creamos en su lugar un nuevo StringNode con el formato predeterminado 'Y-m-d H:i:s'.
Así, $formatNode contiene siempre un nodo del AST válido para el formato.
En la máscara 'echo date(%node) %line;' se usa el nuevo marcador %node, que indica al método
format() que tome el primer argumento siguiente (que es nuestro $formatNode), llame a su método
print() (que devuelve su representación en código PHP) e inserte ese resultado en la posición del marcador.
Implementar getIterator() para los subnodos
Nuestro DatetimeNode tiene ahora un nodo hijo: la expresión $format. Debemos hacer accesible
ese nodo hijo a los pases del compilador devolviéndolo en el método getIterator(). Recuerde devolver una
referencia (&) para que los pases puedan sustituir el nodo si hace falta.
public function &getIterator(): \Generator
{
if ($this->format) {
yield $this->format;
}
}
¿Por qué es crucial? Imagine un pase del Sandbox que necesita comprobar si el argumento $format contiene una
llamada a una función prohibida (por ejemplo, {datetime dangerousFunction()}). Si getIterator() no
devuelve $this->format, el pase del Sandbox nunca vería la llamada a dangerousFunction() dentro del
argumento de nuestra etiqueta, lo que abriría un posible agujero de seguridad. Al devolverlo, permitimos que el Sandbox (y los
demás pases) inspeccionen y, en su caso, modifiquen el nodo de la expresión $format.
Usar la etiqueta mejorada
La etiqueta gestiona ahora correctamente un argumento opcional:
Default format: {datetime}
Custom format: {datetime 'd.m.Y'}
Using variable: {datetime $userDateFormatPreference}
{* Esto provocaría un error tras parsear 'd.m.Y', porque ", foo" no se espera *}
{* {datetime 'd.m.Y', foo} *}
A continuación veremos cómo crear etiquetas pares que procesan el contenido que hay entre ellas.
Etiquetas pares
Hasta ahora, nuestra etiqueta {datetime} es autocerrada (conceptualmente). No tiene contenido entre una
etiqueta de apertura y otra de cierre. Muchas etiquetas útiles, sin embargo, operan sobre un bloque de contenido de la plantilla.
Son las etiquetas pares. Ejemplos: {if}...{/if}, {block}...{/block} o la etiqueta propia que
vamos a construir ahora: {debug}...{/debug}.
Esta etiqueta nos permitirá incluir en nuestras plantillas información de depuración que solo debería verse durante el desarrollo.
Objetivo: crear una etiqueta par {debug} cuyo contenido se renderice solo si está activo un indicador de
“modo de desarrollo”.
Presentación de los proveedores
A veces, sus etiquetas necesitan acceder a datos o servicios que no se pasan directamente como parámetros de la plantilla. Por ejemplo, saber si la aplicación está en modo de desarrollo, acceder a un objeto de usuario u obtener valores de configuración. Para eso, Latte ofrece un mecanismo llamado proveedores.
Los proveedores se registran dentro de su extensión mediante el método
getProviders(). Este método devuelve un array asociativo donde las claves son los nombres con los que los
proveedores estarán accesibles en el código de ejecución de la plantilla, y los valores son los datos u objetos propiamente
dichos.
Dentro del código PHP generado por el método print() de su etiqueta puede acceder a esos proveedores mediante la
propiedad especial del objeto $this->global. Como esa propiedad se comparte entre todas las extensiones, conviene
poner un prefijo a los nombres de sus proveedores para evitar posibles colisiones con los proveedores del núcleo de Latte
o de otras extensiones de terceros. Una convención habitual es usar un prefijo corto y único relacionado con su fabricante
o con el nombre de la extensión. En nuestro ejemplo usaremos el prefijo app, y el indicador de modo de desarrollo
estará disponible como $this->global->appDevMode.
La palabra clave yield para analizar el contenido
¿Cómo le decimos al parser de Latte que procese el contenido entre {debug} y {/debug}? Aquí
entra en juego la palabra clave yield.
Cuando yield se usa en la función create(), esta se convierte en un generador de PHP. Su ejecución se pausa y el control
vuelve al TemplateParser principal. El TemplateParser continúa entonces analizando el contenido de la
plantilla hasta encontrar la etiqueta de cierre correspondiente ({/debug} en nuestro caso).
Una vez encontrada la etiqueta de cierre, el TemplateParser reanuda la ejecución de nuestra función
create() justo después de la sentencia yield. El valor devuelto por yield es un
array con dos elementos:
- Un
AreaNodeque representa el contenido analizado entre la etiqueta de apertura y la de cierre. - El objeto
Tagque representa la etiqueta de cierre (por ejemplo,{/debug}).
Creemos la clase DebugNode y su método create usando yield.
<?php
namespace App\Templating;
use Latte\Compiler\Nodes\AreaNode;
use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\PrintContext;
use Latte\Compiler\Tag;
class DebugNode extends StatementNode
{
// Propiedad pública para guardar el contenido interno parseado
public AreaNode $content;
/**
* Parsing function for the paired {debug} ... {/debug} tag.
*/
public static function create(Tag $tag): \Generator // fíjese en el tipo de retorno
{
$node = $tag->node = new self;
// Pausa el parseo y obtiene el contenido interno y la etiqueta final cuando encuentra {/debug}
[$node->content, $endTag] = yield;
return $node;
}
// ... print() y getIterator() se implementarán a continuación ...
}
Nota: $endTag es null si la etiqueta se usa como n:atributo, es decir,
<div n:debug>...</div>.
Una etiqueta par también se puede cerrar con una barra, como {debug/} (o <div n:debug/>).
Entonces no tiene contenido interior: el generador recibe [$emptyFragmentNode, $startTag], donde el segundo elemento
es la propia etiqueta de apertura, no null.
Implementar print() para el renderizado
condicional
El método print() debe generar ahora código PHP que compruebe en tiempo de ejecución el proveedor
appDevMode y ejecute el código del contenido interior solo si el indicador es verdadero.
public function print(PrintContext $context): string
{
// Genera una sentencia 'if' de PHP que comprueba el proveedor en tiempo de ejecución
return $context->format(
<<<'XX'
if ($this->global->appDevMode) %line {
// Si estamos en modo de desarrollo, imprime el contenido interno
%node
}
XX,
$this->position, // Para el comentario %line
$this->content, // El nodo que contiene el AST del contenido interno
);
}
Es sencillo. Usamos PrintContext::format() para crear una sentencia if estándar de PHP. Dentro del
if colocamos el marcador %node para $this->content. Latte llamará recursivamente a
$this->content->print($context) para generar el código PHP de la parte interior de la etiqueta, pero solo si
$this->global->appDevMode se evalúa como verdadero en tiempo de ejecución.
Implementar getIterator() para el contenido
Igual que con el nodo de argumento del ejemplo anterior, nuestro DebugNode tiene ahora un nodo hijo: el
AreaNode $content. Debemos hacerlo recorrible devolviéndolo en getIterator():
public function &getIterator(): \Generator
{
// Devuelve la referencia al nodo de contenido
yield $this->content;
}
Esto permite que los pases del compilador desciendan al contenido de nuestra etiqueta {debug}, algo importante
incluso si el contenido se renderiza de forma condicional. Por ejemplo, el Sandbox necesita analizar el contenido con
independencia de que appDevMode sea verdadero o falso.
Registro y uso
Registre la etiqueta y el proveedor en su extensión:
class MyLatteExtension extends Extension
{
// Suponemos que $isDevelopmentMode se determina en algún sitio (p. ej. desde la configuración)
public function __construct(
private bool $isDevelopmentMode,
) {
}
public function getTags(): array
{
return [
'datetime' => DatetimeNode::create(...),
'debug' => DebugNode::create(...), // Registra la nueva etiqueta
];
}
public function getProviders(): array
{
return [
'appDevMode' => $this->isDevelopmentMode, // Registra el proveedor
];
}
}
// Al registrar la extensión:
$isDev = true; // Determínelo según el entorno de su aplicación
$latte->addExtension(new MyLatteExtension($isDev));
Y úsela en una plantilla:
<p>Regular content visible always.</p>
{debug}
<div class="debug-panel">
Current user ID: {$user->id}
Request time: {=time()}
</div>
{/debug}
<p>More regular content.</p>
Integración con los n:atributos
Latte ofrece un atajo cómodo para muchas etiquetas pares: los n:atributos. Si tiene una etiqueta par como
{tag}...{/tag} y quiere que su efecto se aplique directamente a un único elemento HTML, a menudo puede escribirla de
forma más concisa como un atributo n:tag en ese elemento.
Para la mayoría de las etiquetas pares estándar que defina (como nuestra {debug}), Latte habilita
automáticamente la versión correspondiente con n:. No necesita hacer nada más al registrarla:
{* Uso estándar de la etiqueta pareada *}
{debug}<div>Debug info</div>{/debug}
{* Uso equivalente con un n:atributo *}
<div n:debug>Debug info</div>
Ambas renderizarán el <div> solo si $this->global->appDevMode es verdadero. Los prefijos
inner- y tag- también funcionan como cabe esperar.
A veces, la lógica de su etiqueta necesita comportarse de forma algo distinta según se use como etiqueta par estándar
o como n:atributo, o si se emplea un prefijo como n:inner-tag o n:tag-tag. El objeto
Latte\Compiler\Tag, que se pasa a su función de análisis create(), ofrece esa información:
$tag->isNAttribute(): bool: devuelvetruesi la etiqueta se está analizando como n:atributo$tag->prefix: ?string: devuelve el prefijo usado con el n:atributo, que puede sernull(no es un n:atributo),Tag::PrefixNone,Tag::PrefixInneroTag::PrefixTag
Ahora que entendemos las etiquetas sencillas, el análisis de argumentos, las etiquetas pares, los proveedores y los
n:atributos, abordemos un escenario más complejo, con etiquetas anidadas dentro de otras etiquetas, partiendo de nuestra etiqueta
{debug}.
Etiquetas intermedias
Algunas etiquetas pares permiten, o incluso exigen, que aparezcan otras etiquetas dentro de ellas antes de la etiqueta
de cierre final. Son las etiquetas intermedias. Ejemplos clásicos: {if}...{elseif}...{else}...{/if} o
{switch}...{case}...{default}...{/switch}.
Ampliemos nuestra etiqueta {debug} para que admita una cláusula {else} opcional, que se renderizará
cuando la aplicación no esté en modo de desarrollo.
Objetivo: modificar {debug} para que admita una etiqueta intermedia {else} opcional. La
sintaxis final debería ser {debug} ... {else} ... {/debug}.
Analizar etiquetas intermedias con yield
Ya sabemos que yield pausa la función de análisis create() y devuelve el contenido analizado junto
con la etiqueta de cierre. Pero yield ofrece más control: puede pasarle un array con los nombres de las
etiquetas intermedias. Cuando el parser encuentre alguna de esas etiquetas en el mismo nivel de anidamiento (es decir,
como hijas directas de la etiqueta padre, no dentro de otros bloques o etiquetas internos), también detendrá el análisis del
contenido.
Cuando el análisis se detiene por una etiqueta intermedia, deja de analizar el contenido, reanuda el generador
create() y le devuelve el contenido parcialmente analizado y la propia etiqueta intermedia (en lugar de la
etiqueta de cierre final). Nuestra función create() puede entonces ocuparse de esa etiqueta intermedia (por ejemplo,
analizar sus argumentos, si los tuviera) y hacer yield de nuevo para analizar la siguiente parte del contenido
hasta encontrar la etiqueta de cierre final u otra etiqueta intermedia esperada.
Modifiquemos DebugNode::create() para esperar {else}:
<?php
namespace App\Templating;
use Latte\Compiler\Nodes\AreaNode;
use Latte\Compiler\Nodes\NopNode;
use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\PrintContext;
use Latte\Compiler\Tag;
class DebugNode extends StatementNode
{
// Contenido para la parte {debug}
public AreaNode $thenContent;
// Contenido opcional para la parte {else}
public ?AreaNode $elseContent = null;
public static function create(Tag $tag): \Generator
{
$node = $tag->node = new self;
// hace yield y espera {/debug} o {else}
[$node->thenContent, $nextTag] = yield ['else'];
// Comprueba si la etiqueta en la que nos hemos detenido era {else}
if ($nextTag?->name === 'else') {
// Vuelve a hacer yield para parsear el contenido entre {else} y {/debug}
[$node->elseContent, $endTag] = yield;
}
return $node;
}
// ... print() y getIterator() se actualizarán a continuación ...
}
Ahora, yield ['else'] le dice a Latte que detenga el análisis no solo con {/debug}, sino también
con {else}. Si se encuentra {else}, $nextTag contendrá el objeto Tag de
{else}. Hacemos entonces yield de nuevo sin argumentos, lo que significa que ahora solo esperamos la
etiqueta final {/debug}, y guardamos el resultado en $node->elseContent. Si no se encontró
{else}, $nextTag sería el Tag de {/debug} (o null si se usó
como n:atributo) y $node->elseContent seguiría siendo null.
Implementar print() con {else}
El método print() debe reflejar la nueva estructura. Debe generar una sentencia if/else de PHP
basada en el proveedor appDevMode.
public function print(PrintContext $context): string
{
return $context->format(
<<<'XX'
if ($this->global->appDevMode) %line {
%node // Código para la rama 'then' (contenido de {debug})
} else {
%node // Código para la rama 'else' (contenido de {else})
}
XX,
$this->position, // Número de línea de la condición 'if'
$this->thenContent, // Primer marcador %node
$this->elseContent ?? new NopNode, // Segundo marcador %node
);
}
Es una estructura if/else estándar de PHP. Usamos %node dos veces; format() sustituye
los nodos indicados de forma secuencial. Usamos ?? new NopNode para evitar errores si
$this->elseContent es null: el NopNode simplemente no imprime nada.
Implementar getIterator() para ambos contenidos
Ahora tenemos potencialmente dos nodos de contenido hijos ($thenContent y $elseContent). Debemos
devolver ambos si existen:
public function &getIterator(): \Generator
{
yield $this->thenContent;
if ($this->elseContent) {
yield $this->elseContent;
}
}
Usar la etiqueta mejorada
La etiqueta se puede usar ahora con una cláusula {else} opcional:
{debug}
<p>Showing debug info because devMode is ON.</p>
{else}
<p>Debug info is hidden because devMode is OFF.</p>
{/debug}
Gestionar el estado y el anidamiento
Nuestros ejemplos anteriores ({datetime}, {debug}) eran relativamente sin estado dentro de sus
métodos print(). O bien imprimían contenido directamente, o bien hacían una comprobación condicional sencilla
basada en un proveedor global. Muchas etiquetas, sin embargo, necesitan gestionar alguna forma de estado durante el
renderizado, o evaluar expresiones proporcionadas por el usuario que solo deberían ejecutarse una vez por rendimiento
o corrección. Además, tenemos que pensar en qué ocurre cuando nuestras etiquetas propias están anidadas.
Ilustremos estos conceptos creando una etiqueta {repeat $count}...{/repeat}. Esta etiqueta repetirá su contenido
interior $count veces.
Objetivo: implementar {repeat $count}, que repite su contenido un número dado de veces.
La necesidad de variables temporales y únicas
Imagine que el usuario escribe:
{repeat rand(1, 5)} Content {/repeat}
Si en nuestro método print() generáramos ingenuamente un bucle for de PHP como este:
// Código generado simplificado e INCORRECTO
for ($i = 0; $i < rand(1, 5); $i++) {
// imprime el contenido
}
¡Sería un error! La expresión rand(1, 5) se reevaluaría en cada iteración del bucle, lo que daría un
número impredecible de repeticiones. Necesitamos evaluar la expresión $count una sola vez antes de que
empiece el bucle y guardar su resultado.
Generaremos código PHP que primero evalúe la expresión del contador y la guarde en una variable temporal de
ejecución. Para evitar choques con las variables definidas por el usuario de la plantilla y con las variables internas
de Latte (como $ʟ_...), usaremos la convención del prefijo $__ (doble guion bajo) para nuestras
variables temporales.
El código generado quedaría así:
$__count = rand(1, 5);
for ($__i = 0; $__i < $__count; $__i++) {
// imprime el contenido
}
Considere ahora el anidamiento:
{repeat $countA} {* Bucle exterior *}
{repeat $countB} {* Bucle interior *}
...
{/repeat}
{/repeat}
Si tanto la etiqueta {repeat} exterior como la interior generaran código con los mismos nombres de
variable temporal (por ejemplo, $__count y $__i), el bucle interior sobrescribiría las variables del
exterior y rompería la lógica.
Tenemos que garantizar que las variables temporales generadas para cada instancia de la etiqueta {repeat} sean
únicas. Lo conseguimos con PrintContext::generateId(). Este método devuelve un entero único durante la fase
de compilación. Podemos añadir ese ID a los nombres de nuestras variables temporales.
Así, en lugar de $__count, generaremos un nombre con un sufijo numérico único, como $__count_0, y
lo mismo para el contador del bucle, por ejemplo $__i_0. Los números reales proceden de un contador para toda la
compilación compartido por todos los nodos, así que solo se garantiza que sean únicos, no que formen una secuencia por
etiqueta.
Implementar RepeatNode
Creemos la clase de nodo.
<?php
namespace App\Templating;
use Latte\CompileException;
use Latte\Compiler\Nodes\AreaNode;
use Latte\Compiler\Nodes\Php\ExpressionNode;
use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\PrintContext;
use Latte\Compiler\Tag;
class RepeatNode extends StatementNode
{
public ExpressionNode $count;
public AreaNode $content;
/**
* Parsing function for {repeat $count} ... {/repeat}
*/
public static function create(Tag $tag): \Generator
{
$tag->expectArguments(); // asegura que se indica $count
$node = $tag->node = new self;
// Parsea la expresión del contador
$node->count = $tag->parser->parseExpression();
// Obtiene el contenido interno
[$node->content] = yield;
return $node;
}
/**
* Generates the PHP 'for' loop with unique variable names.
*/
public function print(PrintContext $context): string
{
// Genera nombres de variable únicos
$id = $context->generateId();
$countVar = '$__count_' . $id; // nombre único, p. ej. $__count_0
$iteratorVar = '$__i_' . $id; // nombre único, p. ej. $__i_0
return $context->format(
<<<'XX'
// Evalúa la expresión del contador *una sola vez* y la guarda
%raw = (int) (%node);
// Itera usando el contador guardado y la variable de iteración única
for (%raw = 0; %2.raw < %0.raw; %2.raw++) %line {
%node // Renderiza el contenido interno
}
XX,
$countVar, // %0 - Variable donde guardar el contador
$this->count, // %1 - El nodo de la expresión del contador
$iteratorVar, // %2 - Nombre de la variable de iteración
$this->position, // %3 - Comentario con el número de línea del propio bucle
$this->content // %4 - El nodo del contenido interno
);
}
/**
* Yields child nodes (the count expression and the content).
*/
public function &getIterator(): \Generator
{
yield $this->count;
yield $this->content;
}
}
El método create() analiza con parseExpression() la expresión $count, que es
obligatoria. Primero se llama a $tag->expectArguments(). Así se garantiza que el usuario ha indicado algo
después de {repeat}. Aunque $tag->parser->parseExpression() fallaría si no se hubiera indicado
nada, el mensaje de error hablaría de una sintaxis inesperada. Con expectArguments() el error es mucho más claro,
porque dice explícitamente que faltan los argumentos de la etiqueta {repeat}.
El método print() genera el código PHP encargado de ejecutar la lógica de repetición en tiempo de ejecución.
Empieza generando nombres únicos para las variables temporales de PHP que necesitará.
El método $context->format() se llama con el nuevo marcador %raw, que inserta la cadena en
bruto pasada como argumento correspondiente. Aquí inserta el nombre único de variable guardado en $countVar
(por ejemplo, $__count_1). ¿Y qué pasa con %0.raw y %2.raw? Esto muestra los
marcadores posicionales. En lugar de un simple %raw, que toma el siguiente argumento en bruto
disponible, %2.raw toma explícitamente el argumento del índice 2 (que es $iteratorVar) e inserta su
valor de cadena en bruto. Así podemos reutilizar la cadena $iteratorVar sin pasarla varias veces en la lista de
argumentos de format().
Esta llamada a format(), cuidadosamente construida, genera un bucle de PHP eficiente y seguro que trata
correctamente la expresión del contador y evita las colisiones de nombres de variable incluso con etiquetas {repeat}
anidadas.
Registro y uso
Registre la etiqueta en su extensión:
use App\Templating\RepeatNode;
class MyLatteExtension extends Extension
{
public function getTags(): array
{
return [
'datetime' => DatetimeNode::create(...),
'debug' => DebugNode::create(...),
'repeat' => RepeatNode::create(...), // Registra la etiqueta repeat
];
}
}
Úsela en una plantilla, también anidada:
{var $rows = rand(5, 7)}
{var $cols = rand(3, 5)}
{repeat $rows}
<tr>
{repeat $cols}
<td>Inner loop</td>
{/repeat}
</tr>
{/repeat}
Este ejemplo muestra cómo gestionar el estado (los contadores del bucle) y los posibles problemas de anidamiento usando
variables temporales con el prefijo $__ y haciéndolas únicas con los IDs de
PrintContext::generateId().
n:atributos puros
Aunque muchos n:atributos, como n:if o n:foreach, son atajos cómodos de sus etiquetas
pares equivalentes ({if}...{/if}, {foreach}...{/foreach}), Latte también le permite definir etiquetas
que existen solo en forma de n:atributo. Suelen usarse para modificar los atributos o el comportamiento del elemento HTML
al que se adjuntan.
Ejemplos estándar integrados en Latte son n:class, que
ayuda a construir dinámicamente el atributo class, y n:attr, que puede establecer varios atributos arbitrarios.
Creemos nuestro propio n:atributo puro: n:confirm, que añadirá un diálogo de confirmación de JavaScript antes
de ejecutar una acción (como seguir un enlace o enviar un formulario).
Objetivo: implementar n:confirm="'Are you sure?'", que añade un manejador onclick para
impedir la acción predeterminada si el usuario cancela el diálogo de confirmación.
Implementar ConfirmNode
Necesitamos una clase de nodo y una función de análisis.
<?php
namespace App\Templating;
use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\PrintContext;
use Latte\Compiler\Tag;
use Latte\Compiler\Nodes\Php\ExpressionNode;
use Latte\Compiler\Nodes\Php\Scalar\StringNode;
class ConfirmNode extends StatementNode
{
public ExpressionNode $message;
public static function create(Tag $tag): self
{
$tag->expectArguments();
$node = $tag->node = new self;
$node->message = $tag->parser->parseExpression();
return $node;
}
/**
* Generates the 'onclick' attribute code with proper escaping.
*/
public function print(PrintContext $context): string
{
// Asegura el escapado correcto tanto en el contexto de JavaScript como en el de atributo HTML.
return $context->format(
<<<'XX'
echo ' onclick="', LR\HtmlHelpers::escapeAttr('return confirm(' . LR\Helpers::escapeJs(%node) . ')'), '"' %line;
XX,
$this->message,
$this->position,
);
}
public function &getIterator(): \Generator
{
yield $this->message;
}
}
El método print() genera el código PHP que acabará imprimiendo el atributo HTML onclick="..."
durante el renderizado de la plantilla. Tratar contextos anidados (JavaScript dentro de un atributo HTML) exige un escapado
cuidadoso. El auxiliar LR\Helpers::escapeJs(%node) se llama en tiempo de ejecución y escapa el mensaje correctamente
para usarlo dentro de JavaScript (la salida sería algo como "Sure?"). Después, el auxiliar
LR\HtmlHelpers::escapeAttr(...) escapa los caracteres especiales dentro de los atributos HTML, de modo que
convertiría la salida en return confirm("Sure?"). Este escapado en dos pasos, en tiempo de
ejecución, garantiza que el mensaje sea seguro para JavaScript y que el código JavaScript resultante sea seguro para incrustarlo
dentro del atributo HTML onclick.
Registro y uso
Registre el n:atributo en su extensión. Recuerde el prefijo n: en la clave:
class MyLatteExtension extends Extension
{
public function getTags(): array
{
return [
'datetime' => DatetimeNode::create(...),
'debug' => DebugNode::create(...),
'repeat' => RepeatNode::create(...),
'n:confirm' => ConfirmNode::create(...), // Registra n:confirm
];
}
}
Ahora puede usar n:confirm en enlaces, botones o elementos de formulario:
<a href="delete.php?id=123" n:confirm='"Do you really want to delete item {$id}?"'>Delete</a>
HTML generado:
<a href="delete.php?id=123" onclick="return confirm("Do you really want to delete item 123?")">Delete</a>
Cuando el usuario pulse el enlace, el navegador ejecutará el código de onclick, mostrará el diálogo de
confirmación y solo continuará a delete.php si el usuario pulsa “OK”.
Este ejemplo muestra cómo crear un n:atributo puro que modifica el comportamiento o los atributos de su elemento HTML
anfitrión generando el código PHP adecuado en su método print(). Recuerde el doble escapado que suele hacer falta:
uno para el contexto de destino (JavaScript en este caso) y otro para el contexto del atributo HTML.
Otros dos miembros del objeto Tag resultan útiles al escribir n:atributos puros:
$tag->htmlElement le da acceso al elemento HTML circundante (un ElementNode), de modo que puede
inspeccionarlo o ajustarlo, y $tag->replaceNAttribute($node) le permite cambiar el atributo por un nodo que usted
construya. De hecho, el nodo devuelto por el create() de un n:atributo puro sustituye automáticamente al atributo en
su elemento.
Temas avanzados
Las secciones anteriores cubren los conceptos básicos, pero aquí tiene algunos temas más avanzados con los que puede encontrarse al crear etiquetas propias de Latte.
Modos de salida de las etiquetas
El objeto Tag que se pasa a su función create() tiene una propiedad outputMode. Esta
propiedad influye en cómo trata Latte los espacios en blanco y la sangría del entorno, sobre todo cuando la etiqueta se usa sola
en una línea. Puede modificar esta propiedad dentro de su función create().
Tag::OutputNone(el valor predeterminado de toda etiqueta, y el que conservan las estructuras de control como{if}o{foreach}): los espacios en blanco alrededor de la etiqueta se tratan exactamente como conOutputRemoveIndentation: se eliminan la sangría inicial y un único salto de línea final. La diferencia real es interna: este modo mantiene el parser de plantillas en el modo “head” de la plantilla. Conviene a las etiquetas de declaración o configuración, como{var}o{default}, que no producen salida directa.Tag::OutputRemoveIndentation(fijado explícitamente por las etiquetas de bloque{block},{embed},{include}y{sandbox}): elimina la sangría anterior a la etiqueta y un único salto de línea final. Esto ayuda a mantener más limpio el código PHP generado y evita líneas vacías de más en la salida HTML causadas por la propia etiqueta.Tag::OutputKeepIndentation(fijado explícitamente por las etiquetas de salida, como{=...}): Latte procura conservar la sangría anterior a la etiqueta; los saltos de línea posteriores se mantienen en general. Es lo adecuado para etiquetas que imprimen contenido en línea; vea el ejemplo de{datetime}de más arriba, que fija este modo precisamente por eso.
Elija el modo que mejor encaje con el propósito de su etiqueta. Como el valor predeterminado es OutputNone, las
etiquetas de control de flujo y de declaración no necesitan cambio alguno; fije OutputKeepIndentation en las
etiquetas que imprimen contenido en su propia línea.
Acceder a las etiquetas padre o más cercanas
A veces, el comportamiento de una etiqueta debe depender del contexto en el que se usa, en concreto de dentro de qué
etiquetas padre se encuentra. El objeto Tag que se pasa a su función create() ofrece precisamente para
eso el método closestTag(array $classes, ?callable $condition = null): ?Tag.
Este método busca hacia arriba por la jerarquía de etiquetas de Latte abiertas en ese momento (la cadena de
$tag->parent; los elementos HTML circundantes no forman parte de ella) y devuelve el objeto Tag del
ancestro más cercano que cumpla ciertos criterios. Si no encuentra ningún ancestro que encaje, devuelve null.
El array $classes indica qué tipo de etiquetas ancestro busca. Comprueba si la clase del nodo asociado a la
etiqueta ancestro ($ancestorTag->node) es exactamente una de las clases listadas; las subclases no cuentan.
function create(Tag $tag)
{
// Busca la etiqueta ancestro más cercana cuyo nodo sea una instancia de ForeachNode
$foreachTag = $tag->closestTag([ForeachNode::class]);
if ($foreachTag) {
// Podemos acceder a la propia instancia de ForeachNode:
$foreachNode = $foreachTag->node;
}
}
Fíjese en $foreachTag->node: esto funciona solo porque en el desarrollo de etiquetas de Latte es una
convención asignar de inmediato el nodo creado a $tag->node dentro del método create(), como hemos
hecho siempre.
A veces no basta con que coincida el tipo de nodo. Puede que necesite comprobar una propiedad concreta de la posible etiqueta
ancestro o de su nodo. El segundo argumento opcional de closestTag() es un callable que recibe el posible objeto
Tag ancestro y debe devolver si es una coincidencia válida.
function create(Tag $tag)
{
$dynamicBlockTag = $tag->closestTag(
[BlockNode::class],
// Condición: el bloque debe ser dinámico
fn(Tag $blockTag) => $blockTag->node->block->isDynamic(),
);
}
Usar closestTag() le permite crear etiquetas conscientes del contexto y hacer cumplir un uso correcto dentro de la
estructura de sus plantillas, lo que da plantillas más robustas y comprensibles.
Marcadores de PrintContext::format()
Hemos usado con frecuencia PrintContext::format() para generar código PHP en los métodos print() de
nuestros nodos. Acepta una cadena de máscara y argumentos posteriores que sustituyen a los marcadores de la máscara. Aquí tiene
un resumen de los marcadores disponibles:
%node: el argumento debe ser una instancia deNode. Llama al métodoprint()del nodo e inserta la cadena de código PHP resultante.%dump: el argumento es cualquier valor de PHP. Exporta el valor a código PHP válido. Adecuado para escalares, arrays y null.$context->format('echo %dump;', 'Hello')→echo 'Hello';$context->format('$arr = %dump;', [1, 2])→$arr = [1, 2];
%raw: inserta el argumento directamente en el código PHP de salida, sin escapado ni modificación alguna. Úselo con precaución, sobre todo para insertar fragmentos de código PHP pregenerados o nombres de variable.$context->format('%raw = 1;', '$variableName')→$variableName = 1;
%args: el argumento debe ser unExpression\ArrayNode. Imprime los elementos del array con el formato de los argumentos de una llamada a función o método (separados por comas, tratando los argumentos nombrados si los hay).$argsNode = new ArrayNode([...]);$context->format('myFunc(%args);', $argsNode)→myFunc(1, name: 'Joe');
%line: el argumento debe ser un objetoPosition(oRange), normalmente$this->position. Inserta un comentario PHP/* pos X:Y */que indica la línea y la columna de origen.$context->format('echo "Hi" %line;', $this->position)→echo "Hi" /* pos 42:1 */;
%escape(...): genera código PHP que, en tiempo de ejecución, escapará la expresión interior según las reglas de escapado sensible al contexto vigentes.$context->format('echo %escape(%node);', $variableNode)
%modify(...): el argumento debe ser unModifierNode. Genera código PHP que aplica al contenido interior los filtros indicados en elModifierNode, incluido el escapado sensible al contexto si no se ha desactivado con|noescape.$context->format('%modify(%node);', $modifierNode, $variableNode)
%modifyContent(...): parecido a%modify, pero pensado para modificar bloques de contenido capturado (a menudo HTML).
Puede referenciar los argumentos explícitamente por su índice, empezando en cero: %0.node, %1.dump,
%2.raw, etc. Esto permite reutilizar un argumento varias veces en la máscara sin pasarlo repetidamente a
format(). Vea el ejemplo de la etiqueta {repeat}, donde se usaron %0.raw y
%2.raw.
Ejemplo de análisis complejo de argumentos
Aunque parseExpression(), parseArguments(), etc., cubren muchos casos, a veces necesita una lógica
de análisis más intrincada usando el TokenStream de más bajo nivel, disponible mediante
$tag->parser->stream.
Objetivo: crear una etiqueta {embedYoutube $videoID, width: 640, height: 480}. Queremos analizar un ID de
vídeo obligatorio (cadena o variable) seguido de pares clave-valor opcionales para las dimensiones.
<?php
namespace App\Templating;
use Latte\CompileException;
use Latte\Compiler\Nodes\Php\ExpressionNode;
use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\Tag;
use Latte\Compiler\Token;
class YoutubeNode extends StatementNode
{
public ExpressionNode $videoId;
public ?ExpressionNode $width = null;
public ?ExpressionNode $height = null;
public static function create(Tag $tag): self
{
$tag->expectArguments();
$node = $tag->node = new self;
// Parsea el ID de vídeo requerido
$node->videoId = $tag->parser->parseExpression();
// Parsea los pares clave-valor opcionales
$stream = $tag->parser->stream; // Obtiene el flujo de tokens
while ($stream->tryConsume(',')) { // Requiere separación por comas
// Espera el identificador 'width' o 'height'
$keyToken = $stream->consume(Token::Php_Identifier);
$key = strtolower($keyToken->text);
$stream->consume(':'); // Espera el separador de dos puntos
$value = $tag->parser->parseExpression(); // Parsea la expresión del valor
if ($key === 'width') {
$node->width = $value;
} elseif ($key === 'height') {
$node->height = $value;
} else {
throw new CompileException("Unknown argument '$key'. Expected 'width' or 'height'.", $keyToken->position);
}
}
return $node;
}
// ... print() and getIterator() ...
}
Este nivel de control le permite definir sintaxis muy concretas y complejas para sus etiquetas propias interactuando directamente con el flujo de tokens.
Usar AuxiliaryNode
Latte ofrece nodos “auxiliares” genéricos para situaciones especiales durante la generación de código o dentro de los
pases del compilador. Son AuxiliaryNode y Php\Expression\AuxiliaryNode.
Piense en AuxiliaryNode como un nodo contenedor flexible que delega sus funciones centrales (la generación de
código y la exposición de los nodos hijos) en los argumentos que se le pasan al constructor:
- Delegación de
print(): el primer argumento del constructor es un closure de PHP. Cuando Latte llama al métodoprint()de unAuxiliaryNode, ejecuta ese closure. El closure recibe elPrintContexty los nodos pasados en el segundo argumento del constructor, lo que le permite definir sobre la marcha una lógica de generación de código PHP totalmente propia. - Delegación de
getIterator(): el segundo argumento del constructor es un array de objetosNode. Cuando Latte necesita recorrer los hijos de unAuxiliaryNode(por ejemplo, durante los pases del compilador), su métodogetIterator()se limita a devolver los nodos de ese array.
Ejemplo:
$node = new AuxiliaryNode(
// 1. Este closure se convierte en el cuerpo de print()
fn(PrintContext $context, $arg1, $arg2) => $context->format('...%node...%node...', $arg1, $arg2),
// 2. Estos nodos los devuelve getIterator() y se pasan al closure de arriba
[$argumentNode1, $argumentNode2]
);
Latte ofrece dos tipos distintos según dónde necesite insertar el código generado:
Latte\Compiler\Nodes\Php\Expression\AuxiliaryNode: úselo cuando necesite generar un fragmento de código PHP que represente una expresiónLatte\Compiler\Nodes\AuxiliaryNode: úselo para fines más generales, cuando necesite insertar un bloque de código PHP que represente una o varias sentencias
La razón importante para usar AuxiliaryNode en lugar de los nodos estándar (como
StaticMethodCallNode) dentro de su método print() o de un pase del compilador es controlar la
visibilidad ante los pases posteriores, sobre todo los relacionados con la seguridad, como el Sandbox.
Piense en este escenario: su pase del compilador necesita envolver una expresión proporcionada por el usuario
($userExpr) con una llamada a una función auxiliar concreta y de confianza,
myInternalSanitize($userExpr). Si crea un nodo estándar
new FunctionCallNode('myInternalSanitize', [$userExpr]), será plenamente visible para el recorredor del AST. Si un
pase del Sandbox se ejecuta después y myInternalSanitize no está en su lista de permitidos, el Sandbox
podría bloquear o modificar esa llamada y romper la lógica interna de su etiqueta, aunque usted, como autor de la
etiqueta, sepa que esa llamada concreta es segura y necesaria. Puede entonces generar la llamada directamente dentro del closure
del AuxiliaryNode.
use Latte\Compiler\Nodes\Php\Expression\AuxiliaryNode;
// ... dentro de print() o de un compiler pass ...
$wrappedNode = new AuxiliaryNode(
fn(PrintContext $context, $userExpr) => $context->format(
'myInternalSanitize(%node)', // Generación directa de código PHP
$userExpr,
),
// IMPORTANTE: ¡pase aquí igualmente el nodo de la expresión original del usuario!
[$userExpr],
);
En este caso, el pase del Sandbox ve el AuxiliaryNode pero no analiza el código PHP generado por su
closure. No puede bloquear directamente la llamada a myInternalSanitize generada dentro del closure.
Aunque el código PHP generado queda oculto a los pases, las entradas de ese código (los nodos que representan datos
o expresiones del usuario) deben seguir siendo recorribles. Por eso es crucial el segundo argumento del constructor de
AuxiliaryNode. Debe pasar un array con todos los nodos originales (como $userExpr en el ejemplo
anterior) que use su closure. El getIterator() de AuxiliaryNode devolverá esos nodos, lo que
permitirá a pases como el Sandbox analizarlos en busca de problemas.
Buenas prácticas
- Propósito claro: asegúrese de que su etiqueta tiene un propósito claro y necesario. No cree etiquetas para tareas que se resuelven fácilmente con filtros o funciones.
- Implemente
getIterator()correctamente: implemente siempregetIterator()y devuelva referencias (&) a todos los nodos hijos (argumentos, contenido) analizados de la plantilla. Es esencial para los pases del compilador, la seguridad (Sandbox) y posibles optimizaciones futuras. - Propiedades públicas para los nodos: haga públicas las propiedades que contienen nodos hijos, para que los pases del compilador puedan modificarlas si hace falta.
- Use
PrintContext::format(): aproveche el métodoformat()para generar código PHP. Se encarga del entrecomillado, escapa los marcadores correctamente y añade automáticamente los comentarios con los números de línea. - Variables temporales (
$__): cuando genere código PHP de ejecución que necesite variables temporales (por ejemplo, para guardar resultados intermedios o contadores de bucle), use la convención del prefijo$__para evitar colisiones con las variables del usuario y con las variables internas$ʟ_de Latte. - Anidamiento e IDs únicos: si su etiqueta puede anidarse o necesita estado propio de cada instancia en tiempo de
ejecución, use
$context->generateId()dentro de su métodoprint()para crear sufijos únicos para sus variables temporales$__. - Proveedores para los datos externos: use los proveedores (registrados con
Extension::getProviders()) para acceder a datos o servicios de ejecución ($this->global->…) en lugar de codificar valores a fuego o depender del estado global. Use prefijos de fabricante en los nombres de los proveedores. - Piense en los n:atributos: si su etiqueta par opera lógicamente sobre un único elemento HTML, es probable que Latte
ofrezca soporte automático de
n:atributo. Téngalo en cuenta por comodidad de los usuarios. Si crea una etiqueta que modifica atributos, valore si unn:atributopuro es la forma más adecuada. - Pruebas: escriba pruebas para sus etiquetas, que cubran tanto el análisis de distintas entradas sintácticas como la corrección de la salida del código PHP generado.
Siguiendo estas pautas podrá crear etiquetas propias potentes, robustas y mantenibles que se integren sin fisuras con el motor de plantillas Latte.
Estudiar las clases de nodo que forman parte de Latte es la mejor manera de aprender todos los entresijos del proceso de análisis.