Nette Documentation Preview

syntax
Creación de etiquetas personalizadas
************************************

.[perex]
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 |extending-latte#Formas de extender Latte] o si ya hay una etiqueta adecuada en el [conjunto estándar |tags]. 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:

1.  **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.).
2.  **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)**.
3.  **Pases del compilador:** antes de generar el código PHP, Latte ejecuta los [pases del compilador|compiler-passes]. Son funciones que recorren todo el AST y pueden modificarlo o recopilar información. Este paso es crucial para funciones como la seguridad ([Sandbox|sandbox]) o las optimizaciones.
4.  **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.
5.  **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:

```php
$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 `.latte` usted 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á con `Latte\Compiler\Nodes\TextNode` para el texto estático, `Latte\Compiler\Nodes\Html\ElementNode` para los elementos HTML, `Latte\Compiler\Nodes\Php\ExpressionNode` para las expresiones dentro de las etiquetas y, lo más importante para las etiquetas propias, nodos que heredan de `Latte\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 [api:Latte\Compiler\Tag], y el [api: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 [api:Latte\Compiler\TagParser], accesible mediante `$tag->parser` (es un parser distinto del que analiza toda la plantilla).
- En las etiquetas pares, usa `yield` para 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, `ExpressionNode` para los argumentos analizados o `AreaNode` para el contenido analizado).
- El método `print(PrintContext $context): string` genera 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(): \Generator` hace 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|compiler-passes]. 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 un `getIterator()` completo, es crucial. Permite que funciones potentes como el [Sandbox|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 |extending-latte#Latte Extension].
- 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
<?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 [api: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 |extending-latte#Latte Extension] (por ejemplo, `MyLatteExtension.php`) y registre la etiqueta en su método `getTags()`.

```php
<?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:

```php
$latte = new Latte\Engine;
$latte->addExtension(new App\Templating\MyLatteExtension);
```

Cree la plantilla:

```latte
<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, [api: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, `foo` se 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 un `StringNode`.
- `parseArguments(): ArrayNode`: analiza argumentos separados por comas, eventualmente con claves, como `10, name: 'John', true`.
- `parseModifier(): ModifierNode`: analiza filtros como `|upper|truncate:10`.
- `parseType(): ?SuperiorTypeNode`: analiza declaraciones de tipo de PHP, como `int`, `?string`, `array|Foo`.

Para necesidades de análisis más complejas o de bajo nivel, puede interactuar directamente con el [flujo de tokens|api:Latte\Compiler\TokenStream] 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 una `CompileException`. Ú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 devuelve `null`. Ú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
<?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'`.

```php
	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.

```php
	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:

```latte
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 |extending-latte#Latte Extension] 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 |https://www.php.net/manual/en/language.generators.overview.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:

1.  Un `AreaNode` que representa el contenido analizado entre la etiqueta de apertura y la de cierre.
2.  El objeto `Tag` que representa la etiqueta de cierre (por ejemplo, `{/debug}`).

Creemos la clase `DebugNode` y su método `create` usando `yield`.

```php
<?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.

```php
	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()`:

```php
	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:

```php
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:

```latte
<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 |syntax#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:

```latte
{* 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`: devuelve `true` si la etiqueta se está analizando como n:atributo
- `$tag->prefix: ?string`: devuelve el prefijo usado con el n:atributo, que puede ser `null` (no es un n:atributo), `Tag::PrefixNone`, `Tag::PrefixInner` o `Tag::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
<?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`.

```php
	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:

```php
	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:

```latte
{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:

```latte
{repeat rand(1, 5)} Content {/repeat}
```

Si en nuestro método `print()` generáramos ingenuamente un bucle `for` de PHP como este:

```php
// 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í:

```php
$__count = rand(1, 5);
for ($__i = 0; $__i < $__count; $__i++) {
	// imprime el contenido
}
```

Considere ahora el anidamiento:

```latte
{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
<?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:

```php
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:

```latte
{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` |tags#n:class], que ayuda a construir dinámicamente el atributo `class`, y [`n:attr` |tags#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
<?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(&quot;Sure?&quot;)`. 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:

```php
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:

```latte
<a href="delete.php?id=123" n:confirm='"Do you really want to delete item {$id}?"'>Delete</a>
```

HTML generado:

```latte
<a href="delete.php?id=123" onclick="return confirm(&quot;Do you really want to delete item 123?&quot;)">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 con `OutputRemoveIndentation`: 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.

```php
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.

```php
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 de `Node`. Llama al método `print()` 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 un `Expression\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 objeto `Position` (o `Range`), 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 un `ModifierNode`. Genera código PHP que aplica al contenido interior los filtros indicados en el `ModifierNode`, 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
<?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étodo `print()` de un `AuxiliaryNode`, ejecuta ese closure. El closure recibe el `PrintContext` y 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 objetos `Node`**. Cuando Latte necesita recorrer los hijos de un `AuxiliaryNode` (por ejemplo, durante los pases del compilador), su método `getIterator()` se limita a devolver los nodos de ese array.

Ejemplo:

```php
$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ón**
- `Latte\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`.

```php
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|custom-filters] o [funciones|custom-functions].
- **Implemente `getIterator()` correctamente:** implemente siempre `getIterator()` 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étodo `format()` 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étodo `print()` 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 un `n:atributo` puro 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.

.[note]
Estudiar las clases de nodo que forman parte de Latte es la mejor manera de aprender todos los entresijos del proceso de análisis.

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:

  1. 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.).
  2. 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).
  3. 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.
  4. 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.
  5. 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 .latte usted 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á con Latte\Compiler\Nodes\TextNode para el texto estático, Latte\Compiler\Nodes\Html\ElementNode para los elementos HTML, Latte\Compiler\Nodes\Php\ExpressionNode para las expresiones dentro de las etiquetas y, lo más importante para las etiquetas propias, nodos que heredan de Latte\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 yield para 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, ExpressionNode para los argumentos analizados o AreaNode para el contenido analizado).
  • El método print(PrintContext $context): string genera 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(): \Generator hace 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 un getIterator() 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, foo se 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 un StringNode.
  • parseArguments(): ArrayNode: analiza argumentos separados por comas, eventualmente con claves, como 10, name: 'John', true.
  • parseModifier(): ModifierNode: analiza filtros como |upper|truncate:10.
  • parseType(): ?SuperiorTypeNode: analiza declaraciones de tipo de PHP, como int, ?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 una CompileException. Ú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 devuelve null. Ú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:

  1. Un AreaNode que representa el contenido analizado entre la etiqueta de apertura y la de cierre.
  2. El objeto Tag que 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: devuelve true si la etiqueta se está analizando como n:atributo
  • $tag->prefix: ?string: devuelve el prefijo usado con el n:atributo, que puede ser null (no es un n:atributo), Tag::PrefixNone, Tag::PrefixInnerTag::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(&quot;Sure?&quot;). 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(&quot;Do you really want to delete item 123?&quot;)">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 con OutputRemoveIndentation: 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 de Node. Llama al método print() 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 un Expression\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 objeto Position (o Range), 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 un ModifierNode. Genera código PHP que aplica al contenido interior los filtros indicados en el ModifierNode, 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étodo print() de un AuxiliaryNode, ejecuta ese closure. El closure recibe el PrintContext y 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 objetos Node. Cuando Latte necesita recorrer los hijos de un AuxiliaryNode (por ejemplo, durante los pases del compilador), su método getIterator() 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ón
  • Latte\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 filtrosfunciones.
  • Implemente getIterator() correctamente: implemente siempre getIterator() 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étodo format() 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étodo print() 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 un n:atributo puro 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.