Passaggi di compilazione
I passaggi di compilazione forniscono un potente meccanismo per analizzare e modificare i template Latte dopo il loro parsing in un albero sintattico astratto (AST) e prima della generazione del codice PHP finale. Ciò consente la manipolazione avanzata dei template, ottimizzazioni, controlli di sicurezza (come la Sandbox) e la raccolta di informazioni sui template. Questa guida vi accompagnerà nella creazione dei vostri passaggi di compilazione personalizzati.
Cos'è un passaggio di compilazione?
Per comprendere il ruolo dei passaggi di compilazione, dai un'occhiata al processo di compilazione di Latte. Come puoi vedere, i passaggi di compilazione operano in una fase chiave, consentendo un intervento profondo tra il parsing iniziale e l'output finale del codice.
Nel suo nucleo, un passaggio di compilazione è semplicemente un oggetto PHP callable (come una funzione, un metodo statico
o un metodo di istanza) che accetta un singolo argomento: il nodo radice dell'AST del template, che è sempre un'istanza di
Latte\Compiler\Nodes\TemplateNode
.
L'obiettivo primario di un passaggio di compilazione è di solito uno o entrambi i seguenti:
- Analisi: Attraversare l'AST e raccogliere informazioni sul template (ad esempio, trovare tutti i blocchi definiti, controllare l'uso di tag specifici, garantire il rispetto di determinati vincoli di sicurezza).
- Modifica: Cambiare la struttura dell'AST o gli attributi dei nodi (ad esempio, aggiungere automaticamente attributi HTML, ottimizzare determinate combinazioni di tag, sostituire tag obsoleti con nuovi, implementare regole di sandbox).
Registrazione
I passaggi di compilazione vengono registrati tramite il metodo getPasses()
dell'estensione. Questo metodo restituisce un array
associativo, dove le chiavi sono nomi univoci dei passaggi (utilizzati internamente e per l'ordinamento) e i valori sono oggetti
PHP callable che implementano la logica del passaggio.
I passaggi registrati dalle estensioni di base di Latte e dalle tue estensioni personalizzate vengono eseguiti in sequenza.
L'ordine può essere importante, specialmente se un passaggio dipende dai risultati o dalle modifiche di un altro. Latte fornisce
un meccanismo di aiuto per controllare questo ordine, se necessario; vedi la documentazione per Extension::getPasses()
per i dettagli.
Esempio di AST
Per avere un'idea migliore dell'AST, aggiungiamo un esempio. Questo è il template sorgente:
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\Essential\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') ) ) ) )
Attraversamento dell'AST con NodeTraverser
Scrivere manualmente funzioni ricorsive per attraversare la complessa struttura dell'AST è noioso e soggetto a errori. Latte fornisce uno strumento speciale per questo scopo: Latte\Compiler\NodeTraverser. Questa classe implementa il design pattern Visitor, grazie al quale l'attraversamento dell'AST diventa sistematico e facilmente gestibile.
L'uso di base prevede la creazione di un'istanza di NodeTraverser
e la chiamata del suo metodo
traverse()
, passando il nodo radice dell'AST e uno o due „visitor“ callable:
Puoi fornire solo il visitor enter
, solo il visitor leave
, o entrambi, a seconda delle tue
esigenze.
enter(Node $node)
: Questa funzione viene eseguita per ogni nodo prima che l'attraversatore visiti
qualsiasi figlio di questo nodo. È utile per:
- Raccogliere informazioni durante l'attraversamento dell'albero verso il basso.
- Prendere decisioni prima dell'elaborazione dei figli (come decidere di saltarli, vedi Ottimizzazione dell'attraversamento).
- Potenziali modifiche al nodo prima della visita dei figli (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 completamente visitati (sia l'ingresso che l'uscita). È il luogo più comune per:
Entrambi i visitor enter
e leave
possono opzionalmente restituire un valore per influenzare il
processo di attraversamento. Restituire null
(o niente) continua l'attraversamento 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
NodeTraverser
utilizza internamente il metodo getIterator()
, che deve essere implementato da ogni
classe Node
(come discusso in Creazione
di tag personalizzati). Itera sui figli ottenuti tramite getIterator()
, chiama ricorsivamente
traverse()
su di essi e assicura che i visitor enter
e leave
siano chiamati nel corretto
ordine depth-first per ogni nodo nell'albero accessibile tramite gli iteratori. Questo sottolinea ancora una volta perché un
getIterator()
implementato correttamente nei tuoi nodi di tag personalizzati è assolutamente necessario per il
corretto funzionamento dei passaggi di compilazione.
Scriviamo un semplice passaggio che conta quante volte viene utilizzato il tag {do}
nel template (rappresentato da
Latte\Essential\Nodes\DoNode
).
In questo esempio, avevamo bisogno solo del visitor enter
per controllare il tipo di ogni nodo visitato.
Successivamente, esploreremo come questi visitor modificano effettivamente l'AST.
Modifica dell'AST
Uno degli scopi principali dei passaggi di compilazione è la modifica dell'albero sintattico astratto. Ciò consente potenti
trasformazioni, ottimizzazioni o l'applicazione di regole direttamente sulla struttura del template prima della generazione del
codice PHP. NodeTraverser
fornisce diversi modi per ottenere ciò all'interno dei visitor enter
e
leave
.
Nota importante: La modifica dell'AST richiede cautela. Cambiamenti errati – come la rimozione di nodi essenziali o la sostituzione di un nodo con un tipo incompatibile – possono portare a errori durante la generazione del codice o causare comportamenti inaspettati durante l'esecuzione del programma. Testa sempre a fondo i tuoi passaggi di modifica.
Modifica delle proprietà dei nodi
Il modo più semplice per modificare l'albero è cambiare direttamente le proprietà pubbliche dei nodi visitati durante l'attraversamento. Tutti i nodi memorizzano i loro argomenti parsati, contenuto o attributi in proprietà pubbliche.
Esempio: Creiamo un passaggio che trova tutti i nodi di testo statico (TextNode
, che rappresentano HTML
comune o testo al di fuori dei tag Latte) e converte il loro contenuto in maiuscolo direttamente nell'AST.
In questo esempio, il visitor enter
controlla se $node
corrente è di tipo TextNode
. Se
sì, aggiorniamo direttamente la sua proprietà pubblica $content
usando mb_strtoupper()
. Questo cambia
direttamente il contenuto del testo statico memorizzato nell'AST prima della generazione del codice PHP. Poiché
modifichiamo l'oggetto direttamente, non dobbiamo restituire nulla dal visitor.
Effetto: Se il template conteneva <p>Hello</p>{= $var }<span>World</span>
, dopo questo
passaggio l'AST rappresenterà qualcosa come: <p>HELLO</p>{= $var }<span>WORLD</span>
. Questo
NON INFLUENZERÀ il contenuto di $var
.
Sostituzione dei nodi
Una tecnica di modifica più potente è la sostituzione completa di un nodo con un altro. Questo si fa restituendo una nuova
istanza di Node
dal visitor enter
o leave
. NodeTraverser
sostituisce
quindi il nodo originale con quello restituito nella struttura del nodo genitore.
Esempio: Creiamo un passaggio che trova tutti gli usi della costante PHP_VERSION
(rappresentata da
ConstantFetchNode
) e li sostituisce direttamente con un letterale stringa (StringNode
) contenente la
versione PHP effettiva rilevata durante la compilazione. Questa è una forma di ottimizzazione al momento della
compilazione.
Qui il visitor leave
identifica lo specifico ConstantFetchNode
per PHP_VERSION
. Quindi
crea un StringNode
completamente nuovo contenente il valore della costante PHP_VERSION
al momento
della compilazione. Restituendo questo $newNode
, dice al traverser di sostituire l'originale
ConstantFetchNode
nell'AST.
Effetto: Se il template conteneva {= PHP_VERSION }
e la compilazione viene eseguita su PHP 8.2.1, l'AST dopo
questo passaggio rappresenterà efficacemente {= '8.2.1' }
.
Scelta tra enter
e leave
per la sostituzione:
- Usa
leave
se la creazione del nuovo nodo dipende dai risultati dell'elaborazione dei figli del vecchio nodo, o se vuoi semplicemente assicurarti che i figli siano visitati prima della sostituzione (pratica comune). - Usa
enter
se vuoi sostituire un nodo prima che i suoi figli vengano visitati.
Rimozione dei nodi
Puoi rimuovere completamente un nodo dall'AST restituendo la costante speciale NodeTraverser::RemoveNode
dal
visitor.
Esempio: Rimuoviamo tutti i commenti del template ({* ... *}
), che sono rappresentati da
CommentNode
nell'AST generato dal core di Latte (anche se tipicamente elaborati prima, questo serve come
esempio).
Attenzione: Usa RemoveNode
con cautela. La rimozione di un nodo che contiene contenuto essenziale
o influisce sulla struttura (come la rimozione del nodo di contenuto di un ciclo) può portare a template danneggiati o codice
generato non valido. È più sicuro per nodi che sono veramente opzionali o autonomi (come commenti o tag di debug) o per nodi
strutturali vuoti (ad esempio, un FragmentNode
vuoto può essere rimosso in sicurezza in alcuni contesti da un
passaggio di pulizia).
Questi tre metodi – modifica delle proprietà, sostituzione dei nodi e rimozione dei nodi – forniscono gli strumenti essenziali per manipolare l'AST all'interno dei tuoi passaggi di compilazione.
Ottimizzazione dell'attraversamento
L'AST dei template può essere piuttosto grande, contenendo potenzialmente migliaia di nodi. Attraversare ogni singolo nodo
può essere superfluo e influire sulle prestazioni di compilazione se il tuo passaggio è interessato solo a parti specifiche
dell'albero. NodeTraverser
offre modi per ottimizzare l'attraversamento:
Saltare i figli
Se sai che una volta incontrato un certo tipo di nodo, nessuno dei suoi discendenti può contenere i nodi che stai cercando,
puoi dire al traverser di saltare la visita dei suoi figli. Questo si fa restituendo la costante
NodeTraverser::DontTraverseChildren
dal visitor enter
. Ciò omette interi rami durante
l'attraversamento, risparmiando potenzialmente tempo considerevole, specialmente nei template con espressioni PHP complesse
all'interno dei tag.
Fermare l'attraversamento
Se il tuo passaggio deve trovare solo la prima occorrenza di qualcosa (un tipo specifico di nodo, il soddisfacimento di
una condizione), puoi fermare completamente l'intero processo di attraversamento una volta trovato. Ciò si ottiene restituendo la
costante NodeTraverser::StopTraversal
dal visitor enter
o leave
. Il metodo
traverse()
smetterà di visitare qualsiasi altro nodo. Questo è altamente efficiente se hai bisogno solo della prima
corrispondenza in un albero potenzialmente molto grande.
Utile helper NodeHelpers
Mentre NodeTraverser
offre un controllo finemente graduato, Latte fornisce anche una pratica classe helper, Latte\Compiler\NodeHelpers, che incapsula
NodeTraverser
per diverse comuni attività di ricerca e analisi, richiedendo spesso meno codice boilerplate.
find(Node $startNode, callable $filter): array
Questo metodo statico trova tutti i nodi nel sottoalbero che inizia da $startNode
(incluso) che soddisfano
il callback $filter
. Restituisce un array dei nodi corrispondenti.
Esempio: Trovare tutti i nodi di variabile (VariableNode
) nell'intero template.
findFirst(Node $startNode, callable $filter): ?Node
Simile a find
, ma interrompe l'attraversamento immediatamente dopo aver trovato il primo nodo che soddisfa
il callback $filter
. Restituisce l'oggetto Node
trovato o null
se non viene trovato alcun
nodo corrispondente. Questo è essenzialmente un comodo wrapper attorno a NodeTraverser::StopTraversal
.
Esempio: Trovare il nodo {parameters}
(uguale all'esempio manuale precedente, ma più corto).
toValue(ExpressionNode $node, bool $constants = false): mixed
Questo metodo statico tenta di valutare un ExpressionNode
al momento della compilazione e restituire il suo
valore PHP corrispondente. Funziona in modo affidabile solo per nodi letterali semplici (StringNode
,
IntegerNode
, FloatNode
, BooleanNode
, NullNode
) e istanze di
ArrayNode
contenenti solo tali elementi valutabili.
Se $constants
è impostato su true
, tenterà anche di risolvere ConstantFetchNode
e
ClassConstantFetchNode
controllando defined()
e usando constant()
.
Se il nodo contiene variabili, chiamate a funzioni o altri elementi dinamici, non può essere valutato al momento della
compilazione e il metodo lancerà InvalidArgumentException
.
Caso d'uso: Ottenere il valore statico di un argomento di tag durante la compilazione per prendere decisioni al momento della compilazione.
toText(?Node $node): ?string
Questo metodo statico è utile per estrarre il contenuto testuale semplice da nodi semplici. Funziona principalmente con:
TextNode
: Restituisce il suo$content
.FragmentNode
: Concatena il risultato ditoText()
per tutti i suoi figli. Se un figlio non è convertibile in testo (ad esempio, contiene unPrintNode
), restituiscenull
.NopNode
: Restituisce una stringa vuota.- Altri tipi di nodi: Restituisce
null
.
Caso d'uso: Ottenere il contenuto testuale statico del valore di un attributo HTML o di un semplice elemento HTML per l'analisi durante un passaggio di compilazione.
NodeHelpers
può semplificare i tuoi passaggi di compilazione fornendo soluzioni pronte all'uso per comuni
attività di attraversamento e analisi dell'AST.
Esempi pratici
Applichiamo i concetti di attraversamento e modifica dell'AST per risolvere alcuni problemi pratici. Questi esempi dimostrano pattern comuni utilizzati nei passaggi di compilazione.
Aggiunta automatica di loading="lazy"
a
<img>
I browser moderni supportano il lazy loading nativo per le immagini tramite l'attributo loading="lazy"
. Creiamo
un passaggio che aggiunge automaticamente questo attributo a tutti i tag <img>
che non hanno ancora un
attributo loading
.
Spiegazione:
- Il visitor
enter
cerca nodiHtml\ElementNode
con nomeimg
. - Itera sugli attributi esistenti (
$node->attributes->children
) e controlla se l'attributoloading
è già presente. - Se non trovato, crea un nuovo
Html\AttributeNode
che rappresentaloading="lazy"
e lo aggiunge ai figli del nodoattributes
.
Controllo delle chiamate a funzioni
I passaggi di compilazione sono alla base della Sandbox di Latte. Anche se la Sandbox reale è sofisticata, possiamo dimostrare il principio di base del controllo delle chiamate a funzioni vietate.
Obiettivo: Impedire l'uso della funzione potenzialmente pericolosa shell_exec
all'interno delle espressioni
del template.
Spiegazione:
- Definiamo un elenco di nomi di funzioni vietate.
- Il visitor
enter
controllaFunctionCallNode
. - Se il nome della funzione (
$node->name
) è unNameNode
statico, controlliamo la sua rappresentazione stringa in minuscolo rispetto al nostro elenco vietato. - Se viene trovata una funzione vietata, lanciamo
Latte\SecurityViolationException
, che indica chiaramente la violazione della regola di sicurezza e interrompe la compilazione.
Questi esempi mostrano come i passaggi di compilazione, utilizzando NodeTraverser
, possono essere sfruttati per
l'analisi, le modifiche automatiche e l'applicazione di vincoli di sicurezza interagendo direttamente con la struttura AST del
template.
Best practice
Quando scrivi passaggi di compilazione, tieni presenti queste linee guida per creare estensioni robuste, manutenibili ed efficienti:
- L'ordine è importante: Sii consapevole dell'ordine in cui vengono eseguiti i passaggi. Se il tuo passaggio dipende
dalla struttura AST creata da un altro passaggio (ad esempio, passaggi di base di Latte o un altro passaggio personalizzato),
o se altri passaggi possono dipendere dalle tue modifiche, usa il meccanismo di ordinamento fornito da
Extension::getPasses()
per definire le dipendenze (before
/after
). Vedi la documentazione perExtension::getPasses()
per i dettagli. - Singola responsabilità: Cerca di creare passaggi che eseguano un singolo compito ben definito. Per trasformazioni complesse, considera la suddivisione della logica in più passaggi – magari uno per l'analisi e un altro per la modifica basata sui risultati dell'analisi. Ciò migliora la leggibilità e la testabilità.
- Prestazioni: Ricorda che i passaggi di compilazione aggiungono tempo alla compilazione del template (anche se questo
di solito avviene solo una volta, finché il template non cambia). Evita operazioni computazionalmente costose nei tuoi passaggi,
se possibile. Utilizza ottimizzazioni dell'attraversamento come
NodeTraverser::DontTraverseChildren
eNodeTraverser::StopTraversal
ogni volta che sai di non aver bisogno di visitare determinate parti dell'AST. - Usa
NodeHelpers
: Per compiti comuni come la ricerca di nodi specifici o la valutazione statica di espressioni semplici, controlla seLatte\Compiler\NodeHelpers
offre un metodo appropriato prima di scrivere la tua logicaNodeTraverser
. Può risparmiare tempo e ridurre la quantità di codice boilerplate. - Gestione degli errori: Se il tuo passaggio rileva un errore o uno stato non valido nell'AST del template, lancia
Latte\CompileException
(oLatte\SecurityViolationException
per problemi di sicurezza) con un messaggio chiaro e l'oggettoPosition
pertinente (di solito$node->position
). Questo fornisce un feedback utile allo sviluppatore del template. - Idempotenza (se possibile): Idealmente, l'esecuzione del tuo passaggio più volte sullo stesso AST dovrebbe produrre lo stesso risultato della sua esecuzione una sola volta. Questo non è sempre fattibile, ma semplifica il debug e la riflessione sulle interazioni dei passaggi, se raggiunto. Ad esempio, assicurati che il tuo passaggio di modifica controlli se la modifica è già stata applicata prima di applicarla di nuovo.
Seguendo queste pratiche, puoi utilizzare efficacemente i passaggi di compilazione per estendere le capacità di Latte in modo potente e affidabile, contribuendo a un'elaborazione dei template più sicura, ottimizzata o funzionalmente più ricca.