Creare tag personalizzati
Questa pagina offre una guida completa alla creazione di tag personalizzati in Latte. Copriremo tutto, dai tag più semplici agli scenari più complessi con contenuto annidato ed esigenze particolari di analisi, partendo dalla comprensione di come Latte compila i template.
I tag personalizzati offrono il massimo controllo sulla sintassi dei template e sulla logica di rendering, ma sono anche il punto di estensione più complesso. Prima di decidere di creare un tag personalizzato, valutate sempre se esiste una soluzione più semplice oppure se un tag adatto esiste già nell'insieme standard. Usate i tag personalizzati solo quando le alternative più semplici non bastano per le vostre esigenze.
Capire il processo di compilazione
Per creare tag personalizzati in modo efficace è utile spiegare come Latte elabora i template. Capire questo processo chiarisce perché i tag sono strutturati così e come si inseriscono nel quadro generale.
La compilazione di un template in Latte, semplificando, comprende questi passaggi fondamentali:
- Analisi lessicale: il lexer legge il codice sorgente del template (il file
.latte) e lo scompone in una sequenza di piccole parti distinte, chiamate token (per esempio{,foreach,$variable,}, testo HTML e così via). - Analisi sintattica: il parser prende questo flusso di token e costruisce una struttura ad albero che rappresenta la logica e il contenuto del template. Questo albero si chiama albero sintattico astratto (AST).
- Compiler pass: prima di generare il codice PHP, Latte esegue i compiler pass. Sono funzioni che attraversano l'intero AST e possono modificarlo o raccogliere informazioni. Questo passaggio è essenziale per funzionalità come la sicurezza (Sandbox) o le ottimizzazioni.
- Generazione del codice: infine il compilatore percorre l'AST (eventualmente modificato) e genera il codice della corrispondente classe PHP. È questo codice PHP a disegnare davvero il template quando viene eseguito.
- Caching: il codice PHP generato viene salvato in una cache su disco, il che rende velocissimi i rendering successivi, perché i passaggi 1–4 vengono saltati.
In realtà la compilazione è un po' più complicata. Latte ha due lexer e due parser: uno per il template HTML e uno per il codice simile a PHP dentro i tag. Inoltre l'analisi sintattica non avviene dopo la tokenizzazione: il lexer e il parser corrono in parallelo in due “thread” e si coordinano. Credetemi, sono David Grudl: programmarlo è stato come costruire un razzo :-)
L'intero processo, dal caricamento del contenuto del template all'analisi fino alla generazione del file risultante, si può eseguire passo per passo con questo codice, con cui potete sperimentare e scaricare i risultati intermedi:
$latte = new Latte\Engine;
$source = $latte->getLoader()->getContent($file);
$ast = $latte->parse($source);
$latte->applyPasses($ast);
$code = $latte->generate($ast, $file);
L'anatomia di un tag
Creare in Latte un tag personalizzato pienamente funzionante coinvolge diverse parti collegate tra loro. Prima di tuffarci nell'implementazione, capiamo i concetti fondamentali e la terminologia, con un'analogia con l'HTML e il Document Object Model (DOM).
Tag e nodi (analogia con l'HTML)
In HTML scriviamo tag come <p> o <div>...</div>. Questi tag sono sintassi
nel codice sorgente. Quando un browser analizza questo HTML, crea in memoria una rappresentazione chiamata Document Object
Model (DOM). Nel DOM i tag HTML sono rappresentati da nodi (nella terminologia del DOM di JavaScript, in particolare
nodi Element). Con questi nodi interagiamo a livello di codice (per esempio in JavaScript
document.getElementById(...) restituisce un nodo Element). Il tag è solo la rappresentazione testuale nel file
sorgente; il nodo è la rappresentazione a oggetti nell'albero logico.
Latte funziona in modo simile:
- In un file di template
.lattescrivete i tag di Latte, come{foreach ...}e{/foreach}. È la sintassi con cui voi, autori del template, interagite. - Quando Latte analizza il template, costruisce un albero sintattico astratto (AST). Questo albero è composto da nodi. Ogni tag di Latte, ogni elemento HTML, ogni pezzo di testo o espressione del template diventa uno o più nodi di questo albero.
- La classe base di tutti i nodi dell'AST è
Latte\Compiler\Node. Proprio come il DOM ha diversi tipi di nodo (Element, Text, Comment), l'AST di Latte ha vari tipi di nodo. IncontrereteLatte\Compiler\Nodes\TextNodeper il testo statico,Latte\Compiler\Nodes\Html\ElementNodeper gli elementi HTML,Latte\Compiler\Nodes\Php\ExpressionNodeper le espressioni dentro i tag e, cosa fondamentale per i tag personalizzati, i nodi che ereditano daLatte\Compiler\Nodes\StatementNode.
Perché StatementNode?
Gli elementi HTML (Html\ElementNode) rappresentano soprattutto struttura e contenuto. Le espressioni PHP
(Php\ExpressionNode) rappresentano valori o calcoli. Ma i tag di Latte come {if},
{foreach} o il nostro {datetime} personalizzato? Questi tag eseguono azioni, controllano il
flusso del programma o generano output in base a una logica. Sono le unità funzionali che fanno di Latte un potente
motore di template, non un semplice linguaggio di markup.
In programmazione queste unità che eseguono azioni si chiamano spesso “statement”, istruzioni. Perciò i nodi che
rappresentano questi tag funzionali di Latte ereditano di norma da Latte\Compiler\Nodes\StatementNode. Questo li
distingue dai nodi puramente strutturali (come gli elementi HTML) o dai nodi che rappresentano valori (come le espressioni).
I componenti fondamentali
Riprendiamo i componenti principali necessari per creare un tag personalizzato:
Funzione di analisi del tag
- Questo callable PHP analizza la sintassi del tag Latte (
{...}) nel sorgente del template. - Riceve le informazioni sul tag (come il nome, la posizione e il fatto che sia un n:attributo) tramite un oggetto Latte\Compiler\Tag, e come secondo argomento il Latte\Compiler\TemplateParser principale. La sua
firma completa è
callable(Tag, TemplateParser): (Node|\Generator|void). - Il suo strumento principale per analizzare argomenti ed espressioni all'interno dei delimitatori del tag è l'oggetto Latte\Compiler\TagParser, accessibile tramite
$tag->parser(è un parser diverso da quello che analizza l'intero template). - Per i tag di tipo pari usa
yieldper segnalare a Latte di analizzare il contenuto interno tra il tag di apertura e quello di chiusura. - L'obiettivo finale della funzione di analisi è creare e restituire un'istanza della classe del nodo, che viene aggiunta all'AST.
- È consuetudine (anche se non obbligatorio) implementare la funzione di analisi come metodo statico (spesso chiamato
create) direttamente nella classe del nodo corrispondente. Questo tiene insieme, in modo ordinato, la logica di analisi e la rappresentazione del nodo, permette di accedere agli elementi privati o protetti della classe se serve e migliora l'organizzazione.
Classe del nodo
- Rappresenta la funzione logica del vostro tag all'interno dell'albero sintattico astratto (AST).
- Contiene le informazioni analizzate (come gli argomenti o il contenuto) in proprietà pubbliche. Queste proprietà contengono
spesso altre istanze di
Node(per esempioExpressionNodeper gli argomenti analizzati,AreaNodeper il contenuto analizzato). - Il metodo
print(PrintContext $context): stringgenera il codice PHP (un'istruzione o una serie di istruzioni) che esegue l'azione del tag durante il rendering del template. - Il metodo
getIterator(): \Generatorrende i nodi figli (argomenti, contenuto) accessibili all'attraversamento da parte dei compiler pass. Deve restituire riferimenti (&), per permettere ai pass di modificare o sostituire i sottonodi. - Dopo che l'intero template è stato analizzato in un AST, Latte esegue una serie di compiler pass. Questi pass attraversano l'intero AST usando il metodo
getIterator()fornito da ogni nodo. Possono esaminare i nodi, raccogliere informazioni e perfino modificare l'albero (per esempio cambiando le proprietà pubbliche dei nodi o sostituendo interamente dei nodi). Questo disegno, che richiede ungetIterator()completo, è essenziale: permette a funzionalità potenti come la Sandbox di analizzare ed eventualmente alterare il comportamento di qualsiasi parte del template, compresi i vostri tag personalizzati, garantendo sicurezza e coerenza.
Registrazione tramite un'estensione
- Dovete comunicare a Latte il vostro nuovo tag e quale funzione di analisi usare per esso. Questo avviene in un'estensione di Latte.
- Nella vostra classe di estensione implementate il metodo
getTags(): array. Questo metodo restituisce un array associativo in cui le chiavi sono i nomi dei tag (per esempio'mytag','n:myattribute') e i valori sono i callable PHP che ne rappresentano le funzioni di analisi (per esempioMyNamespace\DatetimeNode::create(...)).
In sintesi: la funzione di analisi del tag trasforma il codice sorgente del template del vostro tag in un
nodo dell'AST. La classe del nodo sa poi come trasformare sé stessa in codice PHP eseguibile per
il template compilato e rende disponibili i propri sottonodi ai compiler pass tramite getIterator(). La
registrazione tramite un'estensione collega il nome del tag alla funzione di analisi e lo fa conoscere a Latte.
Vedremo ora passo per passo come implementare questi componenti.
Creare un tag semplice
Tuffiamoci nella creazione del vostro primo tag Latte personalizzato. Cominciamo con un esempio molto semplice: un tag chiamato
{datetime} che stampa la data e l'ora correnti. All'inizio questo tag non accetterà argomenti, ma lo
amplieremo più avanti nella sezione Analizzare gli argomenti del tag. Non ha
nemmeno contenuto interno.
Questo esempio vi guiderà attraverso i passaggi essenziali: definire la classe del nodo, implementarne i metodi
print() e getIterator(), creare la funzione di analisi e infine registrare il tag.
Obiettivo: implementare {datetime} in modo che stampi la data e l'ora correnti usando la funzione PHP
date().
Creazione della classe del nodo
Per prima cosa ci serve una classe che rappresenti il nostro tag nell'albero sintattico astratto (AST). Come detto sopra,
ereditiamo da Latte\Compiler\Nodes\StatementNode.
Create un file (per esempio DatetimeNode.php) e definite la classe:
<?php
namespace App\Templating;
use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\PrintContext;
use Latte\Compiler\Tag;
class DatetimeNode extends StatementNode
{
/**
* Funzione di analisi del tag, chiamata quando viene trovato {datetime}.
*/
public static function create(Tag $tag): self
{
// il nostro tag stampa contenuto, quindi conserviamo l'indentazione circostante
$tag->outputMode = $tag::OutputKeepIndentation;
// per ora il nostro semplice tag non accetta argomenti, quindi non dobbiamo analizzare nulla
$node = $tag->node = new self;
return $node;
}
/**
* Genera il codice PHP che verrà eseguito durante il rendering del template.
*/
public function print(PrintContext $context): string
{
return $context->format(
'echo date(\'Y-m-d H:i:s\') %line;',
$this->position,
);
}
/**
* Rende accessibili i nodi figli ai compiler pass di Latte.
*/
public function &getIterator(): \Generator
{
false && yield;
}
}
Quando Latte incontra {datetime} in un template, chiama la funzione di analisi del tag create(). Il
suo compito è restituire un'istanza di DatetimeNode. Impostiamo inoltre $tag->outputMode su
OutputKeepIndentation; poiché un tag lavora nella modalità predefinita OutputNone (spiegata in Modalità di output dei tag), un tag collocato prima del primo testo del template
potrebbe altrimenti emettere il proprio output nel metodo generato prepare() invece che in main().
Impostare questa modalità garantisce che l'output finisca dove sta il tag.
Il metodo print() genera il codice PHP che verrà eseguito durante il rendering del template. Chiamiamo il metodo
$context->format(), che assembla la stringa di codice PHP risultante per il template compilato. Il primo
argomento, 'echo date('Y-m-d H:i:s') %line;', è la maschera in cui vengono sostituiti i parametri successivi. Il
segnaposto %line dice al metodo format() di prendere l'argomento seguente, cioè
$this->position, e di inserire un commento come /* pos 15:1 */ che collega il codice PHP generato
alla riga originale del template, cosa essenziale per il debugging.
La proprietà $this->position è ereditata dalla classe base Node ed è impostata automaticamente
dal parser di Latte. Contiene un oggetto Latte\Compiler\Range (una sottoclasse di
Position estesa con una length in byte) che indica dove si trova il tag nel file sorgente
.latte. Per i tag di tipo pari l'intervallo va dal tag di apertura a quello di chiusura, e i discendenti di
StatementNode espongono inoltre $this->tagRanges, che elenca il Range di ogni tag
costituente (di apertura, intermedi come {else}/{case} e di chiusura).
Il metodo getIterator() è essenziale per i compiler pass. Deve restituire tutti i nodi figli, ma il nostro
semplice DatetimeNode per ora non ha né argomenti né contenuto, quindi non ha nodi figli. Il metodo deve però
esistere comunque ed essere un generatore, cioè la parola chiave yield deve essere in qualche modo presente nel
corpo del metodo.
Registrazione tramite un'estensione
Infine, comunicate a Latte il nuovo tag. Create una classe Extension (per esempio
MyLatteExtension.php) e registrate il tag nel suo metodo getTags().
<?php
namespace App\Templating;
use Latte\Extension;
class MyLatteExtension extends Extension
{
/**
* Restituisce l'elenco dei tag forniti da questa estensione.
* @return array<string, callable> Mappa: 'nome-tag' => funzione-di-analisi
*/
public function getTags(): array
{
return [
'datetime' => DatetimeNode::create(...),
// qui registrerete altri tag più avanti
];
}
}
Registrate poi questa estensione nel motore di Latte:
$latte = new Latte\Engine;
$latte->addExtension(new App\Templating\MyLatteExtension);
Create il template:
<p>Pagina generata il: {datetime}</p>
Output atteso: <p>Pagina generata il: 2023-10-27 11:00:00</p>
Riepilogo di questa fase
Abbiamo creato con successo un tag personalizzato di base, {datetime}. Ne abbiamo definito la rappresentazione
nell'AST (DatetimeNode), gestito l'analisi (create()), indicato come deve generare il codice PHP
(print()), garantito che i suoi figli siano attraversabili (getIterator()) e lo abbiamo registrato
in Latte.
Nella prossima sezione amplieremo questo tag perché accetti argomenti, mostrando come analizzare le espressioni e gestire i nodi figli.
Analizzare gli argomenti del tag
Il nostro semplice tag {datetime} funziona, ma non è molto flessibile. Ampliamolo perché accetti un argomento
facoltativo: una stringa di formato per la funzione date(). La sintassi desiderata sarà
{datetime $format}.
Obiettivo: modificare {datetime} perché accetti come argomento un'espressione PHP facoltativa, da usare
come stringa di formato per date().
Facciamo conoscenza con TagParser
Prima di modificare il codice è importante capire lo strumento che useremo, Latte\Compiler\TagParser. Quando il parser principale
di Latte (TemplateParser) incontra un tag Latte come {datetime ...} o un n:attributo, delega l'analisi
del contenuto interno al tag (la parte tra { e }, oppure il valore dell'attributo) a un
TagParser specializzato.
Questo TagParser lavora esclusivamente sugli argomenti del tag. Il suo compito è consumare i token che
rappresentano questi argomenti. Cosa essenziale, deve analizzare tutto il contenuto che gli viene fornito. Se la vostra
funzione di analisi termina ma il TagParser non ha raggiunto la fine degli argomenti (lo si verifica con
$tag->parser->isEnd()), Latte solleverà un'eccezione, perché significa che dentro il tag sono rimasti token
inattesi. Al contrario, se un tag richiede argomenti, dovreste chiamare $tag->expectArguments() all'inizio
della vostra funzione di analisi. Questo metodo controlla che gli argomenti siano presenti e solleva un'eccezione esplicativa se
il tag è stato usato senza.
TagParser offre metodi utili per analizzare diversi tipi di argomenti:
parseExpression(): ExpressionNode: analizza un'espressione simile a PHP (variabili, letterali, operatori, chiamate a funzioni e metodi ecc.). Gestisce lo zucchero sintattico di Latte, per esempio trattando le semplici stringhe alfanumeriche come stringhe tra apici (per esempiofooviene analizzato come se fosse'foo').parseUnquotedStringOrExpression(): ExpressionNode: analizza una normale espressione oppure una stringa senza apici. Le stringhe senza apici sono sequenze ammesse da Latte senza apici, usate spesso per cose come i percorsi dei file (per esempio{include ../file.latte}). Se analizza una stringa senza apici, restituisce unoStringNode.parseArguments(): ArrayNode: analizza argomenti separati da virgole, eventualmente con chiavi, come10, name: 'John', true.parseModifier(): ModifierNode: analizza i filtri come|upper|truncate:10.parseType(): ?SuperiorTypeNode: analizza le dichiarazioni di tipo di PHP, comeint,?string,array|Foo.
Per esigenze di analisi più complesse o di basso livello potete interagire direttamente con il flusso di token tramite
$tag->parser->stream. Questo oggetto offre metodi per esaminare e consumare i singoli token:
$tag->parser->stream->is(...): bool: controlla se il token corrente corrisponde a uno dei tipi indicati (per esempioToken::Php_Variable) o a valori letterali (per esempio'as') senza consumarlo. Utile per guardare avanti.$tag->parser->stream->consume(...): Token: consuma il token corrente e sposta in avanti la posizione nel flusso. Se come argomenti sono indicati tipi o valori di token attesi e il token corrente non corrisponde, solleva unaCompileException. Usatelo quando vi aspettate un determinato token.$tag->parser->stream->tryConsume(...): ?Token: prova a consumare il token corrente solo se corrisponde a uno dei tipi o valori indicati. Se corrisponde, lo consuma e lo restituisce. Se non corrisponde, lascia invariata la posizione nel flusso e restituiscenull. Usatelo per i token facoltativi o quando dovete scegliere tra percorsi sintattici diversi.
Aggiornare la funzione di analisi create()
Con queste premesse, modifichiamo il metodo create() di DatetimeNode per analizzare l'argomento
facoltativo del formato 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
{
// aggiunge una proprietà pubblica che contiene il nodo dell'espressione del formato
public ?ExpressionNode $format = null;
public static function create(Tag $tag): self
{
$node = $tag->node = new self;
// controlla se ci sono token
if (!$tag->parser->isEnd()) {
// analizza l'argomento come espressione simile a PHP usando il TagParser.
$node->format = $tag->parser->parseExpression();
}
return $node;
}
// ... i metodi print() e getIterator() li aggiorniamo tra poco ...
}
Abbiamo aggiunto la proprietà pubblica $format. In create() usiamo ora
$tag->parser->isEnd() per controllare se gli argomenti ci sono. In tal caso
$tag->parser->parseExpression() consuma i token dell'espressione. Poiché il TagParser deve
consumare tutti i token in ingresso, Latte solleverà automaticamente un errore se l'utente scrive qualcosa di inatteso dopo
l'espressione del formato (per esempio {datetime 'Y-m-d', unexpected}).
Aggiornare il metodo print()
Modifichiamo ora il metodo print() perché usi l'espressione del formato analizzata e salvata in
$this->format. Se non è stato indicato alcun formato ($this->format è null),
dobbiamo usare una stringa di formato predefinita, per esempio 'Y-m-d H:i:s'.
public function print(PrintContext $context): string
{
$formatNode = $this->format ?? new StringNode('Y-m-d H:i:s');
// %node stampa la rappresentazione in codice PHP di $formatNode.
return $context->format(
'echo date(%node) %line;',
$formatNode,
$this->position
);
}
Nella variabile $formatNode salviamo il nodo dell'AST che rappresenta la stringa di formato per la funzione PHP
date(). Qui usiamo l'operatore di coalescenza null (??). Se l'utente ha indicato un argomento nel
template (per esempio {datetime 'd.m.Y'}), la proprietà $this->format contiene il nodo
corrispondente (in questo caso uno StringNode con il valore 'd.m.Y') e viene usato quel nodo. Se
l'utente non ha indicato alcun argomento (ha scritto solo {datetime}), la proprietà $this->format è
null e creiamo invece un nuovo StringNode con il formato predefinito 'Y-m-d H:i:s'. Così
$formatNode contiene sempre un nodo AST valido per il formato.
Nella maschera 'echo date(%node) %line;' compare il nuovo segnaposto %node, che dice al metodo
format() di prendere il primo argomento seguente (cioè il nostro $formatNode), di chiamarne il metodo
print() (che restituisce la sua rappresentazione in codice PHP) e di inserire il risultato al posto del
segnaposto.
Implementare getIterator() per i sottonodi
Il nostro DatetimeNode ha ora un nodo figlio: l'espressione $format. Dobbiamo renderlo
accessibile ai compiler pass restituendolo nel metodo getIterator(). Ricordate di restituire un riferimento
(&), per permettere ai pass di sostituire eventualmente il nodo.
public function &getIterator(): \Generator
{
if ($this->format) {
yield $this->format;
}
}
Perché è essenziale? Immaginate un pass della Sandbox che deve controllare se l'argomento $format contiene una
chiamata a una funzione vietata (per esempio {datetime dangerousFunction()}). Se getIterator() non
restituisse $this->format, il pass della Sandbox non vedrebbe mai la chiamata a dangerousFunction()
dentro l'argomento del nostro tag, creando una potenziale falla di sicurezza. Restituendolo permettiamo alla Sandbox (e agli altri
pass) di esaminare ed eventualmente modificare il nodo dell'espressione $format.
Usare il tag ampliato
Il tag ora gestisce correttamente un argomento facoltativo:
Formato predefinito: {datetime}
Formato personalizzato: {datetime 'd.m.Y'}
Con una variabile: {datetime $userDateFormatPreference}
{* questo causerebbe un errore dopo l'analisi di 'd.m.Y', perché ", foo" è inatteso *}
{* {datetime 'd.m.Y', foo} *}
Vedremo ora come creare tag di tipo pari che elaborano il contenuto racchiuso tra di essi.
Gestire i tag di tipo pari
Finora il nostro tag {datetime} è autochiudente (concettualmente). Non ha alcun contenuto tra un tag di
apertura e uno di chiusura. Molti tag utili, però, lavorano su un blocco di contenuto del template. Si chiamano tag di tipo
pari. Esempi ne sono {if}...{/if}, {block}...{/block} oppure il tag personalizzato che costruiremo
ora: {debug}...{/debug}.
Questo tag ci permetterà di inserire nei nostri template informazioni di debug che devono essere visibili solo durante lo sviluppo.
Obiettivo: creare un tag di tipo pari {debug} il cui contenuto viene disegnato solo se è attivo un
determinato flag di “modalità di sviluppo”.
Facciamo conoscenza con i provider
A volte i vostri tag hanno bisogno di accedere a dati o servizi che non vengono passati direttamente come parametri del template. Per esempio per stabilire se l'applicazione è in modalità di sviluppo, per accedere a un oggetto utente o per ottenere valori di configurazione. Latte offre a questo scopo un meccanismo chiamato provider.
I provider si registrano nella vostra estensione con il metodo
getProviders(). Questo metodo restituisce un array associativo in cui le chiavi sono i nomi con cui i provider
saranno accessibili nel codice di runtime del template e i valori sono i dati o gli oggetti veri e propri.
Nel codice PHP generato dal metodo print() del vostro tag potete poi accedere a questi provider tramite la
proprietà speciale dell'oggetto $this->global. Poiché questa proprietà è condivisa da tutte le estensioni, è
buona pratica anteporre un prefisso ai nomi dei vostri provider, per evitare possibili collisioni di nomi con i provider
interni di Latte o con quelli di altre estensioni di terzi. Una convenzione diffusa è usare un prefisso breve e univoco, legato
al nome del vostro vendor o della vostra estensione. Nel nostro esempio usiamo il prefisso app, quindi il flag della
modalità di sviluppo sarà disponibile come $this->global->appDevMode.
La parola chiave yield per analizzare il
contenuto
Come diciamo al parser di Latte di elaborare il contenuto tra {debug} e {/debug}? È qui che
entra in gioco la parola chiave yield.
Quando yield viene usato nella funzione create(), la funzione diventa un generatore PHP. La sua esecuzione si mette in pausa e il
controllo torna al TemplateParser principale. Il TemplateParser continua allora ad analizzare il
contenuto del template finché non incontra il corrispondente tag di chiusura (nel nostro caso {/debug}).
Trovato il tag di chiusura, il TemplateParser riprende l'esecuzione della nostra funzione create()
subito dopo l'istruzione yield. Il valore restituito da yield è un array di due elementi:
- Un
AreaNodeche rappresenta il contenuto analizzato tra il tag di apertura e quello di chiusura. - L'oggetto
Tagche rappresenta il tag di chiusura (per esempio{/debug}).
Creiamo la classe DebugNode e il suo metodo 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
{
// proprietà pubblica in cui salvare il contenuto interno analizzato
public AreaNode $content;
/**
* Funzione di analisi del tag di tipo pari {debug} ... {/debug}.
*/
public static function create(Tag $tag): \Generator // notate il tipo di ritorno
{
$node = $tag->node = new self;
// mette in pausa l'analisi, ottiene il contenuto interno e il tag finale quando trova {/debug}
[$node->content, $endTag] = yield;
return $node;
}
// ... print() e getIterator() li implementiamo tra poco ...
}
Nota: $endTag è null se il tag è usato come n:attributo, cioè
<div n:debug>...</div>.
Un tag di tipo pari si può anche chiudere con una barra, come {debug/} (oppure
<div n:debug/>). Non ha allora contenuto interno: il generatore riceve
[$emptyFragmentNode, $startTag], dove il secondo elemento è il tag di apertura stesso, non
null.
Implementare print() per il rendering condizionale
Il metodo print() deve ora generare codice PHP che, in fase di esecuzione, controlli il provider
appDevMode ed esegua il codice del contenuto interno solo se il flag è vero.
public function print(PrintContext $context): string
{
// genera un'istruzione PHP 'if' che controlla il provider a runtime
return $context->format(
<<<'XX'
if ($this->global->appDevMode) %line {
// se in modalità di sviluppo, stampa il contenuto interno
%node
}
XX,
$this->position, // per il commento %line
$this->content, // il nodo che contiene l'AST del contenuto interno
);
}
È semplice. Usiamo PrintContext::format() per creare una normale istruzione PHP if. Dentro
l'if mettiamo il segnaposto %node per $this->content. Latte chiamerà ricorsivamente
$this->content->print($context) per generare il codice PHP della parte interna del tag, ma solo se a runtime
$this->global->appDevMode risulta vero.
Implementare getIterator() per il contenuto
Proprio come per il nodo dell'argomento nell'esempio precedente, il nostro DebugNode ha ora un nodo figlio:
l'AreaNode $content. Dobbiamo renderlo attraversabile restituendolo in getIterator():
public function &getIterator(): \Generator
{
// restituisce il riferimento al nodo del contenuto
yield $this->content;
}
Questo permette ai compiler pass di scendere nel contenuto del nostro tag {debug}, cosa importante anche se il
contenuto viene disegnato in modo condizionale. Per esempio, la Sandbox deve analizzare il contenuto indipendentemente dal fatto
che appDevMode sia vero o falso.
Registrazione e uso
Registrate il tag e il provider nella vostra estensione:
class MyLatteExtension extends Extension
{
// si presume che $isDevelopmentMode venga determinato da qualche parte (per esempio dalla configurazione)
public function __construct(
private bool $isDevelopmentMode,
) {
}
public function getTags(): array
{
return [
'datetime' => DatetimeNode::create(...),
'debug' => DebugNode::create(...), // registra il nuovo tag
];
}
public function getProviders(): array
{
return [
'appDevMode' => $this->isDevelopmentMode, // registra il provider
];
}
}
// al momento di registrare l'estensione:
$isDev = true; // determinatelo in base all'ambiente della vostra applicazione
$latte->addExtension(new MyLatteExtension($isDev));
E usatelo in un template:
<p>Contenuto normale, sempre visibile.</p>
{debug}
<div class="debug-panel">
ID dell'utente corrente: {$user->id}
Ora della richiesta: {=time()}
</div>
{/debug}
<p>Altro contenuto normale.</p>
Integrazione con gli n:attributi
Latte offre una comoda forma abbreviata per molti tag di tipo pari: gli n:attributi. Se avete un tag di tipo pari come
{tag}...{/tag} e volete che il suo effetto si applichi direttamente a un singolo elemento HTML, spesso potete
scriverlo in modo più conciso come attributo n:tag su quell'elemento.
Per la maggior parte dei tag di tipo pari standard che definite (come il nostro {debug}), Latte attiva
automaticamente la corrispondente versione con attributo n:. Non dovete fare nulla di più durante la
registrazione:
{* uso standard come tag di tipo pari *}
{debug}<div>Debug info</div>{/debug}
{* uso equivalente con n:attributo *}
<div n:debug>Debug info</div>
Entrambi disegneranno il <div> solo se $this->global->appDevMode è vero. Anche
i prefissi inner- e tag- funzionano come previsto.
A volte la logica del vostro tag può doversi comportare in modo leggermente diverso a seconda che venga usato come tag di
tipo pari standard o come n:attributo, oppure se viene usato un prefisso come n:inner-tag o n:tag-tag.
L'oggetto Latte\Compiler\Tag, passato alla vostra funzione di analisi create(), fornisce questa
informazione:
$tag->isNAttribute(): bool: restituiscetruese il tag viene analizzato come n:attributo$tag->prefix: ?string: restituisce il prefisso usato con l'n:attributo, che può esserenull(non è un n:attributo),Tag::PrefixNone,Tag::PrefixInneroppureTag::PrefixTag
Ora che conosciamo i tag semplici, l'analisi degli argomenti, i tag di tipo pari, i provider e gli n:attributi, affrontiamo
uno scenario più complesso, con tag annidati dentro altri tag, partendo dal nostro tag {debug}.
Tag intermedi
Alcuni tag di tipo pari permettono, o addirittura richiedono, che altri tag compaiano al loro interno prima del tag
di chiusura finale. Si chiamano tag intermedi. Esempi classici sono {if}...{elseif}...{else}...{/if} oppure
{switch}...{case}...{default}...{/switch}.
Ampliamo il nostro tag {debug} perché supporti una clausola {else} facoltativa, che verrà disegnata
quando l'applicazione non è in modalità di sviluppo.
Obiettivo: modificare {debug} perché supporti un tag intermedio {else} facoltativo. La
sintassi finale dovrà essere {debug} ... {else} ... {/debug}.
Analizzare i tag intermedi con yield
Sappiamo già che yield mette in pausa la funzione di analisi create() e restituisce il contenuto
analizzato insieme al tag finale. yield offre però un controllo maggiore: potete passargli un array di nomi di
tag intermedi. Quando il parser incontra uno di questi tag allo stesso livello di annidamento (cioè come figli
diretti del tag genitore, non dentro altri blocchi o tag al suo interno), interrompe anch'esso l'analisi del contenuto.
Quando l'analisi si interrompe per un tag intermedio, il parser smette di analizzare il contenuto, riprende il generatore
create() e restituisce il contenuto analizzato parzialmente e il tag intermedio stesso (invece del tag
finale). La nostra funzione create() può allora gestire questo tag intermedio (per esempio analizzarne gli
argomenti, se ne ha) e chiamare di nuovo yield per analizzare la parte successiva del contenuto, fino al tag
finale di chiusura o a un altro tag intermedio atteso.
Modifichiamo DebugNode::create() perché si aspetti {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
{
// contenuto della parte {debug}
public AreaNode $thenContent;
// contenuto facoltativo della parte {else}
public ?AreaNode $elseContent = null;
public static function create(Tag $tag): \Generator
{
$node = $tag->node = new self;
// yield e attesa di {/debug} oppure di {else}
[$node->thenContent, $nextTag] = yield ['else'];
// controlla se il tag su cui ci siamo fermati era {else}
if ($nextTag?->name === 'else') {
// yield di nuovo, per analizzare il contenuto tra {else} e {/debug}
[$node->elseContent, $endTag] = yield;
}
return $node;
}
// ... print() e getIterator() li aggiorniamo tra poco ...
}
Ora yield ['else'] dice a Latte di interrompere l'analisi non solo per {/debug}, ma anche per
{else}. Se incontra {else}, $nextTag conterrà l'oggetto Tag di
{else}. Chiamiamo poi di nuovo yield senza argomenti, il che significa che ora aspettiamo solo il tag
finale {/debug}, e salviamo il risultato in $node->elseContent. Se {else} non è stato
trovato, $nextTag sarebbe il Tag di {/debug} (oppure null se usato come
n:attributo) e $node->elseContent resterebbe null.
Implementare print() con {else}
Il metodo print() deve rispecchiare la nuova struttura. Deve generare un'istruzione PHP if/else
basata sul provider appDevMode.
public function print(PrintContext $context): string
{
return $context->format(
<<<'XX'
if ($this->global->appDevMode) %line {
%node // codice del ramo 'then' (contenuto di {debug})
} else {
%node // codice del ramo 'else' (contenuto di {else})
}
XX,
$this->position, // numero di riga per la condizione 'if'
$this->thenContent, // primo segnaposto %node
$this->elseContent ?? new NopNode, // secondo segnaposto %node
);
}
È una normale struttura PHP if/else. Usiamo %node due volte; format() sostituisce
i nodi indicati in sequenza. Usiamo ?? new NopNode per evitare errori se $this->elseContent è
null: il NopNode semplicemente non stampa nulla.
Implementare getIterator() per entrambi
i contenuti
Abbiamo ora potenzialmente due nodi figli di contenuto ($thenContent e $elseContent). Dobbiamo
restituirli entrambi, se esistono:
public function &getIterator(): \Generator
{
yield $this->thenContent;
if ($this->elseContent) {
yield $this->elseContent;
}
}
Usare il tag ampliato
Il tag si può ora usare con una clausola {else} facoltativa:
{debug}
<p>Mostro le informazioni di debug perché devMode è attivo.</p>
{else}
<p>Le informazioni di debug sono nascoste perché devMode è disattivato.</p>
{/debug}
Gestire lo stato e l'annidamento
I nostri esempi precedenti ({datetime}, {debug}) erano relativamente privi di stato nei loro metodi
print(). Stampavano direttamente del contenuto oppure eseguivano un semplice controllo condizionale basato su un
provider globale. Molti tag, però, devono gestire una qualche forma di stato durante il rendering, o comportano la
valutazione di espressioni fornite dall'utente che, per prestazioni o correttezza, vanno eseguite una sola volta. Inoltre
dobbiamo considerare cosa succede quando i nostri tag personalizzati vengono annidati.
Illustriamo questi concetti creando un tag {repeat $count}...{/repeat}. Questo tag ripeterà il proprio contenuto
interno $count volte.
Obiettivo: implementare {repeat $count}, che ripete il proprio contenuto un numero indicato di volte.
La necessità di variabili temporanee e univoche
Immaginate che l'utente scriva:
{repeat rand(1, 5)} Contenuto {/repeat}
Se nel nostro metodo print() generassimo ingenuamente un ciclo PHP for come questo:
// semplificato, codice generato ERRATO
for ($i = 0; $i < rand(1, 5); $i++) {
// stampa il contenuto
}
Sarebbe sbagliato! L'espressione rand(1, 5) verrebbe rivalutata a ogni iterazione del ciclo, con un numero
imprevedibile di ripetizioni. Dobbiamo valutare l'espressione $count una sola volta, prima che il ciclo
inizi, e salvarne il risultato.
Genereremo codice PHP che prima valuta l'espressione del conteggio e ne salva il valore in una variabile temporanea di
runtime. Per evitare collisioni con le variabili definite dall'utente del template e con le variabili interne di Latte
(come $ʟ_...), useremo per le nostre variabili temporanee la convenzione del prefisso $__ (doppio
trattino basso).
Il codice generato avrebbe allora questo aspetto:
$__count = rand(1, 5);
for ($__i = 0; $__i < $__count; $__i++) {
// stampa il contenuto
}
Consideriamo ora l'annidamento:
{repeat $countA} {* ciclo esterno *}
{repeat $countB} {* ciclo interno *}
...
{/repeat}
{/repeat}
Se sia il tag {repeat} esterno sia quello interno generassero codice usando gli stessi nomi di variabile
temporanea (per esempio $__count e $__i), il ciclo interno sovrascriverebbe le variabili di quello
esterno, mandando in pezzi la logica.
Dobbiamo garantire che le variabili temporanee generate per ogni istanza del tag {repeat} siano univoche.
Lo otteniamo con PrintContext::generateId(). Questo metodo restituisce un intero univoco durante la fase di
compilazione. Possiamo aggiungere questo ID ai nomi delle nostre variabili temporanee.
Invece di $__count genereremo quindi un nome con un suffisso numerico univoco, per esempio
$__count_0, e allo stesso modo per il contatore del ciclo, per esempio $__i_0. I numeri veri e propri
provengono da un contatore valido per l'intera compilazione e condiviso da tutti i nodi, quindi sono garantiti solo come univoci,
non come sequenza per singolo tag.
Implementare RepeatNode
Creiamo la classe del 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;
/**
* Funzione di analisi di {repeat $count} ... {/repeat}
*/
public static function create(Tag $tag): \Generator
{
$tag->expectArguments(); // garantisce che $count sia indicato
$node = $tag->node = new self;
// analizza l'espressione del conteggio
$node->count = $tag->parser->parseExpression();
// ottiene il contenuto interno
[$node->content] = yield;
return $node;
}
/**
* Genera il ciclo PHP 'for' con nomi di variabile univoci.
*/
public function print(PrintContext $context): string
{
// genera nomi di variabile univoci
$id = $context->generateId();
$countVar = '$__count_' . $id; // nome univoco, per esempio $__count_0
$iteratorVar = '$__i_' . $id; // nome univoco, per esempio $__i_0
return $context->format(
<<<'XX'
// valuta l'espressione del conteggio *una sola volta* e la salva
%raw = (int) (%node);
// cicla usando il conteggio salvato e la variabile iteratore univoca
for (%raw = 0; %2.raw < %0.raw; %2.raw++) %line {
%node // disegna il contenuto interno
}
XX,
$countVar, // %0 - variabile in cui salvare il conteggio
$this->count, // %1 - nodo dell'espressione del conteggio
$iteratorVar, // %2 - nome della variabile iteratore del ciclo
$this->position, // %3 - commento con il numero di riga del ciclo stesso
$this->content // %4 - nodo del contenuto interno
);
}
/**
* Restituisce i nodi figli (l'espressione del conteggio e il contenuto).
*/
public function &getIterator(): \Generator
{
yield $this->count;
yield $this->content;
}
}
Il metodo create() analizza l'espressione obbligatoria $count con parseExpression(). Per
prima cosa viene chiamato $tag->expectArguments(). Questo garantisce che l'utente abbia indicato qualcosa
dopo {repeat}. $tag->parser->parseExpression() fallirebbe comunque se non fosse indicato nulla, ma
il messaggio di errore parlerebbe di sintassi inattesa. Usando expectArguments() l'errore è molto più chiaro e dice
espressamente che al tag {repeat} mancano gli argomenti.
Il metodo print() genera il codice PHP responsabile di eseguire a runtime la logica di ripetizione. Inizia
generando nomi univoci per le variabili PHP temporanee che gli serviranno.
Il metodo $context->format() viene chiamato con il nuovo segnaposto %raw, che inserisce la
stringa grezza passata come argomento corrispondente. Qui inserisce il nome di variabile univoco salvato in
$countVar (per esempio $__count_1). E %0.raw e %2.raw? Sono un esempio di
segnaposti posizionali. Al posto del semplice %raw, che prende l'argomento grezzo successivo
disponibile, %2.raw prende esplicitamente l'argomento all'indice 2 (cioè $iteratorVar) e ne inserisce
il valore grezzo. Questo ci permette di riutilizzare la stringa $iteratorVar senza passarla più volte nell'elenco
degli argomenti di format().
Questa chiamata a format(), costruita con cura, genera un ciclo PHP efficiente e sicuro, che gestisce
correttamente l'espressione del conteggio ed evita le collisioni di nomi anche quando i tag {repeat} sono
annidati.
Registrazione e uso
Registrate il tag nella vostra estensione:
use App\Templating\RepeatNode;
class MyLatteExtension extends Extension
{
public function getTags(): array
{
return [
'datetime' => DatetimeNode::create(...),
'debug' => DebugNode::create(...),
'repeat' => RepeatNode::create(...), // registra il tag repeat
];
}
}
Usatelo in un template, anche annidato:
{var $rows = rand(5, 7)}
{var $cols = rand(3, 5)}
{repeat $rows}
<tr>
{repeat $cols}
<td>Ciclo interno</td>
{/repeat}
</tr>
{/repeat}
Questo esempio mostra come gestire lo stato (i contatori del ciclo) e i possibili problemi di annidamento usando variabili
temporanee con prefisso $__ rese univoche con gli ID di PrintContext::generateId().
n:attributi puri
Molti n:attributi, come n:if o n:foreach, sono comode forme abbreviate dei
corrispondenti tag di tipo pari ({if}...{/if}, {foreach}...{/foreach}), ma Latte permette anche di
definire tag che esistono soltanto nella forma di n:attributo. Servono spesso a modificare gli attributi o il
comportamento dell'elemento HTML a cui sono agganciati.
Esempi standard integrati in Latte sono n:class, che
aiuta a costruire dinamicamente l'attributo class, e n:attr, che può impostare più attributi qualsiasi.
Creiamo un nostro n:attributo puro: n:confirm, che aggiungerà una finestra di conferma JavaScript prima che venga
eseguita un'azione (come seguire un link o inviare un form).
Obiettivo: implementare n:confirm="'Sei sicuro?'", che aggiunge un handler onclick per
impedire l'azione predefinita se l'utente annulla la finestra di conferma.
Implementare ConfirmNode
Ci servono una classe del nodo e una funzione di analisi.
<?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;
}
/**
* Genera il codice dell'attributo 'onclick' con l'escaping corretto.
*/
public function print(PrintContext $context): string
{
// garantisce l'escaping corretto sia per JavaScript sia per il contesto degli attributi 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;
}
}
Il metodo print() genera il codice PHP che alla fine stamperà l'attributo HTML onclick="..." durante
il rendering del template. Gestire contesti annidati (JavaScript dentro un attributo HTML) richiede un escaping accurato. L'helper
LR\Helpers::escapeJs(%node) viene chiamato a runtime ed esegue l'escaping corretto del messaggio per l'uso dentro
JavaScript (l'output sarebbe come "Sicuro?"). Poi l'helper LR\HtmlHelpers::escapeAttr(...) esegue
l'escaping dei caratteri speciali negli attributi HTML, trasformando l'output in
return confirm("Sicuro?"). Questo escaping a runtime in due passaggi garantisce che il messaggio
sia sicuro per JavaScript e che il codice JavaScript risultante sia sicuro da incorporare nell'attributo HTML
onclick.
Registrazione e uso
Registrate l'n:attributo nella vostra estensione. Ricordate il prefisso n: nella chiave:
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
];
}
}
Ora potete usare n:confirm su link, pulsanti o elementi di form:
<a href="delete.php?id=123" n:confirm='"Volete davvero eliminare l\'elemento {$id}?"'>Elimina</a>
HTML generato:
<a href="delete.php?id=123" onclick="return confirm("Volete davvero eliminare l'elemento 123?")">Elimina</a>
Quando l'utente clicca sul link, il browser esegue il codice onclick, mostra la finestra di conferma e prosegue
verso delete.php solo se l'utente clicca “OK”.
Questo esempio mostra come si possa creare un n:attributo puro per modificare il comportamento o gli attributi dell'elemento
HTML ospite, generando il codice PHP appropriato nel suo metodo print(). Ricordate il doppio escaping spesso
necessario: una volta per il contesto di destinazione (in questo caso JavaScript) e di nuovo per il contesto
dell'attributo HTML.
Altri due membri dell'oggetto Tag tornano utili scrivendo n:attributi puri: $tag->htmlElement vi
dà accesso all'elemento HTML circostante (un ElementNode), così potete esaminarlo o modificarlo, e
$tag->replaceNAttribute($node) vi permette di sostituire l'attributo con un nodo che costruite voi. Di fatto il
nodo restituito dal create() di un n:attributo puro sostituisce automaticamente l'attributo sul suo elemento.
Argomenti avanzati
Le sezioni precedenti coprono i concetti fondamentali, ma ecco alcuni argomenti più avanzati che potreste incontrare creando tag Latte personalizzati.
Modalità di output dei tag
L'oggetto Tag passato alla vostra funzione create() ha una proprietà outputMode. Questa
proprietà influenza il modo in cui Latte tratta gli spazi bianchi e l'indentazione circostanti, in particolare quando il tag è
usato da solo su una riga. Potete modificare questa proprietà nella vostra funzione create().
Tag::OutputNone(il valore predefinito per ogni tag, mantenuto dalle strutture di controllo come{if}o{foreach}): gli spazi bianchi attorno al tag vengono trattati esattamente come conOutputRemoveIndentation, cioè l'indentazione iniziale e un singolo a capo finale vengono rimossi. La vera differenza è interna: questa modalità mantiene il parser del template nella modalità “head” del template. È adatta ai tag di dichiarazione o di configurazione, come{var}o{default}, che non producono output diretto.Tag::OutputRemoveIndentation(impostata esplicitamente dai tag di blocco{block},{embed},{include}e{sandbox}): rimuove l'indentazione iniziale davanti al tag e un singolo a capo finale. Questo aiuta a mantenere più pulito il codice PHP generato ed evita righe vuote in più nell'output HTML causate dal tag stesso.Tag::OutputKeepIndentation(impostata esplicitamente dai tag di output come{=...}): Latte cerca di conservare l'indentazione davanti al tag; gli a capo dopo il tag vengono di norma mantenuti. È adatta ai tag che stampano contenuto in linea, vedi l'esempio{datetime}qui sopra, che imposta questa modalità proprio per questo motivo.
Scegliete la modalità che meglio si adatta allo scopo del vostro tag. Poiché il valore predefinito è
OutputNone, i tag di controllo del flusso e di dichiarazione non richiedono modifiche; impostate
OutputKeepIndentation per i tag che stampano contenuto su una riga propria.
Accedere ai tag genitori o più vicini
A volte il comportamento di un tag deve dipendere dal contesto in cui è usato, in particolare dai tag genitori entro cui si
trova. L'oggetto Tag passato alla vostra funzione create() offre a questo scopo il metodo
closestTag(array $classes, ?callable $condition = null): ?Tag.
Questo metodo cerca verso l'alto nella gerarchia dei tag Latte attualmente aperti (la catena di $tag->parent;
gli elementi HTML circostanti non ne fanno parte) e restituisce l'oggetto Tag dell'antenato più vicino che soddisfa
i criteri indicati. Se non trova alcun antenato corrispondente, restituisce null.
L'array $classes indica che tipo di tag antenati state cercando. Controlla se la classe del nodo associato al tag
antenato ($ancestorTag->node) è esattamente una di quelle elencate; le sottoclassi non contano.
function create(Tag $tag)
{
// cerca il tag antenato più vicino il cui nodo è un'istanza di ForeachNode
$foreachTag = $tag->closestTag([ForeachNode::class]);
if ($foreachTag) {
// possiamo accedere all'istanza di ForeachNode stessa:
$foreachNode = $foreachTag->node;
}
}
Notate $foreachTag->node: funziona solo perché nello sviluppo dei tag Latte è convenzione assegnare subito il
nodo creato a $tag->node dentro il metodo create(), come abbiamo sempre fatto.
A volte non basta far corrispondere il tipo di nodo. Potreste dover controllare una proprietà specifica del possibile tag
antenato o del suo nodo. Il secondo argomento facoltativo di closestTag() è un callable che riceve il possibile
oggetto Tag antenato e deve restituire se è una corrispondenza valida.
function create(Tag $tag)
{
$dynamicBlockTag = $tag->closestTag(
[BlockNode::class],
// condizione: il blocco deve essere dinamico
fn(Tag $blockTag) => $blockTag->node->block->isDynamic(),
);
}
Usare closestTag() vi permette di creare tag consapevoli del contesto e di imporre un uso corretto all'interno
della struttura del vostro template, per template più solidi e comprensibili.
Segnaposti di PrintContext::format()
Abbiamo usato spesso PrintContext::format() per generare codice PHP nei metodi print() dei nostri
nodi. Accetta una stringa maschera e gli argomenti successivi, che sostituiscono i segnaposti nella maschera. Ecco un riepilogo
dei segnaposti disponibili:
%node: l'argomento deve essere un'istanza diNode. Ne chiama il metodoprint()e inserisce la stringa di codice PHP risultante.%dump: l'argomento è un valore PHP qualsiasi. Esporta il valore in codice PHP valido. Adatto a scalari, array, null.$context->format('echo %dump;', 'Hello')→echo 'Hello';$context->format('$arr = %dump;', [1, 2])→$arr = [1, 2];
%raw: inserisce l'argomento direttamente nel codice PHP di output, senza alcun escaping o modifica. Usatelo con cautela, soprattutto per inserire frammenti di codice PHP pregenerati o nomi di variabile.$context->format('%raw = 1;', '$variableName')→$variableName = 1;
%args: l'argomento deve essere unExpression\ArrayNode. Stampa gli elementi dell'array formattati come argomenti di una chiamata a funzione o metodo (separati da virgole, gestendo gli argomenti nominali se presenti).$argsNode = new ArrayNode([...]);$context->format('myFunc(%args);', $argsNode)→myFunc(1, name: 'Joe');
%line: l'argomento deve essere un oggettoPosition(oRange), di norma$this->position. Inserisce un commento PHP/* pos X:Y */che indica la riga e la colonna nel sorgente.$context->format('echo "Hi" %line;', $this->position)→echo "Hi" /* pos 42:1 */;
%escape(...): genera codice PHP che, a runtime, esegue l'escaping dell'espressione interna secondo le regole di escaping sensibili al contesto correnti.$context->format('echo %escape(%node);', $variableNode)
%modify(...): l'argomento deve essere unModifierNode. Genera codice PHP che applica al contenuto interno i filtri indicati nelModifierNode, compreso l'escaping sensibile al contesto se non disattivato da|noescape.$context->format('%modify(%node);', $modifierNode, $variableNode)
%modifyContent(...): simile a%modify, ma pensato per modificare blocchi di contenuto catturato (spesso HTML).
Potete fare riferimento esplicito agli argomenti tramite il loro indice a partire da zero: %0.node,
%1.dump, %2.raw e così via. Questo permette di riutilizzare un argomento più volte nella maschera
senza passarlo ripetutamente a format(). Vedi l'esempio del tag {repeat}, dove sono stati usati
%0.raw e %2.raw.
Esempio di analisi complessa degli argomenti
parseExpression(), parseArguments() e simili coprono molti casi, ma a volte serve una logica di
analisi più intricata, che usa il TokenStream di livello più basso, disponibile tramite
$tag->parser->stream.
Obiettivo: creare un tag {embedYoutube $videoID, width: 640, height: 480}. Vogliamo analizzare un ID video
obbligatorio (stringa o variabile) seguito da coppie chiave-valore facoltative per le dimensioni.
<?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;
// analizza l'ID video obbligatorio
$node->videoId = $tag->parser->parseExpression();
// analizza le coppie chiave-valore facoltative
$stream = $tag->parser->stream; // ottiene il flusso di token
while ($stream->tryConsume(',')) { // richiede la separazione con la virgola
// si aspetta l'identificatore 'width' o 'height'
$keyToken = $stream->consume(Token::Php_Identifier);
$key = strtolower($keyToken->text);
$stream->consume(':'); // si aspetta il separatore due punti
$value = $tag->parser->parseExpression(); // analizza l'espressione del valore
if ($key === 'width') {
$node->width = $value;
} elseif ($key === 'height') {
$node->height = $value;
} else {
throw new CompileException("Argomento sconosciuto '$key'. Attesi 'width' o 'height'.", $keyToken->position);
}
}
return $node;
}
// ... print() e getIterator() ...
}
Questo livello di controllo vi permette di definire sintassi molto specifiche e complesse per i vostri tag personalizzati, interagendo direttamente con il flusso di token.
Usare AuxiliaryNode
Latte offre nodi “di supporto” generici per le situazioni particolari durante la generazione del codice o dentro
i compiler pass. Sono AuxiliaryNode e Php\Expression\AuxiliaryNode.
Considerate AuxiliaryNode come un nodo contenitore flessibile, che delega le sue funzionalità principali (la
generazione del codice e l'esposizione dei nodi figli) agli argomenti passati al costruttore:
- Delega di
print(): il primo argomento del costruttore è una closure PHP. Quando Latte chiama il metodoprint()di unAuxiliaryNode, esegue questa closure. La closure riceve ilPrintContexte tutti i nodi passati nel secondo argomento del costruttore, il che vi permette di definire al volo una logica di generazione del codice PHP completamente personalizzata. - Delega di
getIterator(): il secondo argomento del costruttore è un array di oggettiNode. Quando Latte deve attraversare i figli di unAuxiliaryNode(per esempio durante i compiler pass), il suo metodogetIterator()restituisce semplicemente i nodi contenuti in questo array.
Esempio:
$node = new AuxiliaryNode(
// 1. questa closure diventa il corpo di print()
fn(PrintContext $context, $arg1, $arg2) => $context->format('...%node...%node...', $arg1, $arg2),
// 2. questi nodi vengono restituiti da getIterator() e passati alla closure qui sopra
[$argumentNode1, $argumentNode2]
);
Latte offre due tipi distinti, in base al punto in cui vi serve inserire il codice generato:
Latte\Compiler\Nodes\Php\Expression\AuxiliaryNode: usatelo quando dovete generare un pezzo di codice PHP che rappresenta un'espressioneLatte\Compiler\Nodes\AuxiliaryNode: usatelo per scopi più generali, quando dovete inserire un blocco di codice PHP che rappresenta una o più istruzioni
Il motivo importante per usare AuxiliaryNode invece dei nodi normali (come StaticMethodCallNode)
dentro il vostro metodo print() o in un compiler pass è controllare la visibilità per i compiler pass
successivi, soprattutto quelli legati alla sicurezza, come la Sandbox.
Immaginate questo scenario: il vostro compiler pass deve avvolgere un'espressione fornita dall'utente ($userExpr)
in una chiamata a una funzione di supporto specifica e fidata, myInternalSanitize($userExpr). Se create un nodo
normale new FunctionCallNode('myInternalSanitize', [$userExpr]), sarà completamente visibile a chi attraversa l'AST.
Se in seguito viene eseguito un pass della Sandbox e myInternalSanitize non è nella sua lista di funzioni
consentite, la Sandbox potrebbe bloccare o modificare questa chiamata, rompendo la logica interna del vostro tag, anche se
voi, autori del tag, sapete che quella chiamata è sicura e necessaria. Potete quindi generare la chiamata direttamente
dentro la closure dell'AuxiliaryNode.
use Latte\Compiler\Nodes\Php\Expression\AuxiliaryNode;
// ... dentro print() o in un compiler pass ...
$wrappedNode = new AuxiliaryNode(
fn(PrintContext $context, $userExpr) => $context->format(
'myInternalSanitize(%node)', // generazione diretta del codice PHP
$userExpr,
),
// IMPORTANTE: passate comunque qui il nodo dell'espressione originale dell'utente!
[$userExpr],
);
In questo caso il pass della Sandbox vede l'AuxiliaryNode, ma non analizza il codice PHP generato dalla sua
closure. Non può bloccare direttamente la chiamata a myInternalSanitize generata dentro la closure.
Il codice PHP generato è nascosto ai pass, ma gli input di quel codice (i nodi che rappresentano dati o espressioni
dell'utente) devono restare attraversabili. Ecco perché il secondo argomento del costruttore di AuxiliaryNode
è essenziale. Dovete passare un array contenente tutti i nodi originali (come $userExpr nell'esempio qui
sopra) usati dalla vostra closure. Il getIterator() di AuxiliaryNode restituirà questi nodi,
permettendo ai compiler pass come la Sandbox di analizzarli alla ricerca di potenziali problemi.
Buone pratiche
- Scopo chiaro: assicuratevi che il vostro tag abbia uno scopo chiaro e necessario. Non create tag per compiti risolvibili facilmente con i filtri o con le funzioni.
- Implementate correttamente
getIterator(): implementate sempregetIterator()e restituite riferimenti (&) a tutti i nodi figli (argomenti, contenuto) analizzati dal template. È essenziale per i compiler pass, per la sicurezza (Sandbox) e per eventuali ottimizzazioni future. - Proprietà pubbliche per i nodi: rendete pubbliche le proprietà che contengono i nodi figli, così che i compiler pass possano eventualmente modificarle.
- Usate
PrintContext::format(): sfruttate il metodoformat()per generare il codice PHP. Gestisce le virgolette, esegue correttamente l'escaping dei segnaposti e aggiunge automaticamente i commenti con i numeri di riga. - Variabili temporanee (
$__): quando generate codice PHP di runtime che ha bisogno di variabili temporanee (per esempio per salvare risultati intermedi o contatori di ciclo), usate la convenzione del prefisso$__per evitare collisioni con le variabili dell'utente e con le variabili interne$ʟ_di Latte. - Annidamento e ID univoci: se il vostro tag può essere annidato o ha bisogno a runtime di uno stato specifico
dell'istanza, usate
$context->generateId()nel vostro metodoprint()per creare suffissi univoci per le vostre variabili temporanee$__. - Provider per i dati esterni: usate i provider (registrati con
Extension::getProviders()) per accedere ai dati o ai servizi di runtime ($this->global->…), invece di scrivere valori nel codice o affidarvi allo stato globale. Usate prefissi di vendor per i nomi dei provider. - Considerate gli n:attributi: se il vostro tag di tipo pari opera logicamente su un singolo elemento HTML, è probabile
che Latte offra il supporto automatico agli
n:attributi. Tenetelo presente per la comodità degli utenti. Se create un tag che modifica gli attributi, valutate se unn:attributopuro sia la forma più adatta. - Test: scrivete test per i vostri tag, che coprano sia l'analisi dei vari input sintattici sia la correttezza dell'output del codice PHP generato.
Seguendo queste indicazioni potete creare tag personalizzati potenti, solidi e manutenibili, perfettamente integrati con il motore di template Latte.
Studiare le classi dei nodi che fanno parte di Latte è il modo migliore per imparare tutti i dettagli più minuti del processo di analisi.