Creación de etiquetas personalizadas
Esta página proporciona una guía completa para crear etiquetas personalizadas en Latte. Discutiremos todo, desde etiquetas simples hasta escenarios más complejos con contenido anidado y necesidades específicas de análisis, basándonos en su comprensión de cómo Latte compila las plantillas.
Las etiquetas personalizadas proporcionan el nivel más alto de control sobre la sintaxis de la plantilla y la lógica de renderizado, pero también son el punto de extensión más complejo. Antes de decidir crear una etiqueta personalizada, siempre considere si no existe una solución más simple o si ya existe una etiqueta adecuada en el conjunto estándar. Use etiquetas personalizadas solo cuando las alternativas más simples no sean suficientes para sus necesidades.
Entendiendo el proceso de compilación
Para crear etiquetas personalizadas de manera efectiva, es útil explicar cómo Latte procesa las plantillas. Comprender este proceso aclara por qué las etiquetas están estructuradas de esta manera y cómo encajan en el contexto más amplio.
La compilación de una plantilla en Latte, simplificada, incluye estos pasos clave:
- Análisis léxico: El lexer lee el código fuente de la plantilla (archivo
.latte
) y lo divide en una secuencia de pequeñas partes distintas llamadas tokens (por ejemplo,{
,foreach
,$variable
,}
, texto HTML, etc.). - Análisis sintáctico: El parser toma este flujo de tokens y construye una estructura de árbol significativa que representa la lógica y el contenido de la plantilla. Este árbol se llama árbol de sintaxis abstracto (AST).
- Pasos de compilación: Antes de generar el código PHP, Latte ejecuta pasos de compilación. Son funciones que recorren todo el AST y pueden modificarlo o recopilar información. Este paso es crucial para funciones como la seguridad (Sandbox) u optimizaciones.
- Generación de código: Finalmente, el compilador recorre el AST (potencialmente modificado) y genera el código de clase PHP correspondiente. Este código PHP es lo que realmente renderiza la plantilla cuando se ejecuta.
- Caching: El código PHP generado se almacena en el disco, lo que hace que las renderizaciones posteriores sean muy rápidas, ya que se omiten los pasos 1–4.
En realidad, la compilación es un poco más compleja. Latte tiene dos lexers y parsers: uno para la plantilla HTML y otro para el código similar a PHP dentro de las etiquetas. Y tampoco el análisis sintáctico ocurre después de la tokenización, sino que el lexer y el parser se ejecutan en paralelo en dos “hilos” y se coordinan. Créanme, programarlo fue todo un desafío :-)
Todo el proceso, desde cargar el contenido de la plantilla, pasando por el análisis sintáctico, hasta generar el archivo resultante, se puede secuenciar con este código, con el que puede experimentar e imprimir resultados intermedios:
$latte = new Latte\Engine;
$source = $latte->getLoader()->getContent($file);
$ast = $latte->parse($source);
$latte->applyPasses($ast);
$code = $latte->generate($ast, $file);
Anatomía de una etiqueta
La creación de una etiqueta personalizada completamente funcional en Latte implica varias partes interconectadas. Antes de sumergirnos en la implementación, comprendamos los conceptos básicos y la terminología, utilizando una analogía con HTML y el Document Object Model (DOM).
Etiquetas vs. Nodos (Analogía con HTML)
En HTML, escribimos etiquetas como <p>
o <div>...</div>
. Estas etiquetas son
la sintaxis en el código fuente. Cuando un navegador analiza este HTML, crea una representación en memoria llamada Document
Object Model (DOM). En el DOM, las etiquetas HTML están representadas por nodos (específicamente nodos
Element
en la terminología del DOM de JavaScript). Trabajamos programáticamente con estos nodos (por
ejemplo, usando document.getElementById(...)
de JavaScript se devuelve un nodo Element). Una etiqueta es solo una
representación textual en el archivo fuente; un nodo es una representación de objeto en el árbol lógico.
Latte funciona de manera similar:
- En el archivo de plantilla
.latte
, escribe etiquetas Latte, como{foreach ...}
y{/foreach}
. Esta es la sintaxis con la que usted, como autor de la plantilla, trabaja. - Cuando Latte analiza la plantilla, construye un Árbol de Sintaxis Abstracto (AST). Este árbol está compuesto por nodos. Cada etiqueta Latte, elemento HTML, trozo de texto o expresión en la plantilla se convierte en uno o más nodos en este árbol.
- La clase base para todos los nodos en el AST es
Latte\Compiler\Node
. Al igual que el DOM tiene diferentes tipos de nodos (Element, Text, Comment), el AST de Latte tiene diferentes tipos de nodos. Se encontrará conLatte\Compiler\Nodes\TextNode
para texto estático,Latte\Compiler\Nodes\Html\ElementNode
para elementos HTML,Latte\Compiler\Nodes\Php\ExpressionNode
para expresiones dentro de etiquetas y, crucialmente para etiquetas personalizadas, nodos que heredan deLatte\Compiler\Nodes\StatementNode
.
¿Por qué StatementNode
?
Los elementos HTML (Html\ElementNode
) representan principalmente estructura y contenido. Las expresiones PHP
(Php\ExpressionNode
) representan valores o cálculos. Pero, ¿qué pasa con las etiquetas Latte como
{if}
, {foreach}
o nuestra propia {datetime}
? Estas etiquetas realizan acciones,
controlan el flujo del programa o generan salida basada en la lógica. Son unidades funcionales que hacen de Latte un potente
motor de plantillas, no solo un lenguaje de marcado.
En programación, tales unidades que realizan acciones a menudo se llaman “statements” (sentencias). Por lo tanto, los
nodos que representan estas etiquetas Latte funcionales típicamente heredan de Latte\Compiler\Nodes\StatementNode
.
Esto los distingue de los nodos puramente estructurales (como los elementos HTML) o los nodos que representan valores (como las
expresiones).
Los componentes clave
Repasemos los componentes principales necesarios para crear una etiqueta personalizada:
Función de análisis de etiquetas
- Esta función PHP callable analiza la sintaxis de la etiqueta Latte (
{...}
) en la plantilla fuente. - Recibe información sobre la etiqueta (como su nombre, posición y si es un n:attribute) a través del objeto Latte\Compiler\Tag.
- Su herramienta principal para analizar argumentos y expresiones dentro de los delimitadores de la etiqueta es el objeto Latte\Compiler\TagParser, accesible a través de
$tag->parser
(este es un parser diferente al que analiza toda la plantilla). - Para etiquetas emparejadas, usa
yield
para indicar a Latte que analice el contenido interno entre la etiqueta de apertura y la de cierre. - El objetivo final de la función de análisis es crear y devolver una instancia de la clase de nodo, que se agrega 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 en la clase de nodo correspondiente. Esto mantiene la lógica de análisis y la representación del nodo ordenadamente en un solo paquete, permite el acceso a elementos privados/protegidos de la clase si es necesario y mejora la organización.
Clase de nodo
- Representa la función lógica de su etiqueta en el Árbol de Sintaxis Abstracto (AST).
- Contiene información analizada (como argumentos o contenido) como propiedades públicas. Estas propiedades a menudo
contienen otras instancias de
Node
(por ejemplo,ExpressionNode
para argumentos analizados,AreaNode
para contenido analizado). - El método
print(PrintContext $context): string
genera el código PHP (una sentencia o serie de sentencias) que realiza la acción de la etiqueta durante la renderización de la plantilla. - El método
getIterator(): \Generator
expone los nodos hijos (argumentos, contenido) para el recorrido por los pasos de compilación. Debe proporcionar referencias (&
) para permitir que los pasos modifiquen o reemplacen potencialmente los subnodos. - Después de que toda la plantilla se analiza en un AST, Latte ejecuta una serie de pasos de compilación. Estos pasos recorren todo el AST utilizando el
método
getIterator()
proporcionado por cada nodo. Pueden inspeccionar nodos, recopilar información e incluso modificar el árbol (por ejemplo, cambiando las propiedades públicas de los nodos o reemplazando nodos por completo). Este diseño, que requiere ungetIterator()
completo, es crucial. Permite que funciones potentes como Sandbox analicen y potencialmente cambien el comportamiento de cualquier parte de la plantilla, incluidas sus propias etiquetas personalizadas, garantizando la seguridad y la coherencia.
Registro a través de una extensión
- Necesita informar a Latte sobre su nueva etiqueta y qué función de análisis debe usarse para ella. Esto se hace dentro de una extensión 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 funciones PHP callable que representan sus respectivas funciones de análisis (por ejemplo,MyNamespace\DatetimeNode::create(...)
).
Resumen: La función de análisis de etiquetas transforma el código fuente de la plantilla de su etiqueta en
un nodo AST. La clase de nodo puede entonces transformar a sí misma en código PHP ejecutable
para la plantilla compilada y expone sus subnodos para los pasos de compilación a través de getIterator()
.
El registro a través de una extensión conecta el nombre de la etiqueta con la función de análisis y se lo informa
a Latte.
Ahora exploremos cómo implementar estos componentes paso a paso.
Creación de una etiqueta simple
Vamos a crear su primera etiqueta Latte personalizada. Comenzaremos con un ejemplo muy simple: una etiqueta llamada
{datetime}
que imprime la fecha y hora actuales. Inicialmente, esta etiqueta no aceptará ningún argumento,
pero la mejoraremos más adelante en la sección Análisis de argumentos de etiqueta.
Tampoco tiene contenido interno.
Este ejemplo lo guiará a través de los pasos básicos: definir la clase de nodo, implementar sus métodos
print()
y getIterator()
, crear la función de análisis y, finalmente, registrar la etiqueta.
Objetivo: Implementar {datetime}
para generar la fecha y hora actuales usando la función PHP
date()
.
Creación de la clase de nodo
Primero, necesitamos una clase que represente nuestra etiqueta en el Árbol de Sintaxis Abstracto (AST). Como se discutió
anteriormente, heredamos de Latte\Compiler\Nodes\StatementNode
.
Cree un archivo (por ejemplo, DatetimeNode.php
) y defina la clase:
<?php
namespace App\Latte;
use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\PrintContext;
use Latte\Compiler\Tag;
class DatetimeNode extends StatementNode
{
/**
* Función de análisis de etiquetas, llamada cuando se encuentra {datetime}.
*/
public static function create(Tag $tag): self
{
// Nuestra etiqueta simple actualmente no acepta argumentos, así que no necesitamos analizar nada
$node = $tag->node = new self;
return $node;
}
/**
* Genera el código PHP que se ejecutará al renderizar la plantilla.
*/
public function print(PrintContext $context): string
{
return $context->format(
'echo date(\'Y-m-d H:i:s\') %line;',
$this->position,
);
}
/**
* Proporciona acceso a los nodos hijos para los pasos de compilación de Latte.
*/
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
.
El método print()
genera el código PHP que se ejecutará al renderizar la plantilla. Llamamos al método
$context->format()
, que construye la cadena de código PHP resultante para la plantilla compilada. El primer
argumento, 'echo date('Y-m-d H:i:s') %line;'
, es una máscara en la que se insertan los siguientes parámetros. El
placeholder %line
le dice al método format()
que use el segundo argumento, que es
$this->position
, e inserte un comentario como /* line 15 */
, que vincula el código PHP generado de
nuevo a la línea original de la plantilla, lo cual es crucial para la depuración.
La propiedad $this->position
se hereda de la clase base Node
y es establecida automáticamente por
el parser de Latte. Contiene un objeto Latte\Compiler\Position que indica dónde se encontró
la etiqueta en el archivo fuente .latte
.
El método getIterator()
es crucial para los pasos de compilación. Debe proporcionar todos los nodos hijos, pero
nuestro simple DatetimeNode
actualmente no tiene argumentos ni contenido, por lo tanto, no tiene nodos hijos. Sin
embargo, el método aún debe existir y ser un generador, es decir, la palabra clave yield
debe estar presente de
alguna manera en el cuerpo del método.
Registro a través de una extensión
Finalmente, informemos a Latte sobre 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\Latte;
use Latte\Extension;
class MyLatteExtension extends Extension
{
/**
* Devuelve la lista de etiquetas proporcionadas por esta extensión.
* @return array<string, callable> Mapa: 'nombre-etiqueta' => funcion-analisis
*/
public function getTags(): array
{
return [
'datetime' => DatetimeNode::create(...),
// Registre más etiquetas aquí más tarde
];
}
}
Luego registre esta extensión en el Latte Engine:
$latte = new Latte\Engine;
$latte->addExtension(new App\Latte\MyLatteExtension);
Cree una plantilla:
<p>Página generada: {datetime}</p>
Salida esperada: <p>Página generada: 2023-10-27 11:00:00</p>
Resumen de esta fase
Hemos creado con éxito una etiqueta personalizada básica {datetime}
. Definimos su representación en el AST
(DatetimeNode
), manejamos su análisis (create()
), especificamos cómo debería generar código PHP
(print()
), nos aseguramos de que sus hijos sean accesibles para el recorrido (getIterator()
) y la
registramos en Latte.
En la siguiente sección, mejoraremos esta etiqueta para aceptar argumentos y mostraremos cómo analizar expresiones y administrar nodos hijos.
Análisis de argumentos de etiqueta
Nuestra etiqueta simple {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 requerida será
{datetime $format}
.
Objetivo: Modificar {datetime}
para que acepte una expresión PHP opcional como argumento, que se
utilizará como cadena de formato para date()
.
Introducción a TagParser
Antes de modificar el código, es importante comprender la herramienta que utilizaremos: Latte\Compiler\TagParser. Cuando el parser principal
de Latte (TemplateParser
) encuentra una etiqueta Latte como {datetime ...}
o un n:attribute, delega el
análisis del contenido dentro de la etiqueta (la parte entre {
y }
o el valor del atributo) a
un TagParser
especializado.
Este TagParser
trabaja exclusivamente con los argumentos de la etiqueta. Su tarea es procesar los tokens
que representan estos argumentos. Es crucial que debe procesar todo el contenido que se le proporciona. Si su función de
análisis termina, pero TagParser
no ha llegado al final de los argumentos (verificado a través de
$tag->parser->isEnd()
), Latte lanzará una excepción, ya que indica que quedaron tokens inesperados dentro de
la etiqueta. Por el contrario, si la etiqueta requiere argumentos, debe llamar a $tag->expectArguments()
al
principio de su función de análisis. Este método verifica si hay argumentos presentes y lanza una excepción útil si la
etiqueta se usó sin ningún argumento.
TagParser
ofrece métodos útiles para analizar diferentes tipos de argumentos:
parseExpression(): ExpressionNode
: Analiza una expresión similar a PHP (variables, literales, operadores, llamadas a funciones/métodos, etc.). Maneja el azúcar sintáctico de Latte, como tratar cadenas alfanuméricas simples como cadenas entre comillas (por ejemplo,foo
se analiza como si fuera'foo'
).parseUnquotedStringOrExpression(): ExpressionNode
: Analiza una expresión estándar o una cadena sin comillas. Las cadenas sin comillas son secuencias permitidas por Latte sin comillas, a menudo utilizadas para cosas como rutas de archivos (por ejemplo,{include ../file.latte}
). Si analiza una cadena sin comillas, devuelve unStringNode
.parseArguments(): ArrayNode
: Analiza argumentos separados por comas, potencialmente con claves, como10, name: 'John', true
.parseModifier(): ModifierNode
: Analiza filtros como|upper|truncate:10
.parseType(): ?SuperiorTypeNode
: Analiza type hints 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 a través de
$tag->parser->stream
. Este objeto proporciona métodos para verificar y procesar tokens individuales:
$tag->parser->stream->is(...): bool
: Verifica si el token actual coincide con alguno de los tipos especificados (por ejemplo,Token::Php_Variable
) o valores literales (por ejemplo,'as'
) sin consumirlo. Útil para mirar hacia adelante.$tag->parser->stream->consume(...): Token
: Consume el token actual y avanza la posición del flujo. Si se proporcionan tipos/valores de token esperados como argumentos y el token actual no coincide, lanzaCompileException
. Use esto cuando espere un token específico.$tag->parser->stream->tryConsume(...): ?Token
: Intenta consumir el token actual solo si coincide con uno de los tipos/valores especificados. Si coincide, consume el token y lo devuelve. Si no coincide, deja la posición del flujo sin cambios y devuelvenull
. Use esto para tokens opcionales o cuando elija entre diferentes rutas sintácticas.
Actualización de la función de análisis create()
Con esta comprensión, modifiquemos el método create()
en DatetimeNode
para analizar el argumento de
formato opcional usando $tag->parser
.
<?php
namespace App\Latte;
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
{
// Agregamos una propiedad pública para almacenar el nodo de expresión de formato analizado
public ?ExpressionNode $format = null;
public static function create(Tag $tag): self
{
$node = $tag->node = new self;
// Verificamos si existen algunos tokens
if (!$tag->parser->isEnd()) {
// Analizamos el argumento como una expresión similar a PHP usando TagParser.
$node->format = $tag->parser->parseExpression();
}
return $node;
}
// ... los métodos print() y getIterator() se actualizarán a continuación ...
}
Agregamos una propiedad pública $format
. En create()
, ahora usamos
$tag->parser->isEnd()
para verificar si existen argumentos. Si es así,
$tag->parser->parseExpression()
procesa los tokens para la expresión. Dado que TagParser
debe
procesar todos los 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}
).
Actualización del método print()
Ahora modifiquemos el método print()
para usar la expresión de formato analizada almacenada en
$this->format
. Si no se proporcionó ningún 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 de código PHP de $formatNode.
return $context->format(
'echo date(%node) %line;',
$formatNode,
$this->position
);
}
En la variable $formatNode
almacenamos el nodo AST que representa la cadena de formato para la función PHP
date()
. Usamos aquí el operador de fusión de null (??
). Si el usuario proporcionó un argumento en la
plantilla (por ejemplo, {datetime 'd.m.Y'}
), entonces la propiedad $this->format
contiene el nodo
correspondiente (en este caso, un StringNode
con el valor 'd.m.Y'
), y se usa este nodo. Si el usuario no
proporcionó un argumento (solo escribió {datetime}
), la propiedad $this->format
es
null
, y en su lugar creamos un nuevo StringNode
con el formato predeterminado
'Y-m-d H:i:s'
. Esto asegura que $formatNode
siempre contenga un nodo AST válido para el formato.
En la máscara 'echo date(%node) %line;'
se utiliza un nuevo placeholder %node
, que le dice al
método format()
que tome el primer argumento siguiente (que es nuestro $formatNode
), llame a su método
print()
(que devolverá su representación de código PHP) e inserte el resultado en la posición del
placeholder.
Implementación de getIterator()
para subnodos
Nuestro DatetimeNode
ahora tiene un nodo hijo: la expresión $format
. Debemos hacer que este
nodo hijo sea accesible para los pasos de compilación proporcionándolo en el método getIterator()
. No olvide
proporcionar una referencia (&
) para permitir que los pasos reemplacen potencialmente el nodo.
public function &getIterator(): \Generator
{
if ($this->format) {
yield $this->format;
}
}
¿Por qué es esto crucial? Imagine un paso de Sandbox que necesita verificar si el argumento $format
no contiene
una llamada a función prohibida (por ejemplo, {datetime dangerousFunction()}
). Si getIterator()
no
proporciona $this->format
, el paso de Sandbox nunca vería la llamada a dangerousFunction()
dentro
del argumento de nuestra etiqueta, lo que crearía una posible brecha de seguridad. Al proporcionarlo, permitimos que Sandbox (y
otros pasos) inspeccionen y potencialmente modifiquen el nodo de expresión $format
.
Uso de la etiqueta mejorada
La etiqueta ahora maneja correctamente el argumento opcional:
Formato predeterminado: {datetime}
Formato personalizado: {datetime 'd.m.Y'}
Uso de variable: {datetime $userDateFormatPreference}
{* Esto causaría un error después de analizar 'd.m.Y', porque ", foo" es inesperado *}
{* {datetime 'd.m.Y', foo} *}
A continuación, veremos la creación de etiquetas emparejadas que procesan el contenido entre ellas.
Manejo de etiquetas emparejadas
Hasta ahora, nuestra etiqueta {datetime}
era auto-cerrada (conceptualmente). No tiene contenido entre la
etiqueta de apertura y la de cierre. Sin embargo, muchas etiquetas útiles trabajan con un bloque de contenido de plantilla. Estas
se llaman etiquetas emparejadas. Ejemplos incluyen {if}...{/if}
, {block}...{/block}
o una
etiqueta personalizada que ahora crearemos: {debug}...{/debug}
.
Esta etiqueta nos permitirá incluir información de depuración en nuestras plantillas que solo debería ser visible durante el desarrollo.
Objetivo: Crear una etiqueta emparejada {debug}
, cuyo contenido se renderiza solo si está activa una
bandera específica de “modo de desarrollo”.
Introducción a los proveedores
A veces, sus etiquetas necesitan acceso a datos o servicios que no se pasan directamente como parámetros de plantilla. Por ejemplo, determinar si la aplicación está en modo de desarrollo, acceder al objeto de usuario u obtener valores de configuración. Latte proporciona un mecanismo llamado proveedores (Providers) para este propósito.
Los proveedores se registran en su extensión
usando el método getProviders()
. Este método devuelve un array asociativo donde las claves son los nombres bajo los
cuales los proveedores serán accesibles en el código de tiempo de ejecución de la plantilla, y los valores son los datos
u objetos reales.
Dentro del código PHP generado por el método print()
de su etiqueta, puede acceder a estos proveedores a través
de la propiedad especial del objeto $this->global
. Dado que esta propiedad se comparte entre todas las
extensiones, es una buena práctica prefijar los nombres de sus proveedores para evitar posibles colisiones de nombres con
proveedores clave de Latte o proveedores de otras extensioniones de terceros. Una convención común es usar un prefijo corto y
único relacionado con su fabricante o nombre de extensión. Para nuestro ejemplo, usaremos el prefijo app
y la
bandera de modo de desarrollo estará disponible como $this->global->appDevMode
.
La palabra clave yield
para analizar contenido
¿Cómo le decimos al parser de Latte que procese el contenido entre {debug}
y {/debug}
? Aquí
es donde entra en juego la palabra clave yield
.
Cuando se usa yield
en la función create()
, la función se convierte en un generador PHP. Su ejecución se pausa y el control
vuelve al TemplateParser
principal. El TemplateParser
luego continúa analizando el contenido de la
plantilla hasta que encuentra la etiqueta de cierre correspondiente ({/debug}
en nuestro caso).
Una vez que se encuentra la etiqueta de cierre, TemplateParser
reanuda la ejecución de nuestra función
create()
justo después de la sentencia yield
. El valor devuelto por la sentencia
yield
es un array que contiene dos elementos:
- Un
AreaNode
que representa el contenido analizado entre las etiquetas de apertura y cierre. - Un objeto
Tag
que representa la etiqueta de cierre (por ejemplo,{/debug}
).
Creemos la clase DebugNode
y su método create
utilizando yield
.
<?php
namespace App\Latte;
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 almacenar el contenido interno analizado
public AreaNode $content;
/**
* Función de análisis para la etiqueta emparejada {debug} ... {/debug}.
*/
public static function create(Tag $tag): \Generator // observe el tipo de retorno
{
$node = $tag->node = new self;
// Pausar el análisis, obtener el contenido interno y la etiqueta final cuando se encuentre {/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 un n:attribute, es decir,
<div n:debug>...</div>
.
Implementación de print()
para renderizado
condicional
El método print()
ahora necesita generar código PHP que verifique en tiempo de ejecución el proveedor
appDevMode
y solo ejecute el código para el contenido interno si la bandera es true.
public function print(PrintContext $context): string
{
// Genera una sentencia PHP 'if' que verifica el proveedor en tiempo de ejecución
return $context->format(
<<<'XX'
if ($this->global->appDevMode) %line {
// Si está 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
);
}
Esto es simple. Usamos PrintContext::format()
para crear una sentencia PHP if
estándar. Dentro del
if
, colocamos el placeholder %node
para $this->content
. Latte llamará recursivamente a
$this->content->print($context)
para generar el código PHP para la parte interna de la etiqueta, pero solo si
$this->global->appDevMode
se evalúa como true en tiempo de ejecución.
Implementación de getIterator()
para contenido
Al igual que con el nodo de argumento en el ejemplo anterior, nuestro DebugNode
ahora tiene un nodo hijo:
AreaNode $content
. Debemos hacerlo accesible proporcionándolo en getIterator()
:
public function &getIterator(): \Generator
{
// Proporciona una referencia al nodo de contenido
yield $this->content;
}
Esto permite que los pasos de compilación desciendan al contenido de nuestra etiqueta {debug}
, lo cual es
importante incluso si el contenido se renderiza condicionalmente. Por ejemplo, Sandbox necesita analizar el contenido
independientemente de si appDevMode
es true o false.
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 lugar (por ejemplo, desde la configuración)
public function __construct(
private bool $isDevelopmentMode,
) {
}
public function getTags(): array
{
return [
'datetime' => DatetimeNode::create(...),
'debug' => DebugNode::create(...), // Registro de la nueva etiqueta
];
}
public function getProviders(): array
{
return [
'appDevMode' => $this->isDevelopmentMode, // Registro del proveedor
];
}
}
// Al registrar la extensión:
$isDev = true; // Determine esto según el entorno de su aplicación
$latte->addExtension(new App\Latte\MyLatteExtension($isDev));
Y su uso en la plantilla:
<p>Contenido normal visible siempre.</p>
{debug}
<div class="debug-panel">
ID del usuario actual: {$user->id}
Tiempo de la petición: {=time()}
</div>
{/debug}
<p>Otro contenido normal.</p>
Integración de n:attributes
Latte ofrece una notación abreviada conveniente para muchas etiquetas emparejadas: n:attributes. Si tiene una etiqueta emparejada como
{tag}...{/tag}
y desea que su efecto se aplique directamente a un solo elemento HTML, a menudo puede escribirlo de
manera más concisa como un atributo n:tag
en ese elemento.
Para la mayoría de las etiquetas emparejadas estándar que defina (como nuestra {debug}
), Latte habilitará
automáticamente la versión de atributo n:
correspondiente. No necesita hacer nada adicional durante el
registro:
{* Uso estándar de la etiqueta emparejada *}
{debug}<div>Información de depuración</div>{/debug}
{* Uso equivalente con n:attribute *}
<div n:debug>Información de depuración</div>
Ambas versiones renderizarán el <div>
solo si $this->global->appDevMode
es true. Los
prefijos inner-
y tag-
también funcionan como se espera.
A veces, la lógica de su etiqueta puede necesitar comportarse de manera ligeramente diferente dependiendo de si se usa como
una etiqueta emparejada estándar o como un n:attribute, o si se usa un prefijo como n:inner-tag
o
n:tag-tag
. El objeto Latte\Compiler\Tag
, pasado a su función de análisis create()
,
proporciona esta información:
$tag->isNAttribute(): bool
: Devuelvetrue
si la etiqueta se analiza como un n:attribute$tag->prefix: ?string
: Devuelve el prefijo utilizado con el n:attribute, que puede sernull
(no es un n:attribute),Tag::PrefixNone
,Tag::PrefixInner
oTag::PrefixTag
Ahora que entendemos las etiquetas simples, el análisis de argumentos, las etiquetas emparejadas, los proveedores y los
n:attributes, abordemos un escenario más complejo que involucra etiquetas anidadas dentro de otras etiquetas, utilizando nuestra
etiqueta {debug}
como punto de partida.
Etiquetas intermedias
Algunas etiquetas emparejadas permiten o incluso requieren que otras etiquetas aparezcan dentro de ellas antes de la
etiqueta de cierre final. Estas se llaman etiquetas intermedias. Ejemplos clásicos incluyen
{if}...{elseif}...{else}...{/if}
o {switch}...{case}...{default}...{/switch}
.
Ampliemos nuestra etiqueta {debug}
para admitir una cláusula {else}
opcional, que se renderizará
cuando la aplicación no esté en modo de desarrollo.
Objetivo: Modificar {debug}
para admitir una etiqueta intermedia opcional {else}
. La sintaxis
final debería ser {debug} ... {else} ... {/debug}
.
Análisis de 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 final. Sin embargo, yield
ofrece más control: puede proporcionarle un array de nombres de
etiquetas intermedias. Cuando el parser encuentra cualquiera de estas etiquetas especificadas en el mismo nivel de
anidamiento (es decir, como hijos directos de la etiqueta padre, no dentro de otros bloques o etiquetas dentro de ella),
también detiene el análisis.
Cuando el análisis se detiene debido a una etiqueta intermedia, detiene el análisis del contenido, reanuda el generador
create()
y devuelve el contenido parcialmente analizado y la etiqueta intermedia misma (en lugar de la
etiqueta final). Nuestra función create()
puede entonces procesar esta etiqueta intermedia (por ejemplo, analizar
sus argumentos si los tuviera) y usar yield
nuevamente para analizar la siguiente parte del contenido hasta la
etiqueta final final u otra etiqueta intermedia esperada.
Modifiquemos DebugNode::create()
para esperar {else}
:
<?php
namespace App\Latte;
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;
// yield y esperar {/debug} o {else}
[$node->thenContent, $nextTag] = yield ['else'];
// Verificar si la etiqueta en la que nos detuvimos fue {else}
if ($nextTag?->name === 'else') {
// Yield de nuevo para analizar 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 para {/debug}
, sino también
para {else}
. Si se encuentra {else}
, $nextTag
contendrá el objeto Tag
para
{else}
. Luego usamos yield
nuevamente sin argumentos, lo que significa que ahora solo esperamos la
etiqueta final {/debug}
, y almacenamos el resultado en $node->elseContent
. Si no se encontró
{else}
, $nextTag
sería el Tag
para {/debug}
(o null
si se usa
como n:attribute) y $node->elseContent
permanecería null
.
Implementación de print()
con {else}
El método print()
necesita reflejar la nueva estructura. Debería generar una sentencia PHP if/else
basada en el proveedor devMode
.
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 para la condición 'if'
$this->thenContent, // Primer placeholder %node
$this->elseContent ?? new NopNode, // Segundo placeholder %node
);
}
Esta es una estructura PHP if/else
estándar. Usamos %node
dos veces; format()
reemplaza
los nodos proporcionados secuencialmente. Usamos ?? new NopNode
para evitar errores si
$this->elseContent
es null
– NopNode
simplemente no imprime nada.
Implementación de getIterator()
para ambos contenidos
Ahora tenemos potencialmente dos nodos hijos de contenido ($thenContent
y $elseContent
). Debemos
proporcionar ambos si existen:
public function &getIterator(): \Generator
{
yield $this->thenContent;
if ($this->elseContent) {
yield $this->elseContent;
}
}
Uso de la etiqueta mejorada
La etiqueta ahora se puede usar con la cláusula {else}
opcional:
{debug}
<p>Mostrando información de depuración porque devMode está ACTIVADO.</p>
{else}
<p>La información de depuración está oculta porque devMode está DESACTIVADO.</p>
{/debug}
Manejo de estado y anidamiento
Nuestros ejemplos anteriores ({datetime}
, {debug}
) eran relativamente sin estado dentro de sus
métodos print()
. O bien imprimían directamente el contenido o realizaban una simple verificación condicional
basada en un proveedor global. Sin embargo, muchas etiquetas necesitan administrar alguna forma de estado durante la
renderización o implican la evaluación de expresiones de usuario que deberían ejecutarse solo una vez por rendimiento
o corrección. Además, debemos considerar qué sucede cuando nuestras etiquetas personalizadas están anidadas.
Ilustremos estos conceptos creando una etiqueta {repeat $count}...{/repeat}
. Esta etiqueta repetirá su contenido
interno $count
veces.
Objetivo: Implementar {repeat $count}
, que repite su contenido un número específico de veces.
La necesidad de variables temporales y únicas
Imagine que un usuario escribe:
{repeat rand(1, 5)} Contenido {/repeat}
Si generáramos ingenuamente un bucle for
de PHP de esta manera en nuestro método print()
:
// Código generado simplificado, INCORRECTO
for ($i = 0; $i < rand(1, 5); $i++) {
// imprimir contenido
}
¡Esto estaría mal! La expresión rand(1, 5)
se volvería a evaluar en cada iteración del bucle, lo que
llevaría a un número impredecible de repeticiones. Necesitamos evaluar la expresión $count
una vez antes
de que comience el bucle y almacenar su resultado.
Generaremos código PHP que primero evalúe la expresión de conteo y la almacene en una variable temporal en tiempo de
ejecución. Para evitar colisiones con variables definidas por el usuario de la plantilla y variables internas de Latte
(como $ʟ_...
), usaremos la convención de prefijar $__
(doble guion bajo) para nuestras
variables temporales.
El código generado se vería así:
$__count = rand(1, 5);
for ($__i = 0; $__i < $__count; $__i++) {
// imprimir contenido
}
Ahora considere el anidamiento:
{repeat $countA} {* Bucle externo *}
{repeat $countB} {* Bucle interno *}
...
{/repeat}
{/repeat}
Si tanto la etiqueta {repeat}
externa como la interna generaran código usando los mismos nombres de
variables temporales (por ejemplo, $__count
y $__i
), el bucle interno sobrescribiría las variables del
bucle externo, rompiendo la lógica.
Necesitamos asegurarnos de que las variables temporales generadas para cada instancia de la etiqueta {repeat}
sean
únicas. Logramos esto usando PrintContext::generateId()
. Este método devuelve un entero único durante la
fase de compilación. Podemos agregar este ID a los nombres de nuestras variables temporales.
Entonces, en lugar de $__count
, generaremos $__count_1
para la primera etiqueta repeat,
$__count_2
para la segunda, etc. De manera similar, para el contador del bucle, usaremos $__i_1
,
$__i_2
, etc.
Implementación de RepeatNode
Vamos a crear la clase de nodo.
<?php
namespace App\Latte;
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;
/**
* Función de análisis para {repeat $count} ... {/repeat}
*/
public static function create(Tag $tag): \Generator
{
$tag->expectArguments(); // se asegura de que $count se proporcione
$node = $tag->node = new self;
// Analiza la expresión de conteo
$node->count = $tag->parser->parseExpression();
// Obtiene el contenido interno
[$node->content] = yield;
return $node;
}
/**
* Genera un bucle 'for' de PHP con nombres de variables únicos.
*/
public function print(PrintContext $context): string
{
// Generación de nombres de variables únicos
$id = $context->generateId();
$countVar = '$__count_' . $id; // ej. $__count_1, $__count_2, etc.
$iteratorVar = '$__i_' . $id; // ej. $__i_1, $__i_2, etc.
return $context->format(
<<<'XX'
// Evaluar la expresión de conteo *una vez* y almacenar
%raw = (int) (%node);
// Bucle usando el conteo almacenado y una variable de iteración única
for (%raw = 0; %2.raw < %0.raw; %2.raw++) %line {
%node // Renderizar el contenido interno
}
XX,
$countVar, // %0 - Variable para almacenar el conteo
$this->count, // %1 - Nodo de expresión para el conteo
$iteratorVar, // %2 - Nombre de la variable de iteración del bucle
$this->position, // %3 - Comentario con el número de línea para el bucle mismo
$this->content // %4 - Nodo del contenido interno
);
}
/**
* Proporciona los nodos hijos (expresión de conteo y contenido).
*/
public function &getIterator(): \Generator
{
yield $this->count;
yield $this->content;
}
}
El método create()
analiza la expresión $count
requerida usando parseExpression()
.
Primero se llama a $tag->expectArguments()
. Esto asegura que el usuario proporcionó algo después de
{repeat}
. Si bien $tag->parser->parseExpression()
fallaría si no se proporcionara nada, el
mensaje de error podría ser sobre sintaxis inesperada. Usar expectArguments()
proporciona un error mucho más claro,
indicando específicamente que faltan argumentos para la etiqueta {repeat}
.
El método print()
genera el código PHP responsable de ejecutar la lógica de repetición en tiempo de
ejecución. Comienza generando nombres únicos para las variables PHP temporales que necesitará.
Se llama al método $context->format()
con un nuevo placeholder %raw
, que inserta la cadena
cruda proporcionada como el argumento correspondiente. Aquí inserta el nombre de variable único almacenado en
$countVar
(por ejemplo, $__count_1
). ¿Y qué pasa con %0.raw
y %2.raw
? Esto
demuestra los placeholders posicionales. En lugar de simplemente %raw
, que toma el siguiente argumento
crudo disponible, %2.raw
toma explícitamente el argumento en el índice 2 (que es $iteratorVar
) e
inserta su valor de cadena cruda. Esto nos permite reutilizar la cadena $iteratorVar
sin pasarla varias veces en la
lista de argumentos para format()
.
Esta llamada a format()
cuidadosamente construida genera un bucle PHP eficiente y seguro que maneja correctamente
la expresión de conteo y evita colisiones de nombres de variables incluso cuando las etiquetas {repeat}
están
anidadas.
Registro y uso
Registre la etiqueta en su extensión:
use App\Latte\RepeatNode;
class MyLatteExtension extends Extension
{
public function getTags(): array
{
return [
'datetime' => DatetimeNode::create(...),
'debug' => DebugNode::create(...),
'repeat' => RepeatNode::create(...), // Registro de la etiqueta repeat
];
}
}
Úsela en la plantilla, incluido el anidamiento:
{var $rows = rand(5, 7)}
{var $cols = rand(3, 5)}
{repeat $rows}
<tr>
{repeat $cols}
<td>Bucle interno</td>
{/repeat}
</tr>
{/repeat}
Este ejemplo demuestra cómo manejar el estado (contadores de bucle) y posibles problemas de anidamiento utilizando variables
temporales con prefijo $__
y únicas con un ID de PrintContext::generateId()
.
n:attributes puros
Si bien muchos n:attributes
como n:if
o n:foreach
sirven como atajos convenientes para
sus contrapartes de etiquetas emparejadas ({if}...{/if}
, {foreach}...{/foreach}
), Latte también permite
definir etiquetas que existen solo en forma de n:attribute. Estos se usan a menudo para modificar atributos o el
comportamiento del elemento HTML al que están adjuntos.
Ejemplos estándar incorporados en Latte incluyen n:class
, que ayuda a construir dinámicamente el atributo
class
, y n:attr
, que puede establecer
múltiples atributos arbitrarios.
Creemos nuestro propio n:attribute puro: n:confirm
, que agregará un diálogo de confirmación de JavaScript antes
de realizar una acción (como seguir un enlace o enviar un formulario).
Objetivo: Implementar n:confirm="'¿Estás seguro?'"
, que agrega un manejador onclick
para
prevenir la acción predeterminada si el usuario cancela el diálogo de confirmación.
Implementación de ConfirmNode
Necesitamos una clase Node y una función de análisis.
<?php
namespace App\Latte;
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;
}
/**
* Genera el código del atributo 'onclick' con el escapado correcto.
*/
public function print(PrintContext $context): string
{
// Asegura el escapado correcto para los contextos de atributo JavaScript y HTML.
return $context->format(
<<<'XX'
echo ' onclick="', LR\Filters::escapeHtmlAttr('return confirm(' . LR\Filters::escapeJs(%node) . ')'), '"' %line;
XX,
$this->message,
$this->position,
);
}
public function &getIterator(): \Generator
{
yield $this->message;
}
}
El método print()
genera código PHP que finalmente, durante la renderización de la plantilla, imprimirá el
atributo HTML onclick="..."
. El manejo de contextos anidados (JavaScript dentro de un atributo HTML) requiere un
escapado cuidadoso. El filtro LR\Filters::escapeJs(%node)
se llama en tiempo de ejecución y escapa el mensaje
correctamente para su uso dentro de JavaScript (la salida sería como "Sure?"
). Luego, el filtro
LR\Filters::escapeHtmlAttr(...)
escapa los caracteres que son especiales en los atributos HTML, por lo que cambiaría
la salida a return confirm("Sure?")
. Este escapado de dos pasos en tiempo de ejecución asegura que
el mensaje sea seguro para JavaScript y que el código JavaScript resultante sea seguro para incrustar en el atributo HTML
onclick
.
Registro y uso
Registre el n:attribute en su extensión. No olvide 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(...), // Registro de n:confirm
];
}
}
Ahora puede usar n:confirm
en enlaces, botones o elementos de formulario:
<a href="delete.php?id=123" n:confirm='"¿Realmente quieres eliminar el elemento {$id}?"'>Eliminar</a>
HTML generado:
<a href="delete.php?id=123" onclick="return confirm("¿Realmente quieres eliminar el elemento 123?")">Eliminar</a>
Cuando el usuario hace clic en el enlace, el navegador ejecuta el código onclick
, muestra el diálogo de
confirmación y solo navega a delete.php
si el usuario hace clic en “Aceptar”.
Este ejemplo demuestra cómo se puede crear un n:attribute puro para modificar el comportamiento o los atributos de su
elemento HTML anfitrión generando el código PHP apropiado en su método print()
. No olvide el doble escapado que a
menudo se requiere: una vez para el contexto de destino (JavaScript en este caso) y nuevamente para el contexto del
atributo HTML.
Temas avanzados
Si bien las secciones anteriores cubren los conceptos básicos, aquí hay algunos temas más avanzados que puede encontrar al crear etiquetas Latte personalizadas.
Modos de salida de etiquetas
El objeto Tag
pasado a su función create()
tiene una propiedad outputMode
. Esta
propiedad afecta cómo Latte maneja los espacios en blanco y la indentación circundantes, especialmente cuando la etiqueta se usa
en su propia línea. Puede modificar esta propiedad en su función create()
.
Tag::OutputKeepIndentation
(Predeterminado para la mayoría de las etiquetas como{=...}
): Latte intenta preservar la indentación antes de la etiqueta. Las nuevas líneas después de la etiqueta generalmente se conservan. Esto es adecuado para etiquetas que imprimen contenido en línea.Tag::OutputRemoveIndentation
(Predeterminado para etiquetas de bloque como{if}
,{foreach}
): Latte elimina la indentación inicial y potencialmente una nueva línea siguiente. Esto ayuda a mantener el código PHP generado más limpio y evita líneas vacías adicionales en la salida HTML causadas por la propia etiqueta. Use esto para etiquetas que representan estructuras de control o bloques que no deberían agregar espacios en blanco por sí mismos.Tag::OutputNone
(Usado por etiquetas como{var}
,{default}
): Similar aRemoveIndentation
, pero indica más fuertemente que la etiqueta en sí no produce salida directa, lo que potencialmente afecta el manejo de espacios en blanco a su alrededor de manera aún más agresiva. Adecuado para etiquetas declarativas o de configuración.
Elija el modo que mejor se adapte al propósito de su etiqueta. Para la mayoría de las etiquetas estructurales o de control,
OutputRemoveIndentation
suele ser apropiado.
Acceso a etiquetas padre/más cercanas
A veces, el comportamiento de una etiqueta necesita depender del contexto en el que se usa, específicamente en qué
etiqueta(s) padre se encuentra. El objeto Tag
pasado a su función create()
proporciona el método
closestTag(array $classes, ?callable $condition = null): ?Tag
exactamente para este propósito.
Este método busca hacia arriba en la jerarquía de etiquetas actualmente abiertas (incluidos los elementos HTML representados
internamente durante el análisis) y devuelve el objeto Tag
del ancestro más cercano que coincida con criterios
específicos. Si no se encuentra ningún ancestro coincidente, devuelve null
.
El array $classes
especifica qué tipo de etiquetas ancestro está buscando. Comprueba si el nodo asociado de la
etiqueta ancestro ($ancestorTag->node
) es una instancia de esta clase.
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 instancia de ForeachNode misma:
$foreachNode = $foreachTag->node;
}
}
Observe $foreachTag->node
: Esto solo funciona porque es una convención en el desarrollo de etiquetas Latte
asignar inmediatamente el nodo creado a $tag->node
dentro del método create()
, como siempre
hemos hecho.
A veces, simplemente comparar el tipo de nodo no es suficiente. Es posible que necesite verificar una propiedad específica de
la etiqueta ancestro potencial o su nodo. El segundo argumento opcional para closestTag()
es un callable que recibe
el objeto Tag
ancestro potencial 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 que son conscientes del contexto y hacer cumplir el uso correcto
dentro de la estructura de su plantilla, lo que lleva a plantillas más robustas y comprensibles.
Placeholders de PrintContext::format()
A menudo hemos usado 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 reemplazan los placeholders en la máscara. Aquí hay
un resumen de los placeholders 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 PHP. Exporta el valor a código PHP válido. Adecuado para escalares, arrays, 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 ningún escapado o modificación. Use con precaución, principalmente para insertar fragmentos de código PHP pregenerados o nombres de variables.$context->format('%raw = 1;', '$variableName')
→$variableName = 1;
%args
: El argumento debe ser unExpression\ArrayNode
. Imprime los elementos del array formateados como argumentos para una llamada a función o método (separados por comas, maneja argumentos con nombre si están presentes).$argsNode = new ArrayNode([...]);
$context->format('myFunc(%args);', $argsNode)
→myFunc(1, name: 'Joe');
%line
: El argumento debe ser un objetoPosition
(generalmente$this->position
). Inserta un comentario PHP/* line X */
que indica el número de línea de origen.$context->format('echo "Hi" %line;', $this->position)
→echo "Hi" /* line 42 */;
%escape(...)
: Genera código PHP que escapa en tiempo de ejecución la expresión interna utilizando las reglas de escapado actuales conscientes del contexto.$context->format('echo %escape(%node);', $variableNode)
%modify(...)
: El argumento debe ser unModifierNode
. Genera código PHP que aplica los filtros especificados en elModifierNode
al contenido interno, incluido el escapado consciente del contexto si no está deshabilitado con|noescape
.$context->format('%modify(%node);', $modifierNode, $variableNode)
%modifyContent(...)
: Similar a%modify
, pero diseñado para modificar bloques de contenido capturado (a menudo HTML).
Puede hacer referencia explícita a los argumentos por su índice (basado en cero): %0.node
, %1.dump
,
%2.raw
, etc. Esto le permite reutilizar un argumento varias veces en la máscara sin pasarlo repetidamente a
format()
. Consulte el ejemplo de la etiqueta {repeat}
, donde se usaron %0.raw
y
%2.raw
.
Ejemplo de análisis complejo de argumentos
Si bien parseExpression()
, parseArguments()
, etc., cubren muchos casos, a veces necesita una lógica
de análisis más compleja utilizando el TokenStream
de nivel inferior disponible a través de
$tag->parser->stream
.
Objetivo: Crear una etiqueta {embedYoutube $videoID, width: 640, height: 480}
. Queremos analizar el ID de
video requerido (cadena o variable) seguido de pares clave-valor opcionales para las dimensiones.
<?php
namespace App\Latte;
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;
// Analizar el ID de video requerido
$node->videoId = $tag->parser->parseExpression();
// Analizar pares clave-valor opcionales
$stream = $tag->parser->stream; // Obtener el flujo de tokens
while ($stream->tryConsume(',')) { // Requiere separación por coma
// Esperar el identificador 'width' o 'height'
$keyToken = $stream->consume(Token::Php_Identifier);
$key = strtolower($keyToken->text);
$stream->consume(':'); // Esperar el separador de dos puntos
$value = $tag->parser->parseExpression(); // Analizar la expresión del valor
if ($key === 'width') {
$node->width = $value;
} elseif ($key === 'height') {
$node->height = $value;
} else {
throw new CompileException("Argumento desconocido '$key'. Se esperaba 'width' o 'height'.", $keyToken->position);
}
}
return $node;
}
}
Este nivel de control le permite definir sintaxis muy específicas y complejas para sus etiquetas personalizadas interactuando directamente con el flujo de tokens.
Uso de AuxiliaryNode
Latte proporciona nodos “auxiliares” genéricos para situaciones especiales durante la generación de código o dentro de
los pasos de compilación. Son AuxiliaryNode
y Php\Expression\AuxiliaryNode
.
Considere AuxiliaryNode
como un nodo contenedor flexible que delega sus funcionalidades principales (generación
de código y exposición de nodos hijos) a los argumentos proporcionados en su constructor:
- Delegación de
print()
: El primer argumento del constructor es un closure PHP. Cuando Latte llama al métodoprint()
en unAuxiliaryNode
, ejecuta este closure proporcionado. El closure recibe unPrintContext
y cualquier nodo pasado en el segundo argumento del constructor, lo que le permite definir una lógica de generación de código PHP completamente personalizada en tiempo de ejecución. - 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 pasos de compilación), su métodogetIterator()
simplemente proporciona los nodos listados en este 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 son proporcionados por el método getIterator() y pasados al closure anterior
[$argumentNode1, $argumentNode2]
);
Latte proporciona dos tipos distintos basados en dónde necesita insertar el código generado:
Latte\Compiler\Nodes\Php\Expression\AuxiliaryNode
: Use esto cuando necesite generar un fragmento de código PHP que represente una expresiónLatte\Compiler\Nodes\AuxiliaryNode
: Use esto para propósitos más generales, cuando necesite insertar un bloque de código PHP que represente una o más sentencias
Una razón importante para usar AuxiliaryNode
en lugar de nodos estándar (como StaticMethodCallNode
)
dentro de su método print()
o paso de compilación es controlar la visibilidad para los pasos de compilación
posteriores, especialmente aquellos relacionados con la seguridad como Sandbox.
Considere un escenario: Su paso de compilación necesita envolver una expresión proporcionada por el usuario
($userExpr
) con una llamada a una función auxiliar específica y confiable
myInternalSanitize($userExpr)
. Si crea un nodo estándar
new FunctionCallNode('myInternalSanitize', [$userExpr])
, será completamente visible para el recorrido del AST. Si un
paso de Sandbox se ejecuta más tarde y myInternalSanitize
no está en su lista blanca, Sandbox podría
bloquear o modificar esta llamada, interrumpiendo potencialmente la lógica interna de su etiqueta, incluso si
usted, el autor de la etiqueta, sabe que esta llamada específica es segura y necesaria. Por lo tanto, puede generar la
llamada directamente dentro del closure de AuxiliaryNode
.
use Latte\Compiler\Nodes\Php\Expression\AuxiliaryNode;
// ... dentro de print() o un paso de compilación ...
$wrappedNode = new AuxiliaryNode(
fn(PrintContext $context, $userExpr) => $context->format(
'myInternalSanitize(%node)', // Generación directa de código PHP
$userExpr,
),
// IMPORTANTE: ¡Aún así, pase el nodo de expresión de usuario original aquí!
[$userExpr],
);
En este caso, el paso de 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.
Si bien el código PHP generado en sí está oculto a los pasos, las entradas a ese código (nodos que representan datos
o expresiones del usuario) aún deben ser recorribles. Por eso, el segundo argumento del constructor de
AuxiliaryNode
es crucial. Debe pasar un array que contenga todos los nodos originales (como
$userExpr
en el ejemplo anterior) que usa su closure. El getIterator()
de AuxiliaryNode
proporcionará estos nodos, permitiendo que los pasos de compilación como Sandbox los analicen en busca de posibles
problemas.
Mejores prácticas
- Propósito claro: Asegúrese de que su etiqueta tenga un propósito claro y necesario. No cree etiquetas para tareas que se puedan resolver fácilmente con filtros o funciones.
- Implemente
getIterator()
correctamente: Siempre implementegetIterator()
y proporcione referencias (&
) a todos los nodos hijos (argumentos, contenido) que se analizaron desde la plantilla. Esto es esencial para los pasos de compilación, la seguridad (Sandbox) y posibles optimizaciones futuras. - Propiedades públicas para nodos: Haga públicas las propiedades que contienen nodos hijos para que los pasos de compilación puedan modificarlos si es necesario.
- Use
PrintContext::format()
: Utilice el métodoformat()
para generar código PHP. Maneja las comillas, escapa correctamente los placeholders y agrega comentarios con el número de línea automáticamente. - Variables temporales (
$__
): Al generar código PHP en tiempo de ejecución que necesita variables temporales (por ejemplo, para almacenar subtotales, contadores de bucle), use la convención de prefijo$__
para evitar colisiones con variables de usuario y variables internas de Latte$ʟ_
. - Anidamiento e IDs únicos: Si su etiqueta puede anidarse o necesita un estado específico de instancia en tiempo de
ejecución, use
$context->generateId()
dentro de su métodoprint()
para crear sufijos únicos para sus variables temporales$__
. - Proveedores para datos externos: Use proveedores (registrados a través de
Extension::getProviders()
) para acceder a datos o servicios en tiempo de ejecución ($this->global->…) en lugar de codificar valores o depender del estado global. Use prefijos de fabricante para los nombres de los proveedores. - Considere los n:attributes: Si su etiqueta emparejada opera lógicamente en un solo elemento HTML, Latte probablemente
proporciona soporte automático de
n:attribute
. Tenga esto en cuenta para la conveniencia del usuario. Si está creando una etiqueta modificadora de atributos, considere si unn:attribute
puro es la forma más apropiada. - Pruebas: Escriba pruebas para sus etiquetas, cubriendo tanto el análisis de diferentes entradas sintácticas como la corrección de la salida del código PHP generado.
Siguiendo estas pautas, puede crear etiquetas personalizadas potentes, robustas y mantenibles que se integren perfectamente con el motor de plantillas Latte.
Estudiar las clases de nodos que forman parte de Latte es la mejor manera de aprender todos los detalles sobre el proceso de análisis.