Creación de pases del compilador
Los pases del compilador ofrecen un mecanismo potente para analizar y modificar las plantillas de Latte después de que se hayan analizado en un árbol de sintaxis abstracta (AST) y antes de que se genere el código PHP final. Esto permite manipulaciones avanzadas de las plantillas, optimizaciones, comprobaciones de seguridad (como el Sandbox) y la recopilación de información sobre las plantillas. Esta guía le acompañará en la creación de sus propios pases del compilador.
¿Qué es un pase del compilador?
Para entender el papel de los pases del compilador, vea el proceso de compilación de Latte. Como se aprecia, los pases del compilador actúan en una fase crucial y permiten intervenir a fondo entre el análisis inicial y la salida final del código.
En esencia, un pase del compilador no es más que un callable de PHP (una función, un método estático o un método de
instancia) que acepta un argumento: el nodo raíz del AST de la plantilla, que siempre es una instancia de
Latte\Compiler\Nodes\TemplateNode.
El objetivo principal de un pase del compilador suele ser uno de estos dos, o ambos:
- Análisis: recorrer el AST y reunir información sobre la plantilla (por ejemplo, encontrar todos los bloques definidos, comprobar el uso de determinadas etiquetas o asegurar que se cumplen ciertas restricciones de seguridad).
- Modificación: cambiar la estructura del AST o las propiedades de los nodos (por ejemplo, añadir atributos HTML automáticamente, optimizar ciertas combinaciones de etiquetas, sustituir etiquetas obsoletas por nuevas o aplicar reglas de sandbox).
Registro
Los pases del compilador se registran mediante el método getPasses() de una extensión. Este método
devuelve un array asociativo donde las claves son nombres únicos de los pases (usados internamente y para el orden) y los valores
son los callables de PHP que implementan la lógica del pase.
use Latte\Compiler\Nodes\TemplateNode;
use Latte\Extension;
class MyExtension extends Extension
{
public function getPasses(): array
{
return [
'modificationPass' => $this->modifyTemplateAst(...),
// ... otros passes ...
];
}
public function modifyTemplateAst(TemplateNode $templateNode): void
{
// Implementación...
}
}
Los pases registrados por las extensiones del núcleo de Latte y por sus extensiones propias se ejecutan de forma secuencial.
El orden puede ser importante, sobre todo si un pase depende de los resultados o las modificaciones de otro. Latte ofrece un
mecanismo auxiliar para controlar ese orden si hace falta; vea los detalles en la documentación de Extension::getPasses().
Ejemplo de AST
Para hacerse una idea mejor del AST, añadimos una muestra. Esta es la plantilla de origen:
{foreach $category->getItems() as $item}
<li>{$item->name|upper}</li>
{else}
no items found
{/foreach}
Y esta es su representación en forma de AST:
Latte\Compiler\Nodes\TemplateNode(
Latte\Compiler\Nodes\FragmentNode(
- Latte\Essential\Nodes\ForeachNode(
expression: Latte\Compiler\Nodes\Php\Expression\MethodCallNode(
object: Latte\Compiler\Nodes\Php\Expression\VariableNode('$category')
name: Latte\Compiler\Nodes\Php\IdentifierNode('getItems')
)
value: Latte\Compiler\Nodes\Php\Expression\VariableNode('$item')
content: Latte\Compiler\Nodes\FragmentNode(
- Latte\Compiler\Nodes\TextNode(' ')
- Latte\Compiler\Nodes\Html\ElementNode('li')(
content: Latte\Compiler\Nodes\PrintNode(
expression: Latte\Compiler\Nodes\Php\Expression\PropertyFetchNode(
object: Latte\Compiler\Nodes\Php\Expression\VariableNode('$item')
name: Latte\Compiler\Nodes\Php\IdentifierNode('name')
)
modifier: Latte\Compiler\Nodes\Php\ModifierNode(
filters:
- Latte\Compiler\Nodes\Php\FilterNode('upper')
)
)
)
)
else: Latte\Compiler\Nodes\FragmentNode(
- Latte\Compiler\Nodes\TextNode('no items found')
)
)
)
)
Recorrer el AST con NodeTraverser
Escribir a mano funciones recursivas para recorrer la compleja estructura del AST es tedioso y propenso a errores. Latte ofrece una herramienta específica para ello: Latte\Compiler\NodeTraverser. Esta clase implementa el patrón de diseño Visitor, lo que hace el recorrido del AST sistemático y manejable.
El uso básico consiste en crear una instancia de NodeTraverser y llamar a su método traverse(),
pasándole el nodo raíz del AST y uno o dos callables “visitantes”:
use Latte\Compiler\Node;
use Latte\Compiler\NodeTraverser;
use Latte\Compiler\Nodes;
(new NodeTraverser)->traverse(
$templateNode,
// visitante 'enter': se llama al entrar en un nodo (antes que sus hijos)
enter: function (Node $node) {
echo "Entering node of type: " . $node::class . "\n";
// Aquí puede inspeccionar el nodo
if ($node instanceof Nodes\TextNode) {
// echo "Found text: " . $node->content . "\n";
}
},
// visitante 'leave': se llama al salir de un nodo (después de sus hijos)
leave: function (Node $node) {
echo "Leaving node of type: " . $node::class . "\n";
// Aquí puede realizar acciones después de procesar los hijos
},
);
Puede proporcionar solo el visitante enter, solo el leave o ambos, según sus necesidades.
enter(Node $node): esta función se ejecuta para cada nodo antes de que el recorredor visite ninguno
de sus hijos. Resulta útil para:
- Reunir información mientras se desciende por el árbol.
- Tomar decisiones antes de procesar los hijos (por ejemplo, decidir saltárselos, vea Optimizar el recorrido).
- Modificar eventualmente el nodo antes de visitar los hijos (menos habitual).
leave(Node $node): esta función se ejecuta para cada nodo después de que todos sus hijos (y sus
subárboles completos) se hayan visitado por entero (tanto entrada como salida). Es el lugar más habitual para:
- Sustituir un nodo después de procesar sus hijos.
- Eliminar nodos del AST.
- Agregar la información reunida en todo el subárbol.
Tanto el visitante enter como el leave pueden devolver opcionalmente un valor para influir en el
recorrido. Devolver null (o nada) continúa el recorrido con normalidad, devolver una instancia de Node
sustituye el nodo actual, y devolver constantes especiales como NodeTraverser::RemoveNode o
NodeTraverser::StopTraversal modifica el flujo, como se explica en las secciones siguientes.
Cómo funciona el recorrido
Internamente, NodeTraverser usa el método getIterator() que toda clase Node debe
implementar (como se comenta en Creación de etiquetas
personalizadas). Itera sobre los hijos que devuelve getIterator() y llama recursivamente a
traverse() sobre ellos, garantizando que los visitantes enter y leave se llamen en el orden
correcto en profundidad para cada nodo del árbol accesible mediante iteradores. Esto vuelve a poner de relieve por qué un
getIterator() bien implementado en los nodos de sus etiquetas propias es absolutamente esencial para que los pases
del compilador funcionen correctamente.
Escribamos un pase sencillo que cuente cuántas veces se usa la etiqueta {do} (representada por
Latte\Essential\Nodes\DoNode) en la plantilla.
use Latte\Compiler\Node;
use Latte\Compiler\NodeTraverser;
use Latte\Compiler\Nodes\TemplateNode;
use Latte\Essential\Nodes\DoNode;
function countDoTags(TemplateNode $templateNode): void
{
$count = 0;
(new NodeTraverser)->traverse(
$templateNode,
enter: function (Node $node) use (&$count): void {
if ($node instanceof DoNode) {
$count++;
}
},
// el visitante 'leave' no hace falta para esta tarea
);
echo "Found {do} tag $count times.\n";
}
$latte = new Latte\Engine;
$ast = $latte->parse($templateSource);
countDoTags($ast);
En este ejemplo solo necesitábamos el visitante enter para comprobar el tipo de cada nodo encontrado.
A continuación veremos cómo usar estos visitantes para modificar realmente el AST.
Modificar el AST
Uno de los principales cometidos de los pases del compilador es modificar el árbol de sintaxis abstracta. Esto permite
transformaciones potentes, optimizaciones o la aplicación de reglas directamente sobre la estructura de la plantilla antes de
generar el código PHP. NodeTraverser ofrece varias formas de lograrlo dentro de los visitantes enter y
leave.
Nota importante: modificar el AST exige cuidado. Los cambios incorrectos, como eliminar nodos esenciales o sustituir un nodo por otro de tipo incompatible, pueden provocar errores durante la generación del código o un comportamiento inesperado en ejecución. Pruebe siempre a fondo sus pases de modificación.
Cambiar propiedades de los nodos
La forma más sencilla de modificar el árbol es cambiar directamente las propiedades públicas de los nodos encontrados durante el recorrido. Todos los nodos guardan sus argumentos analizados, su contenido o sus atributos en propiedades públicas.
Ejemplo: creemos un pase que encuentre todos los nodos de texto estático (TextNode, que representan HTML
o texto plano fuera de las etiquetas de Latte) y convierta su contenido a mayúsculas directamente en el AST.
use Latte\Compiler\Node;
use Latte\Compiler\NodeTraverser;
use Latte\Compiler\Nodes\TemplateNode;
use Latte\Compiler\Nodes\TextNode;
function uppercaseStaticText(TemplateNode $templateNode): void
{
(new NodeTraverser)->traverse(
$templateNode,
// Podemos usar 'enter' porque TextNode no tiene hijos que procesar primero
enter: function (Node $node) {
// ¿Es este nodo un bloque de texto estático?
if ($node instanceof TextNode) {
// ¡Sí! Modificamos directamente su propiedad pública 'content'.
$node->content = mb_strtoupper(html_entity_decode($node->content));
}
// No hace falta devolver nada; la modificación ocurre in situ.
},
);
}
En este ejemplo, el visitante enter comprueba si el $node actual es un TextNode. Si lo
es, actualizamos directamente su propiedad pública $content con mb_strtoupper(). Esto cambia
directamente el contenido del texto estático guardado en el AST antes de generar el código PHP. Como modificamos el
objeto directamente, no necesitamos devolver nada desde el visitante.
Efecto: si la plantilla contenía <p>Hello</p>{= $var }<span>World</span>, tras este pase
el AST representará algo como: <p>HELLO</p>{= $var }<span>WORLD</span>. Esto NO afecta al
contenido de $var.
Sustituir nodos
Una técnica de modificación más potente es sustituir por completo un nodo por otro distinto. Esto se hace devolviendo la
nueva instancia de Node desde el visitante enter o leave. El NodeTraverser
sustituirá entonces el nodo original por el devuelto dentro de la estructura del nodo padre.
Ejemplo: creemos un pase que encuentre todos los usos de la constante PHP_VERSION (representada por
ConstantFetchNode) y los sustituya directamente por un literal de cadena (StringNode) que contenga la
versión real de PHP detectada durante la compilación. Es una forma de optimización en tiempo de
compilación.
use Latte\Compiler\Node;
use Latte\Compiler\NodeTraverser;
use Latte\Compiler\Nodes\TemplateNode;
use Latte\Compiler\Nodes\Php\Expression\ConstantFetchNode;
use Latte\Compiler\Nodes\Php\Scalar\StringNode;
function inlinePhpVersion(TemplateNode $templateNode): void
{
(new NodeTraverser)->traverse(
$templateNode,
// 'leave' se usa a menudo para los reemplazos, para asegurar que los hijos (si los hay)
// se procesen primero, aunque aquí también funcionaría 'enter'.
leave: function (Node $node) {
// ¿Es este nodo un acceso a constante y la constante se llama 'PHP_VERSION'?
if ($node instanceof ConstantFetchNode && (string) $node->name === 'PHP_VERSION') {
// Crea un nuevo StringNode con la versión actual de PHP
$newNode = new StringNode(PHP_VERSION);
// Opcional pero recomendable: copia la información de posición
$newNode->position = $node->position;
// Devuelve el nuevo StringNode. El traverser sustituirá
// el ConstantFetchNode original por este $newNode.
return $newNode;
}
// Si no devolvemos un Node, se conserva el $node original.
},
);
}
Aquí, el visitante leave identifica el ConstantFetchNode concreto de PHP_VERSION.
Después crea un StringNode completamente nuevo que contiene el valor de la constante PHP_VERSION en
tiempo de compilación. Al devolver este $newNode, le indica al recorredor que sustituya en el AST el
ConstantFetchNode original.
Efecto: si la plantilla contenía {= PHP_VERSION } y la compilación se ejecuta en PHP 8.2.1, el AST tras este
pase representará en la práctica {= '8.2.1' }.
Elegir enter o leave para la sustitución:
- Use
leavesi la creación del nuevo nodo depende del resultado de procesar los hijos del nodo antiguo, o si simplemente quiere asegurarse de que los hijos se visitan antes de la sustitución (práctica habitual). - Use
entersi quiere sustituir un nodo antes incluso de visitar sus hijos.
Eliminar nodos
Puede eliminar por completo un nodo del AST devolviendo desde un visitante la constante especial
NodeTraverser::RemoveNode.
Ejemplo: eliminemos de la salida todos los comentarios HTML (<!-- ... -->). Los comentarios de Latte
{* ... *} no se pueden atacar así, porque el analizador descarta su contenido y los sustituye por un
NopNode vacío en lugar de un nodo de comentario específico, pero los comentarios HTML se conservan como nodos
Html\CommentNode, así que aquí sí podemos quitarlos.
use Latte\Compiler\Node;
use Latte\Compiler\NodeTraverser;
use Latte\Compiler\Nodes\TemplateNode;
use Latte\Compiler\Nodes\Html\CommentNode;
function removeHtmlComments(TemplateNode $templateNode): void
{
(new NodeTraverser)->traverse(
$templateNode,
// aquí vale 'enter', porque no necesitamos información de los hijos para eliminar un comentario
enter: function (Node $node) {
if ($node instanceof CommentNode) {
// Indica al traverser que elimine este nodo del AST
return NodeTraverser::RemoveNode;
}
},
);
}
Precaución: use RemoveNode con cuidado. Eliminar un nodo que contenga contenido esencial o que afecte a
la estructura (como eliminar el nodo de contenido de un bucle) puede dejar plantillas rotas o generar código inválido. Es más
seguro con nodos verdaderamente opcionales o autocontenidos (como los comentarios o las etiquetas de depuración) o con nodos
estructurales vacíos (por ejemplo, un FragmentNode vacío puede eliminarse sin riesgo en algunos contextos mediante
un pase de limpieza).
Estos tres métodos (modificar propiedades, sustituir nodos y eliminar nodos) constituyen las herramientas fundamentales para manipular el AST dentro de sus pases del compilador.
Optimizar el recorrido
Los AST de las plantillas pueden llegar a ser bastante grandes y contener miles de nodos. Recorrer cada nodo puede ser
innecesario y afectar al rendimiento de la compilación si su pase solo se interesa por partes concretas del árbol.
NodeTraverser ofrece formas de optimizar el recorrido:
Saltarse los hijos
Si sabe que, en cuanto encuentre cierto tipo de nodo, ninguno de sus descendientes puede contener los nodos que busca, puede
indicarle al recorredor que se salte la visita a sus hijos. Se hace devolviendo la constante
NodeTraverser::DontTraverseChildren desde el visitante enter. Así poda ramas enteras del
recorrido y puede ahorrar mucho tiempo, sobre todo en plantillas con expresiones PHP complejas dentro de las etiquetas.
Detener el recorrido
Si su pase solo necesita encontrar la primera aparición de algo (un tipo de nodo concreto, el cumplimiento de una
condición), puede detener por completo todo el proceso de recorrido en cuanto lo encuentre. Se consigue devolviendo la constante
NodeTraverser::StopTraversal desde el visitante enter o leave. El método
traverse() deja de visitar más nodos. Esto es muy eficaz cuando solo necesita la primera coincidencia en un árbol
potencialmente enorme.
La útil clase NodeHelpers
Aunque NodeTraverser ofrece un control muy fino, Latte proporciona también una cómoda clase de utilidades, Latte\Compiler\NodeHelpers, que envuelve a
NodeTraverser para varias tareas habituales de búsqueda y análisis, a menudo con menos código repetitivo.
find (Node $startNode, callable $filter): array
Este método estático encuentra todos los nodos del subárbol que empieza en $startNode (incluido) que
cumplen el callback $filter. Devuelve un array con los nodos coincidentes.
Ejemplo: encontrar todos los nodos de variable (VariableNode) de toda la plantilla.
use Latte\Compiler\NodeHelpers;
use Latte\Compiler\Nodes\Php\Expression\VariableNode;
use Latte\Compiler\Nodes\TemplateNode;
function findAllVariables(TemplateNode $templateNode): array
{
return NodeHelpers::find(
$templateNode,
fn($node) => $node instanceof VariableNode,
);
}
findFirst (Node $startNode, callable $filter): ?Node
Parecido a find, pero detiene el recorrido en cuanto encuentra el primer nodo que cumple el callback
$filter. Devuelve el objeto Node encontrado o null si no hay ninguna coincidencia. En
esencia, es un envoltorio cómodo de NodeTraverser::StopTraversal.
Ejemplo: encontrar el nodo {parameters}.
use Latte\Compiler\NodeHelpers;
use Latte\Compiler\Nodes\TemplateNode;
use Latte\Essential\Nodes\ParametersNode;
function findParametersNodeHelper(TemplateNode $templateNode): ?ParametersNode
{
return NodeHelpers::findFirst(
$templateNode->head, // Busca solo en la sección head por eficiencia
fn($node) => $node instanceof ParametersNode,
);
}
clone (Latte\Compiler\Node $node): Node
Este método estático crea una copia profunda de un nodo y de todo su subárbol. Resulta útil cuando necesita duplicar una rama del AST, por ejemplo para insertar una copia modificada de un nodo dejando el original intacto.
use Latte\Compiler\NodeHelpers;
$copy = NodeHelpers::clone($node);
toValue (ExpressionNode $node, bool $constants = false): mixed
Este método estático intenta evaluar un ExpressionNode en tiempo de compilación y devolver su valor PHP
correspondiente. Funciona de forma fiable solo con nodos literales simples (StringNode, IntegerNode,
FloatNode, BooleanNode, NullNode) e instancias de ArrayNode que contengan
únicamente elementos evaluables de ese tipo.
Si $constants se pone a true, intentará además resolver ConstantFetchNode y
ClassConstantFetchNode comprobando defined() y usando constant().
Si el nodo contiene variables, llamadas a funciones u otros elementos dinámicos, no se puede evaluar en tiempo de
compilación y el método lanzará una InvalidArgumentException.
Caso de uso: obtener el valor estático del argumento de una etiqueta durante la compilación para tomar decisiones en ese momento.
use Latte\Compiler\NodeHelpers;
use Latte\Compiler\Nodes\Php\ExpressionNode;
function getStaticStringArgument(ExpressionNode $argumentNode): ?string
{
try {
$value = NodeHelpers::toValue($argumentNode);
return is_string($value) ? $value : null;
} catch (\InvalidArgumentException $e) {
// El argumento no era una cadena literal estática
return null;
}
}
toText (?Node $node): ?string
Este método estático resulta útil para extraer el contenido de texto plano de nodos sencillos. Funciona sobre todo con:
TextNode: devuelve su$content.FragmentNode: concatena el resultado detoText()de todos sus hijos. Si algún hijo no se puede convertir a texto (por ejemplo, contiene unPrintNode), devuelvenull.NopNode: devuelve una cadena vacía.- Otros tipos de nodo: devuelve
null.
Caso de uso: obtener el contenido de texto estático del valor de un atributo HTML o de un elemento HTML sencillo para analizarlo durante un pase del compilador.
use Latte\Compiler\NodeHelpers;
use Latte\Compiler\Nodes\Html\AttributeNode;
function getStaticAttributeValue(AttributeNode $attr): ?string
{
// $attr->value suele ser un AreaNode (como FragmentNode o TextNode)
return NodeHelpers::toText($attr->value);
}
// Ejemplo de uso en un pass:
// if ($node instanceof Html\ElementNode && $node->name === 'meta') {
// $nameAttrValue = $node->getAttribute('name');
// if ($nameAttrValue === 'description') { ... }
// }
NodeHelpers puede simplificar sus pases del compilador aportando soluciones listas para las tareas habituales de
recorrido y análisis del AST.
Ejemplos prácticos
Apliquemos los conceptos de recorrido y modificación del AST para resolver algunos problemas prácticos. Estos ejemplos muestran patrones habituales en los pases del compilador.
Añadir automáticamente loading="lazy" a
<img>
Los navegadores modernos admiten la carga diferida nativa de imágenes mediante el atributo loading="lazy".
Creemos un pase que añada automáticamente este atributo a todas las etiquetas <img> que aún no tengan un
atributo loading.
use Latte\Compiler\Node;
use Latte\Compiler\NodeTraverser;
use Latte\Compiler\Nodes;
use Latte\Compiler\Nodes\Html;
function addLazyLoading(Nodes\TemplateNode $templateNode): void
{
(new NodeTraverser)->traverse(
$templateNode,
// Podemos usar 'enter' porque modificamos el nodo directamente
// y esta decisión no depende de los hijos.
enter: function (Node $node) {
// ¿Es un elemento HTML llamado 'img'?
if ($node instanceof Html\ElementNode && $node->name === 'img') {
// Comprueba si el atributo 'loading' ya existe (sin distinguir mayúsculas)
foreach ($node->attributes->children as $attrNode) {
if ($attrNode instanceof Html\AttributeNode
&& $attrNode->name instanceof Nodes\TextNode // Nombre de atributo estático
&& strtolower($attrNode->name->content) === 'loading'
) {
return; // Ya existe, no hace nada
}
}
// Antepone un espacio si los atributos no están vacíos
if ($node->attributes->children) {
$node->attributes->children[] = new Nodes\TextNode(' ');
}
// Crea el nuevo nodo de atributo: loading="lazy"
$node->attributes->children[] = new Html\AttributeNode(
name: new Nodes\TextNode('loading'),
value: new Nodes\TextNode('lazy'),
quote: '"',
);
// Modificación hecha in situ, no hace falta devolver nada.
}
},
);
}
Explicación:
- El visitante
enterbusca nodosHtml\ElementNodellamadosimg. - Recorre los atributos existentes (
$node->attributes->children) para comprobar si ya hay un atributoloading. - Si no lo encuentra, crea un nuevo
Html\AttributeNodeque representaloading="lazy"y lo añade (con un espacio delante si hace falta).
Comprobar llamadas a funciones
Los pases del compilador son la base del Sandbox de Latte. Aunque el Sandbox real es sofisticado, podemos mostrar el principio básico de comprobar llamadas a funciones prohibidas.
Objetivo: impedir el uso de la función potencialmente peligrosa shell_exec dentro de las expresiones de
las plantillas.
use Latte\Compiler\Node;
use Latte\Compiler\NodeTraverser;
use Latte\Compiler\Nodes;
use Latte\Compiler\Nodes\Php;
use Latte\SecurityViolationException;
function checkForbiddenFunctions(Nodes\TemplateNode $templateNode): void
{
$forbiddenFunctions = ['shell_exec' => true, 'exec' => true]; // Lista simple
(new NodeTraverser)->traverse(
$templateNode,
enter: function (Node $node) use ($forbiddenFunctions) {
// ¿Es un nodo de llamada directa a función?
if ($node instanceof Php\Expression\FunctionCallNode
&& $node->name instanceof Php\NameNode
&& isset($forbiddenFunctions[strtolower((string) $node->name)])
) {
throw new SecurityViolationException(
"Function {$node->name}() is not allowed.",
$node->position,
);
}
},
);
}
Explicación:
- Definimos una lista de nombres de funciones prohibidas.
- El visitante
enterbusca nodosFunctionCallNode. - Si el nombre de la función (
$node->name) es unNameNodeestático, comparamos su representación en minúsculas con nuestra lista de prohibidas. - Si encontramos una función prohibida, lanzamos una
Latte\SecurityViolationException, que indica con claridad la violación de una regla de seguridad y detiene la compilación.
Estos ejemplos muestran cómo los pases del compilador, mediante NodeTraverser, se pueden emplear para analizar,
modificar automáticamente y hacer cumplir restricciones de seguridad interactuando directamente con la estructura del AST de la
plantilla.
Buenas prácticas
Al escribir pases del compilador, tenga presentes estas pautas para crear extensiones robustas, mantenibles y eficientes:
- El orden importa: sea consciente del orden en que se ejecutan los pases. Si su pase depende de la estructura del AST
creada por otro pase (por ejemplo, los pases del núcleo de Latte u otro pase propio), o si otros pases pueden depender de sus
modificaciones, use el mecanismo de ordenación que ofrece
Extension::getPasses()para definir dependencias (before/after). Vea los detalles en la documentación deExtension::getPasses(). - Responsabilidad única: procure que cada pase realice una única tarea bien definida. Para transformaciones complejas, valore dividir la lógica en varios pases: quizá uno para el análisis y otro para modificar según los resultados del análisis. Así gana claridad y facilidad de prueba.
- Rendimiento: recuerde que los pases del compilador se suman al tiempo de compilación de la plantilla (aunque esto
suele ocurrir una sola vez, hasta que la plantilla cambia). Evite en lo posible operaciones costosas dentro de sus pases.
Aproveche las optimizaciones del recorrido, como
NodeTraverser::DontTraverseChildrenyNodeTraverser::StopTraversal, siempre que sepa que no necesita visitar ciertas partes del AST. - Use
NodeHelpers: para tareas habituales, como encontrar nodos concretos o evaluar estáticamente expresiones sencillas, compruebe siLatte\Compiler\NodeHelpersofrece un método adecuado antes de escribir su propia lógica conNodeTraverser. Puede ahorrarle tiempo y reducir el código repetitivo. - Tratamiento de errores: si su pase detecta un error o un estado inválido en el AST de la plantilla, lance una
Latte\CompileException(oLatte\SecurityViolationExceptionen cuestiones de seguridad) con un mensaje claro y el objetoPositioncorrespondiente (normalmente$node->position). Así ofrece información útil a quien desarrolla la plantilla. - Idempotencia (si es posible): lo ideal es que ejecutar su pase varias veces sobre el mismo AST produzca el mismo resultado que ejecutarlo una vez. No siempre es viable, pero cuando se logra simplifica la depuración y el razonamiento sobre la interacción entre pases. Por ejemplo, haga que su pase de modificación compruebe si la modificación ya se ha aplicado antes de aplicarla de nuevo.
Siguiendo estas prácticas podrá aprovechar los pases del compilador para extender las capacidades de Latte de forma potente y fiable, contribuyendo a un procesamiento de plantillas más seguro, más optimizado o más rico en funciones.