Creare compiler pass
I compiler pass offrono un meccanismo potente per analizzare e modificare i template Latte dopo che sono stati analizzati e trasformati in un albero sintattico astratto (AST) e prima che venga generato il codice PHP finale. Permettono manipolazioni avanzate dei template, ottimizzazioni, controlli di sicurezza (come la Sandbox) e la raccolta di informazioni sui template. Questa guida vi accompagnerà nella creazione dei vostri compiler pass.
Cos'è un compiler pass?
Per capire il ruolo dei compiler pass, guardate il processo di compilazione di Latte. Come vedete, i compiler pass operano in una fase cruciale e permettono un intervento profondo tra l'analisi iniziale e la generazione finale del codice.
In sostanza un compiler pass è semplicemente un callable PHP (una funzione, un metodo statico o un metodo di istanza) che
accetta un argomento: il nodo radice dell'AST del template, che è sempre un'istanza di
Latte\Compiler\Nodes\TemplateNode.
L'obiettivo principale di un compiler pass è di solito uno dei due seguenti, o entrambi:
- Analisi: attraversare l'AST e raccogliere informazioni sul template (per esempio trovare tutti i blocchi definiti, verificare l'uso di determinati tag, assicurarsi che siano rispettati certi vincoli di sicurezza).
- Modifica: cambiare la struttura dell'AST o le proprietà dei nodi (per esempio aggiungere automaticamente attributi HTML, ottimizzare determinate combinazioni di tag, sostituire tag deprecati con quelli nuovi, applicare le regole della sandbox).
Registrazione
I compiler pass si registrano tramite il metodo getPasses() di un'estensione. Questo metodo restituisce un array associativo in
cui le chiavi sono nomi univoci dei pass (usati internamente e per l'ordinamento) e i valori sono i callable PHP che ne
implementano la logica.
use Latte\Compiler\Nodes\TemplateNode;
use Latte\Extension;
class MyExtension extends Extension
{
public function getPasses(): array
{
return [
'modificationPass' => $this->modifyTemplateAst(...),
// ... altri pass ...
];
}
public function modifyTemplateAst(TemplateNode $templateNode): void
{
// implementazione...
}
}
I pass registrati dalle estensioni interne di Latte e dalle vostre estensioni personalizzate vengono eseguiti in sequenza.
L'ordine può essere importante, soprattutto se un pass si basa sui risultati o sulle modifiche di un altro. Latte offre un
meccanismo di supporto per controllare quest'ordine, se necessario; per i dettagli vedi la documentazione di Extension::getPasses().
Esempio di AST
Per farvi un'idea migliore dell'AST, aggiungiamo un esempio. Questo è il template sorgente:
{foreach $category->getItems() as $item}
<li>{$item->name|upper}</li>
{else}
nessun elemento trovato
{/foreach}
E questa è la sua rappresentazione sotto forma di 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('nessun elemento trovato')
)
)
)
)
Attraversare l'AST con NodeTraverser
Scrivere a mano funzioni ricorsive per percorrere la complessa struttura dell'AST è noioso e favorisce gli errori. Latte offre uno strumento dedicato: Latte\Compiler\NodeTraverser. Questa classe implementa il design pattern Visitor, rendendo l'attraversamento dell'AST sistematico e gestibile.
L'uso di base consiste nel creare un'istanza di NodeTraverser e chiamarne il metodo traverse(),
passando il nodo radice dell'AST e uno o due callable “visitor”:
use Latte\Compiler\Node;
use Latte\Compiler\NodeTraverser;
use Latte\Compiler\Nodes;
(new NodeTraverser)->traverse(
$templateNode,
// visitor 'enter': chiamato entrando in un nodo (prima dei suoi figli)
enter: function (Node $node) {
echo "Entering node of type: " . $node::class . "\n";
// qui potete esaminare il nodo
if ($node instanceof Nodes\TextNode) {
// echo "Found text: " . $node->content . "\n";
}
},
// visitor 'leave': chiamato uscendo da un nodo (dopo i suoi figli)
leave: function (Node $node) {
echo "Leaving node of type: " . $node::class . "\n";
// qui potete agire dopo che i figli sono stati elaborati
},
);
Potete indicare solo il visitor enter, solo il visitor leave oppure entrambi, a seconda delle vostre
esigenze.
enter(Node $node): questa funzione viene eseguita per ogni nodo prima che il traverser visiti uno
qualsiasi dei suoi figli. È utile per:
- raccogliere informazioni scendendo lungo l'albero,
- prendere decisioni prima di elaborare i figli (per esempio decidere di saltarli, vedi Ottimizzare l'attraversamento),
- eventualmente modificare il nodo prima che i figli vengano visitati (caso meno comune).
leave(Node $node): questa funzione viene eseguita per ogni nodo dopo che tutti i suoi figli (e
i loro interi sottoalberi) sono stati visitati per intero (sia entrando sia uscendo). È il punto più comune per:
- sostituire un nodo dopo che i suoi figli sono stati elaborati,
- rimuovere nodi dall'AST,
- aggregare le informazioni raccolte dall'intero sottoalbero.
Sia il visitor enter sia il visitor leave possono facoltativamente restituire un valore per
influenzare il processo di attraversamento. Restituire null (o nulla) prosegue normalmente, restituire un'istanza di
Node sostituisce il nodo corrente e restituire costanti speciali come NodeTraverser::RemoveNode o
NodeTraverser::StopTraversal modifica il flusso, come spiegato nelle sezioni seguenti.
Come funziona l'attraversamento
Internamente NodeTraverser usa il metodo getIterator() che ogni classe Node deve
implementare (come descritto in Creare tag personalizzati). Itera
sui figli restituiti da getIterator(), chiama ricorsivamente traverse() su di essi e garantisce che
i visitor enter e leave vengano chiamati nell'ordine corretto, in profondità, per ogni nodo
dell'albero accessibile tramite gli iteratori. Questo mette ancora una volta in evidenza perché un getIterator()
implementato correttamente nei nodi dei vostri tag personalizzati sia assolutamente essenziale al buon funzionamento dei
compiler pass.
Scriviamo un semplice pass che conta quante volte il tag {do} (rappresentato da
Latte\Essential\Nodes\DoNode) è usato nel template.
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++;
}
},
// il visitor 'leave' non serve per questo compito
);
echo "Il tag {do} è stato trovato $count volte.\n";
}
$latte = new Latte\Engine;
$ast = $latte->parse($templateSource);
countDoTags($ast);
In questo esempio ci è bastato il visitor enter per controllare il tipo di ogni nodo incontrato.
Vediamo ora come usare questi visitor per modificare davvero l'AST.
Modificare l'AST
Uno degli scopi principali dei compiler pass è modificare l'albero sintattico astratto. Questo permette trasformazioni
potenti, ottimizzazioni o l'applicazione di regole direttamente sulla struttura del template, prima che venga generato il codice
PHP. NodeTraverser offre diversi modi di farlo all'interno dei visitor enter e leave.
Nota importante: modificare l'AST richiede attenzione. Modifiche sbagliate, come rimuovere nodi essenziali o sostituire un nodo con uno di tipo incompatibile, possono portare a errori durante la generazione del codice o a comportamenti inattesi in fase di esecuzione. Provate sempre a fondo i vostri pass di modifica.
Cambiare le proprietà dei nodi
Il modo più semplice di modificare l'albero è cambiare direttamente le proprietà pubbliche dei nodi incontrati durante l'attraversamento. Tutti i nodi conservano in proprietà pubbliche gli argomenti analizzati, il contenuto o gli attributi.
Esempio: creiamo un pass che trova tutti i nodi di testo statico (TextNode, che rappresentano HTML
o testo semplice fuori dai tag Latte) e converte il loro contenuto in maiuscolo direttamente nell'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,
// possiamo usare 'enter', perché TextNode non ha figli da elaborare prima
enter: function (Node $node) {
// questo nodo è un blocco di testo statico?
if ($node instanceof TextNode) {
// sì! Modifichiamo direttamente la sua proprietà pubblica 'content'.
$node->content = mb_strtoupper(html_entity_decode($node->content));
}
// non serve restituire nulla; la modifica avviene sul posto.
},
);
}
In questo esempio il visitor enter controlla se il $node corrente è un TextNode. In
caso affermativo aggiorniamo direttamente la sua proprietà pubblica $content usando mb_strtoupper().
Questo cambia direttamente il contenuto testuale statico salvato nell'AST prima della generazione del codice PHP. Poiché
modifichiamo direttamente l'oggetto, non dobbiamo restituire nulla dal visitor.
Effetto: se il template conteneva <p>Hello</p>{= $var }<span>World</span>, dopo questo
pass l'AST rappresenterà qualcosa come <p>HELLO</p>{= $var }<span>WORLD</span>. Questo NON
influisce sul contenuto di $var.
Sostituire i nodi
Una tecnica di modifica più potente è sostituire completamente un nodo con un altro. Lo si fa restituendo la nuova istanza
di Node dal visitor enter o leave. NodeTraverser sostituirà allora il
nodo originale con quello restituito nella struttura del nodo genitore.
Esempio: creiamo un pass che trova tutti gli usi della costante PHP_VERSION (rappresentata da
ConstantFetchNode) e li sostituisce direttamente con un valore stringa (StringNode) contenente la
versione reale di PHP rilevata durante la compilazione. È una forma di ottimizzazione in fase di
compilazione.
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,
// per le sostituzioni si usa spesso 'leave', così i figli (se ci sono)
// vengono elaborati prima, anche se qui funzionerebbe pure 'enter'.
leave: function (Node $node) {
// questo nodo è un accesso a una costante e il nome è 'PHP_VERSION'?
if ($node instanceof ConstantFetchNode && (string) $node->name === 'PHP_VERSION') {
// crea un nuovo StringNode con la versione corrente di PHP
$newNode = new StringNode(PHP_VERSION);
// facoltativo ma buona pratica: copia le informazioni di posizione
$newNode->position = $node->position;
// restituisce il nuovo StringNode. Il traverser sostituirà
// il ConstantFetchNode originale con questo $newNode.
return $newNode;
}
// se non restituiamo un Node, il $node originale viene mantenuto.
},
);
}
Qui il visitor leave individua lo specifico ConstantFetchNode di PHP_VERSION. Crea poi
un StringNode completamente nuovo, contenente il valore della costante PHP_VERSION al momento della
compilazione. Restituendo questo $newNode dice al traverser di sostituire nell'AST il
ConstantFetchNode originale.
Effetto: se il template conteneva {= PHP_VERSION } e la compilazione avviene su PHP 8.2.1, dopo questo pass l'AST
rappresenterà di fatto {= '8.2.1' }.
Scegliere enter o leave per la sostituzione:
- Usate
leavese la creazione del nuovo nodo dipende dai risultati dell'elaborazione dei figli del vecchio nodo, o se volete semplicemente essere sicuri che i figli vengano visitati prima della sostituzione (pratica comune). - Usate
enterse volete sostituire un nodo prima ancora che i suoi figli vengano visitati.
Rimuovere i nodi
Potete rimuovere del tutto un nodo dall'AST restituendo da un visitor la costante speciale
NodeTraverser::RemoveNode.
Esempio: rimuoviamo dall'output tutti i commenti HTML (<!-- ... -->). I commenti di Latte
{* ... *} non si possono prendere di mira in questo modo, perché il parser ne scarta il contenuto e li sostituisce
con un NopNode vuoto anziché con un nodo commento dedicato; i commenti HTML, invece, vengono conservati come nodi
Html\CommentNode, quindi qui possiamo eliminarli.
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,
// qui 'enter' va bene, perché per rimuovere un commento non servono i figli
enter: function (Node $node) {
if ($node instanceof CommentNode) {
// segnala al traverser di rimuovere questo nodo dall'AST
return NodeTraverser::RemoveNode;
}
},
);
}
Attenzione: usate RemoveNode con cautela. Rimuovere un nodo che contiene contenuto essenziale o che
influisce sulla struttura (per esempio rimuovere il nodo con il contenuto di un ciclo) può portare a template rotti o a codice
generato non valido. È più sicuro con i nodi davvero facoltativi o autonomi (come i commenti o i tag di debug) oppure con
i nodi strutturali vuoti (per esempio un FragmentNode vuoto in certi contesti può essere rimosso senza rischi da un
pass di pulizia).
Questi tre metodi (modificare le proprietà, sostituire i nodi e rimuovere i nodi) sono gli strumenti fondamentali per manipolare l'AST nei vostri compiler pass.
Ottimizzare l'attraversamento
Gli AST dei template possono diventare piuttosto grandi e contenere anche migliaia di nodi. Attraversare ogni singolo nodo può
essere inutile e incidere sulle prestazioni della compilazione, se il vostro pass si interessa solo a parti specifiche
dell'albero. NodeTraverser offre alcuni modi di ottimizzare l'attraversamento:
Saltare i figli
Se sapete che, una volta incontrato un certo tipo di nodo, nessuno dei suoi discendenti può contenere i nodi che state
cercando, potete dire al traverser di saltare la visita dei suoi figli. Lo si fa restituendo dal visitor enter
la costante NodeTraverser::DontTraverseChildren. Tagliate così interi rami dal percorso di attraversamento, con un
risparmio di tempo potenzialmente notevole, soprattutto nei template con espressioni PHP complesse dentro i tag.
Interrompere l'attraversamento
Se il vostro pass deve trovare solo la prima occorrenza di qualcosa (un determinato tipo di nodo, una condizione
soddisfatta), potete interrompere completamente l'intero processo di attraversamento non appena l'avete trovata. Lo si ottiene
restituendo la costante NodeTraverser::StopTraversal dal visitor enter o leave. Il metodo
traverse() smette di visitare altri nodi. È molto efficace quando vi serve solo la prima corrispondenza in un albero
potenzialmente enorme.
L'utile classe NodeHelpers
NodeTraverser offre un controllo molto fine, ma Latte mette a disposizione anche una comoda classe di utilità, Latte\Compiler\NodeHelpers, che incapsula
NodeTraverser per diversi compiti comuni di ricerca e analisi, spesso richiedendo meno codice ripetitivo.
find (Node $startNode, callable $filter): array
Questo metodo statico trova tutti i nodi del sottoalbero che parte da $startNode (incluso) che soddisfano
la callback $filter. Restituisce un array dei nodi corrispondenti.
Esempio: trovare tutti i nodi variabile (VariableNode) dell'intero template.
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
Simile a find, ma interrompe l'attraversamento subito dopo aver trovato il primo nodo che soddisfa la
callback $filter. Restituisce l'oggetto Node trovato oppure null se nessun nodo
corrisponde. È in sostanza un comodo involucro attorno a NodeTraverser::StopTraversal.
Esempio: trovare il 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, // per efficienza cerca solo nella sezione head
fn($node) => $node instanceof ParametersNode,
);
}
clone (Latte\Compiler\Node $node): Node
Questo metodo statico crea una copia profonda di un nodo e del suo intero sottoalbero. È utile quando dovete duplicare un ramo dell'AST, per esempio per inserire una copia modificata di un nodo lasciando intatto l'originale.
use Latte\Compiler\NodeHelpers;
$copy = NodeHelpers::clone($node);
toValue (ExpressionNode $node, bool $constants = false): mixed
Questo metodo statico prova a valutare un ExpressionNode in fase di compilazione e a restituirne il valore
PHP corrispondente. Funziona in modo affidabile solo con i nodi letterali semplici (StringNode,
IntegerNode, FloatNode, BooleanNode, NullNode) e con le istanze di
ArrayNode che contengono solo elementi valutabili di questo tipo.
Se $constants è impostato a true, proverà a risolvere anche ConstantFetchNode e
ClassConstantFetchNode verificando con defined() e usando constant().
Se il nodo contiene variabili, chiamate di funzione o altri elementi dinamici, non può essere valutato in fase di
compilazione e il metodo solleverà un'eccezione InvalidArgumentException.
Caso d'uso: ottenere il valore statico dell'argomento di un tag durante la compilazione, per prendere decisioni in fase di compilazione.
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) {
// l'argomento non era una stringa letterale statica
return null;
}
}
toText (?Node $node): ?string
Questo metodo statico è utile per estrarre il contenuto testuale semplice dai nodi elementari. Funziona soprattutto con:
TextNode: restituisce il suo$content.FragmentNode: concatena il risultato ditoText()di tutti i suoi figli. Se qualche figlio non è convertibile in testo (per esempio contiene unPrintNode), restituiscenull.NopNode: restituisce una stringa vuota.- Altri tipi di nodo: restituisce
null.
Caso d'uso: ottenere il contenuto testuale statico del valore di un attributo HTML o di un semplice elemento HTML, per analizzarlo durante un compiler pass.
use Latte\Compiler\NodeHelpers;
use Latte\Compiler\Nodes\Html\AttributeNode;
function getStaticAttributeValue(AttributeNode $attr): ?string
{
// $attr->value è di norma un AreaNode (come FragmentNode o TextNode)
return NodeHelpers::toText($attr->value);
}
// esempio d'uso in un pass:
// if ($node instanceof Html\ElementNode && $node->name === 'meta') {
// $nameAttrValue = $node->getAttribute('name');
// if ($nameAttrValue === 'description') { ... }
// }
NodeHelpers può semplificare i vostri compiler pass offrendo soluzioni pronte per i compiti più comuni di
attraversamento e analisi dell'AST.
Esempi pratici
Applichiamo i concetti di attraversamento e modifica dell'AST alla soluzione di alcuni problemi pratici. Questi esempi mostrano schemi ricorrenti nei compiler pass.
Aggiungere automaticamente loading="lazy" a
<img>
I browser moderni supportano il caricamento pigro nativo delle immagini tramite l'attributo loading="lazy".
Creiamo un pass che aggiunge automaticamente questo attributo a tutti i tag <img> che non hanno già un
attributo 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,
// possiamo usare 'enter', perché modifichiamo direttamente il nodo
// e per questa decisione non dipendiamo dai figli.
enter: function (Node $node) {
// è un elemento HTML chiamato 'img'?
if ($node instanceof Html\ElementNode && $node->name === 'img') {
// controlla se l'attributo 'loading' esiste già (ignorando maiuscole/minuscole)
foreach ($node->attributes->children as $attrNode) {
if ($attrNode instanceof Html\AttributeNode
&& $attrNode->name instanceof Nodes\TextNode // nome di attributo statico
&& strtolower($attrNode->name->content) === 'loading'
) {
return; // esiste già, non fa nulla
}
}
// antepone uno spazio se gli attributi non sono vuoti
if ($node->attributes->children) {
$node->attributes->children[] = new Nodes\TextNode(' ');
}
// crea il nuovo nodo attributo: loading="lazy"
$node->attributes->children[] = new Html\AttributeNode(
name: new Nodes\TextNode('loading'),
value: new Nodes\TextNode('lazy'),
quote: '"',
);
// modifica eseguita sul posto, non serve alcun return.
}
},
);
}
Spiegazione:
- Il visitor
entercerca i nodiHtml\ElementNodechiamatiimg. - Scorre gli attributi esistenti (
$node->attributes->children) per controllare se l'attributoloadingè già presente. - Se non lo trova, crea un nuovo
Html\AttributeNodeche rappresentaloading="lazy"e lo aggiunge (preceduto da uno spazio, se necessario).
Controllare le chiamate di funzione
I compiler pass sono il fondamento della Sandbox di Latte. La Sandbox vera è sofisticata, ma possiamo mostrare il principio di base del controllo delle chiamate di funzione vietate.
Obiettivo: impedire l'uso della funzione potenzialmente pericolosa shell_exec nelle espressioni dei
template.
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]; // elenco semplice
(new NodeTraverser)->traverse(
$templateNode,
enter: function (Node $node) use ($forbiddenFunctions) {
// è un nodo di chiamata diretta a una funzione?
if ($node instanceof Php\Expression\FunctionCallNode
&& $node->name instanceof Php\NameNode
&& isset($forbiddenFunctions[strtolower((string) $node->name)])
) {
throw new SecurityViolationException(
"La funzione {$node->name}() non è consentita.",
$node->position,
);
}
},
);
}
Spiegazione:
- Definiamo un elenco di nomi di funzione vietati.
- Il visitor
entercerca iFunctionCallNode. - Se il nome della funzione (
$node->name) è unNameNodestatico, ne confrontiamo la rappresentazione in minuscolo con il nostro elenco di funzioni vietate. - Se troviamo una funzione vietata, solleviamo un'eccezione
Latte\SecurityViolationException, che segnala chiaramente la violazione di una regola di sicurezza e interrompe la compilazione.
Questi esempi mostrano come i compiler pass, usando NodeTraverser, possano servire all'analisi, alle modifiche
automatiche e all'applicazione di vincoli di sicurezza, interagendo direttamente con la struttura dell'AST del template.
Buone pratiche
Scrivendo compiler pass, tenete a mente queste indicazioni per creare estensioni solide, manutenibili ed efficienti:
- L'ordine conta: siate consapevoli dell'ordine in cui vengono eseguiti i pass. Se il vostro pass si basa sulla
struttura dell'AST creata da un altro pass (per esempio i pass interni di Latte o un altro pass personalizzato), oppure se altri
pass potrebbero dipendere dalle vostre modifiche, usate il meccanismo di ordinamento offerto da
Extension::getPasses()per definire le dipendenze (before/after). Per i dettagli vedi la documentazione diExtension::getPasses(). - Responsabilità unica: puntate a pass che svolgono un solo compito ben definito. Per le trasformazioni complesse valutate di dividere la logica in più pass, magari uno per l'analisi e un altro per la modifica basata sui risultati dell'analisi. Questo migliora la chiarezza e la testabilità.
- Prestazioni: ricordate che i compiler pass si aggiungono al tempo di compilazione del template (anche se di norma
questo avviene una sola volta, finché il template non cambia). Evitate, se possibile, operazioni computazionalmente costose nei
vostri pass. Sfruttate le ottimizzazioni dell'attraversamento, come
NodeTraverser::DontTraverseChildreneNodeTraverser::StopTraversal, ogni volta che sapete di non dover visitare certe parti dell'AST. - Usate
NodeHelpers: per i compiti comuni, come trovare determinati nodi o valutare staticamente espressioni semplici, controllate seLatte\Compiler\NodeHelpersoffre un metodo adatto prima di scrivere logica personalizzata conNodeTraverser. Vi farà risparmiare tempo e ridurrà il codice ripetitivo. - Gestione degli errori: se il vostro pass rileva un errore o uno stato non valido nell'AST del template, sollevate
un'eccezione
Latte\CompileException(oppureLatte\SecurityViolationExceptionper i problemi di sicurezza) con un messaggio chiaro e il relativo oggettoPosition(di norma$node->position). Questo offre un riscontro utile a chi sviluppa il template. - Idempotenza (se possibile): idealmente, eseguire il vostro pass più volte sullo stesso AST dovrebbe produrre lo stesso risultato di una singola esecuzione. Non è sempre fattibile, ma quando ci si riesce semplifica il debug e il ragionamento sulle interazioni tra i pass. Per esempio, fate in modo che il vostro pass di modifica controlli se la modifica è già stata applicata prima di applicarla di nuovo.
Attenendovi a queste pratiche potete sfruttare efficacemente i compiler pass per estendere le capacità di Latte in modi potenti e affidabili, contribuendo a un'elaborazione dei template più sicura, più ottimizzata o più ricca di funzionalità.