Creazione di tag personalizzati
Questa pagina fornisce una guida completa per la creazione di tag personalizzati in Latte. Discuteremo di tutto, dai tag semplici a scenari più complessi con contenuto nidificato e specifiche esigenze di parsing, basandoci sulla tua comprensione di come Latte compila i template.
I tag personalizzati forniscono il massimo livello di controllo sulla sintassi del template e sulla logica di rendering, ma sono anche il punto di estensione più complesso. Prima di decidere di creare un tag personalizzato, considera sempre se non esiste una soluzione più semplice o se un tag adatto non esiste già nel set standard. Utilizza i tag personalizzati solo quando le alternative più semplici non sono sufficienti per le tue esigenze.
Comprensione del processo di compilazione
Per creare efficacemente tag personalizzati, è utile spiegare come Latte elabora i template. Comprendere questo processo chiarisce perché i tag sono strutturati in questo modo e come si inseriscono nel contesto più ampio.
La compilazione di un template in Latte, semplificata, include questi passaggi chiave:
- Analisi lessicale: Il lexer legge il codice sorgente del template (file
.latte
) e lo divide in una sequenza di piccole parti distinte chiamate token (ad es.{
,foreach
,$variable
,}
, testo HTML, ecc.). - Parsing: Il parser prende questo flusso di token e costruisce da esso una struttura ad albero significativa che rappresenta la logica e il contenuto del template. Questo albero è chiamato Abstract Syntax Tree (AST).
- Passaggi di compilazione: Prima di generare il codice PHP, Latte esegue i passaggi di compilazione. Si tratta di funzioni che attraversano l'intero AST e possono modificarlo o raccogliere informazioni. Questo passaggio è fondamentale per funzionalità come la sicurezza (Sandbox) o l'ottimizzazione.
- Generazione del codice: Infine, il compilatore attraversa l'AST (potenzialmente modificato) e genera il codice PHP corrispondente per la classe. Questo codice PHP è ciò che effettivamente renderizza il template durante l'esecuzione.
- Caching: Il codice PHP generato viene salvato su disco, rendendo i rendering successivi molto veloci, poiché i passaggi 1–4 vengono saltati.
In realtà, la compilazione è un po' più complessa. Latte ha due lexer e parser: uno per il template HTML e un altro per il codice PHP-like all'interno dei tag. Inoltre, il parsing non avviene dopo la tokenizzazione, ma lexer e parser vengono eseguiti parallelamente in due “thread” e si coordinano. Credetemi, programmarlo è stata scienza missilistica :-)
L'intero processo, dal caricamento del contenuto del template, attraverso il parsing, fino alla generazione del file risultante, può essere sequenziato con questo codice, con cui è possibile sperimentare e stampare i risultati intermedi:
$latte = new Latte\Engine;
$source = $latte->getLoader()->getContent($file);
$ast = $latte->parse($source);
$latte->applyPasses($ast);
$code = $latte->generate($ast, $file);
Anatomia di un tag
La creazione di un tag personalizzato completamente funzionale in Latte coinvolge diverse parti interconnesse. Prima di immergerci nell'implementazione, comprendiamo i concetti di base e la terminologia, utilizzando un'analogia con HTML e il Document Object Model (DOM).
Tag vs. Nodi (Analogia con HTML)
In HTML, scriviamo tag come <p>
o <div>...</div>
. Questi tag sono la
sintassi nel codice sorgente. Quando il browser analizza questo HTML, crea una rappresentazione in memoria chiamata Document
Object Model (DOM). Nel DOM, i tag HTML sono rappresentati da nodi (specificamente nodi Element
nella
terminologia del DOM JavaScript). Lavoriamo programmaticamente con questi nodi (ad esempio, usando
document.getElementById(...)
di JavaScript si ottiene un nodo Element). Un tag è solo una rappresentazione testuale
nel file sorgente; un nodo è una rappresentazione orientata agli oggetti nell'albero logico.
Latte funziona in modo simile:
- Nel file di template
.latte
, scrivi tag Latte, come{foreach ...}
e{/foreach}
. Questa è la sintassi con cui tu, come autore del template, lavori. - Quando Latte analizza il template, costruisce un Abstract Syntax Tree (AST). Questo albero è composto da nodi. Ogni tag Latte, elemento HTML, pezzo di testo o espressione nel template diventa uno o più nodi in questo albero.
- La classe base per tutti i nodi nell'AST è
Latte\Compiler\Node
. Proprio come il DOM ha diversi tipi di nodi (Element, Text, Comment), l'AST di Latte ha diversi tipi di nodi. IncontreraiLatte\Compiler\Nodes\TextNode
per il testo statico,Latte\Compiler\Nodes\Html\ElementNode
per gli elementi HTML,Latte\Compiler\Nodes\Php\ExpressionNode
per le espressioni all'interno dei tag e, fondamentale per i tag personalizzati, nodi che ereditano daLatte\Compiler\Nodes\StatementNode
.
Perché StatementNode
?
Gli elementi HTML (Html\ElementNode
) rappresentano principalmente struttura e contenuto. Le espressioni PHP
(Php\ExpressionNode
) rappresentano valori o calcoli. Ma che dire dei tag Latte come {if}
,
{foreach}
o il nostro {datetime}
personalizzato? Questi tag eseguono azioni, controllano il
flusso del programma o generano output basato sulla logica. Sono unità funzionali che rendono Latte un potente engine di
template, non solo un linguaggio di markup.
Nella programmazione, tali unità che eseguono azioni sono spesso chiamate “statements” (istruzioni). Pertanto, i nodi che
rappresentano questi tag Latte funzionali ereditano tipicamente 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).
Componenti chiave
Esaminiamo i componenti principali necessari per creare un tag personalizzato:
Funzione di parsing del tag
- Questa funzione PHP callable analizza la sintassi del tag Latte (
{...}
) nel template sorgente. - Riceve informazioni sul tag (come il suo nome, posizione e se si tratta di un n:attribute) tramite l'oggetto Latte\Compiler\Tag.
- Il suo strumento principale per analizzare gli argomenti e le espressioni all'interno dei delimitatori del tag è l'oggetto Latte\Compiler\TagParser, accessibile tramite
$tag->parser
(questo è un parser diverso da quello che analizza l'intero template). - Per i tag accoppiati, utilizza
yield
per segnalare a Latte di analizzare il contenuto interno tra il tag di apertura e quello di chiusura. - L'obiettivo finale della funzione di parsing è creare e restituire un'istanza della classe del nodo, che viene aggiunta all'AST.
- È consuetudine (anche se non obbligatorio) implementare la funzione di parsing come metodo statico (spesso chiamato
create
) direttamente nella classe del nodo corrispondente. Ciò mantiene la logica di parsing e la rappresentazione del nodo ordinatamente in un unico pacchetto, consente l'accesso agli elementi privati/protetti della classe, se necessario, e migliora l'organizzazione.
Classe del nodo
- Rappresenta la funzione logica del tuo tag nell'Abstract Syntax Tree (AST).
- Contiene le informazioni analizzate (come argomenti o contenuto) come proprietà pubbliche. Queste proprietà spesso
contengono altre istanze di
Node
(ad es.ExpressionNode
per gli argomenti analizzati,AreaNode
per il contenuto analizzato). - Il metodo
print(PrintContext $context): string
genera il codice PHP (un'istruzione o una serie di istruzioni) che esegue l'azione del tag durante il rendering del template. - Il metodo
getIterator(): \Generator
rende accessibili i nodi figli (argomenti, contenuto) per l'attraversamento da parte dei passaggi di compilazione. Deve fornire riferimenti (&
) per consentire ai passaggi di modificare o sostituire potenzialmente i sottonodi. - Dopo che l'intero template è stato analizzato nell'AST, Latte esegue una serie di passaggi di compilazione. Questi passaggi attraversano l'intero AST
utilizzando il metodo
getIterator()
fornito da ciascun nodo. Possono ispezionare i nodi, raccogliere informazioni e persino modificare l'albero (ad es. cambiando le proprietà pubbliche dei nodi o sostituendo completamente i nodi). Questo design, che richiede ungetIterator()
completo, è cruciale. Consente a potenti funzionalità come Sandbox di analizzare e potenzialmente modificare il comportamento di qualsiasi parte del template, inclusi i tuoi tag personalizzati, garantendo sicurezza e coerenza.
Registrazione tramite un'estensione
- Devi informare Latte del tuo nuovo tag e quale funzione di parsing deve essere utilizzata per esso. Questo avviene all'interno di un'estensione Latte.
- All'interno della tua classe di estensione, implementi il metodo
getTags(): array
. Questo metodo restituisce un array associativo in cui le chiavi sono i nomi dei tag (ad es.'mytag'
,'n:myattribute'
) e i valori sono le funzioni PHP callable che rappresentano le rispettive funzioni di parsing (ad es.MyNamespace\DatetimeNode::create(...)
).
Riepilogo: La funzione di parsing del tag trasforma il codice sorgente del template del tuo tag in un nodo
AST. La classe del nodo può quindi trasformare se stessa in codice PHP eseguibile per il template
compilato e rende accessibili i suoi sottonodi per i passaggi di compilazione tramite getIterator()
. La
registrazione tramite estensione collega il nome del tag alla funzione di parsing e lo rende noto a Latte.
Ora esploreremo come implementare questi componenti passo dopo passo.
Creazione di un tag semplice
Immergiamoci nella creazione del tuo primo tag Latte personalizzato. Inizieremo con un esempio molto semplice: un tag chiamato
{datetime}
che stampa la data e l'ora correnti. Inizialmente, questo tag non accetterà alcun argomento, ma lo
miglioreremo più avanti nella sezione “Parsing degli argomenti del tag”. Non ha
nemmeno alcun contenuto interno.
Questo esempio ti guiderà attraverso i passaggi fondamentali: definire la classe del nodo, implementare i suoi metodi
print()
e getIterator()
, creare la funzione di parsing e infine registrare il tag.
Obiettivo: Implementare {datetime}
per stampare la data e l'ora correnti utilizzando la funzione PHP
date()
.
Creazione della classe del nodo
Innanzitutto, abbiamo bisogno di una classe che rappresenti il nostro tag nell'Abstract Syntax Tree (AST). Come discusso sopra,
ereditiamo da Latte\Compiler\Nodes\StatementNode
.
Crea un file (ad es. DatetimeNode.php
) e definisci la classe:
<?php
namespace App\Latte;
use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\PrintContext;
use Latte\Compiler\Tag;
class DatetimeNode extends StatementNode
{
/**
* Funzione di parsing del tag, chiamata quando viene trovato {datetime}.
*/
public static function create(Tag $tag): self
{
// Il nostro tag semplice attualmente 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,
);
}
/**
* Fornisce accesso ai nodi figli per i passaggi di compilazione di Latte.
*/
public function &getIterator(): \Generator
{
false && yield;
}
}
Quando Latte incontra {datetime}
in un template, chiama la funzione di parsing create()
. Il suo
compito è restituire un'istanza di DatetimeNode
.
Il metodo print()
genera il codice PHP che verrà eseguito durante il rendering del template. Chiamiamo il metodo
$context->format()
, che costruisce la stringa finale del codice PHP per il template compilato. Il primo argomento,
'echo date('Y-m-d H:i:s') %line;'
, è una maschera in cui vengono inseriti i parametri successivi. Il segnaposto
%line
dice al metodo format()
di utilizzare il secondo argomento, che è
$this->position
, e inserire un commento come /* line 15 */
, che collega il codice PHP generato alla
riga originale del template, il che è fondamentale per il debugging.
La proprietà $this->position
è ereditata dalla classe base Node
ed è impostata automaticamente
dal parser di Latte. Contiene un oggetto Latte\Compiler\Position che indica dove è stato
trovato il tag nel file sorgente .latte
.
Il metodo getIterator()
è fondamentale per i passaggi di compilazione. Deve fornire tutti i nodi figli, ma il
nostro semplice DatetimeNode
attualmente non ha argomenti né contenuto, quindi nessun nodo figlio. Tuttavia, il
metodo deve comunque esistere ed essere un generatore, cioè la parola chiave yield
deve essere presente in qualche
modo nel corpo del metodo.
Registrazione tramite un'estensione
Infine, informiamo Latte del nuovo tag. Crea una classe di estensione (ad es.
MyLatteExtension.php
) e registra il tag nel suo metodo getTags()
.
<?php
namespace App\Latte;
use Latte\Extension;
class MyLatteExtension extends Extension
{
/**
* Restituisce l'elenco dei tag forniti da questa estensione.
* @return array<string, callable> Mappa: 'nome-tag' => funzione-parsing
*/
public function getTags(): array
{
return [
'datetime' => DatetimeNode::create(...),
// Registra altri tag qui più tardi
];
}
}
Quindi, registra questa estensione nell'Engine di Latte:
$latte = new Latte\Engine;
$latte->addExtension(new App\Latte\MyLatteExtension);
Crea un 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}
. Abbiamo definito la sua rappresentazione
nell'AST (DatetimeNode
), gestito il suo parsing (create()
), specificato come dovrebbe generare codice
PHP (print()
), assicurato che i suoi figli siano accessibili per l'attraversamento (getIterator()
) e lo
abbiamo registrato in Latte.
Nella prossima sezione, miglioreremo questo tag per accettare argomenti e mostreremo come analizzare le espressioni e gestire i nodi figli.
Parsing degli argomenti del tag
Il nostro semplice tag {datetime}
funziona, ma non è molto flessibile. Miglioriamolo per accettare un argomento
opzionale: una stringa di formato per la funzione date()
. La sintassi richiesta sarà
{datetime $format}
.
Obiettivo: Modificare {datetime}
in modo che accetti un'espressione PHP opzionale come argomento, che
verrà utilizzata come stringa di formato per date()
.
Introduzione a TagParser
Prima di modificare il codice, è importante comprendere lo strumento che useremo: Latte\Compiler\TagParser. Quando il parser principale
di Latte (TemplateParser
) incontra un tag Latte come {datetime ...}
o un n:attribute, delega il parsing
del contenuto all'interno del tag (la parte tra {
e }
o il valore dell'attributo) a un
TagParser
specializzato.
Questo TagParser
lavora esclusivamente con gli argomenti del tag. Il suo compito è elaborare i token che
rappresentano questi argomenti. È fondamentale che elabori l'intero contenuto che gli viene fornito. Se la tua funzione di
parsing termina, ma TagParser
non ha raggiunto la fine degli argomenti (controllato tramite
$tag->parser->isEnd()
), Latte lancerà un'eccezione, poiché ciò indica che all'interno del tag sono rimasti
token imprevisti. Al contrario, se il tag richiede argomenti, dovresti chiamare $tag->expectArguments()
all'inizio della tua funzione di parsing. Questo metodo controlla se gli argomenti sono presenti e lancia un'eccezione utile se il
tag è stato utilizzato senza alcun argomento.
TagParser
offre metodi utili per analizzare diversi tipi di argomenti:
parseExpression(): ExpressionNode
: Analizza un'espressione PHP-like (variabili, letterali, operatori, chiamate di funzioni/metodi, ecc.). Gestisce lo zucchero sintattico di Latte, come trattare semplici stringhe alfanumeriche come stringhe tra virgolette (ad es.foo
viene analizzato come se fosse'foo'
).parseUnquotedStringOrExpression(): ExpressionNode
: Analizza o un'espressione standard o una stringa non tra virgolette. Le stringhe non tra virgolette sono sequenze consentite da Latte senza virgolette, spesso utilizzate per cose come percorsi di file (ad es.{include ../file.latte}
). Se analizza una stringa non tra virgolette, restituisceStringNode
.parseArguments(): ArrayNode
: Analizza argomenti separati da virgole, potenzialmente con chiavi, come10, name: 'John', true
.parseModifier(): ModifierNode
: Analizza filtri come|upper|truncate:10
.parseType(): ?SuperiorTypeNode
: Analizza type hint PHP comeint
,?string
,array|Foo
.
Per esigenze di parsing più complesse o di basso livello, puoi interagire direttamente con il flusso di token tramite
$tag->parser->stream
. Questo oggetto fornisce metodi per controllare ed elaborare singoli token:
$tag->parser->stream->is(...): bool
: Controlla se il token corrente corrisponde a uno dei tipi specificati (ad es.Token::Php_Variable
) o valori letterali (ad es.'as'
) senza consumarlo. Utile per guardare avanti.$tag->parser->stream->consume(...): Token
: Consuma il token corrente e sposta la posizione del flusso in avanti. Se vengono forniti tipi/valori di token attesi come argomenti e il token corrente non corrisponde, lanciaCompileException
. Usalo quando ti aspetti un certo token.$tag->parser->stream->tryConsume(...): ?Token
: Tenta di consumare il token corrente solo se corrisponde a uno dei tipi/valori specificati. Se corrisponde, consuma il token e lo restituisce. Se non corrisponde, lascia invariata la posizione del flusso e restituiscenull
. Usalo per token opzionali o quando scegli tra diversi percorsi sintattici.
Aggiornamento della funzione di parsing create()
Con questa comprensione, modifichiamo il metodo create()
in DatetimeNode
per analizzare l'argomento
di formato opzionale usando $tag->parser
.
<?php
namespace App\Latte;
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
{
// Aggiungiamo una proprietà pubblica per conservare il nodo dell'espressione del formato analizzato
public ?ExpressionNode $format = null;
public static function create(Tag $tag): self
{
$node = $tag->node = new self;
// Controlliamo se ci sono token
if (!$tag->parser->isEnd()) {
// Analizziamo l'argomento come un'espressione PHP-like usando TagParser.
$node->format = $tag->parser->parseExpression();
}
return $node;
}
// ... i metodi print() e getIterator() saranno aggiornati più avanti ...
}
Abbiamo aggiunto una proprietà pubblica $format
. In create()
, ora usiamo
$tag->parser->isEnd()
per controllare se esistono argomenti. Se sì,
$tag->parser->parseExpression()
elabora i token per l'espressione. Poiché TagParser
deve
elaborare tutti i token di input, Latte lancerà automaticamente un errore se l'utente scrive qualcosa di inaspettato dopo
l'espressione del formato (ad es. {datetime 'Y-m-d', unexpected}
).
Aggiornamento del metodo print()
Ora modifichiamo il metodo print()
per utilizzare l'espressione del formato analizzata memorizzata in
$this->format
. Se non è stato fornito alcun formato ($this->format
è null
),
dovremmo usare una stringa di formato predefinita, ad 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 del codice PHP di $formatNode.
return $context->format(
'echo date(%node) %line;',
$formatNode,
$this->position
);
}
Nella variabile $formatNode
memorizziamo il nodo AST che rappresenta la stringa di formato per la funzione PHP
date()
. Usiamo qui l'operatore di coalescenza nullo (??
). Se l'utente ha fornito un argomento nel
template (ad es. {datetime 'd.m.Y'}
), allora la proprietà $this->format
contiene il nodo
corrispondente (in questo caso StringNode
con il valore 'd.m.Y'
), e questo nodo viene utilizzato. Se
l'utente non ha fornito un 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'
. Ciò
garantisce che $formatNode
contenga sempre un nodo AST valido per il formato.
Nella maschera 'echo date(%node) %line;'
viene utilizzato un nuovo segnaposto %node
, che dice al
metodo format()
di prendere il primo argomento successivo (che è il nostro $formatNode
), chiamare il
suo metodo print()
(che restituirà la sua rappresentazione del codice PHP) e inserire il risultato nella posizione
del segnaposto.
Implementazione di getIterator()
per i sottonodi
Il nostro DatetimeNode
ora ha un nodo figlio: l'espressione $format
. Dobbiamo rendere questo
nodo figlio accessibile ai passaggi di compilazione fornendolo nel metodo getIterator()
. Ricorda di fornire un
riferimento (&
) per consentire ai passaggi di sostituire potenzialmente il nodo.
public function &getIterator(): \Generator
{
if ($this->format) {
yield $this->format;
}
}
Perché è fondamentale? Immagina un passaggio Sandbox che deve controllare se l'argomento $format
non contiene
una chiamata a una funzione vietata (ad es. {datetime dangerousFunction()}
). Se getIterator()
non
fornisce $this->format
, il passaggio Sandbox non vedrebbe mai la chiamata dangerousFunction()
all'interno dell'argomento del nostro tag, creando una potenziale falla di sicurezza. Fornendolo, consentiamo a Sandbox (e ad
altri passaggi) di controllare e potenzialmente modificare il nodo dell'espressione $format
.
Utilizzo del tag migliorato
Il tag ora gestisce correttamente l'argomento opzionale:
Formato predefinito: {datetime}
Formato personalizzato: {datetime 'd.m.Y'}
Utilizzo di una variabile: {datetime $userDateFormatPreference}
{* Questo causerebbe un errore dopo il parsing di 'd.m.Y', perché ", foo" è inaspettato *}
{* {datetime 'd.m.Y', foo} *}
Successivamente, esamineremo la creazione di tag accoppiati che elaborano il contenuto tra di loro.
Gestione dei tag accoppiati
Finora, il nostro tag {datetime}
era auto-chiudente (concettualmente). Non aveva alcun contenuto tra il tag
di apertura e quello di chiusura. Tuttavia, molti tag utili lavorano con un blocco di contenuto del template. Questi sono chiamati
tag accoppiati. Esempi includono {if}...{/if}
, {block}...{/block}
o un tag personalizzato che
creeremo ora: {debug}...{/debug}
.
Questo tag ci consentirà di includere informazioni di debug nei nostri template, che dovrebbero essere visibili solo durante lo sviluppo.
Obiettivo: Creare un tag accoppiato {debug}
, il cui contenuto viene renderizzato solo quando è attivo uno
specifico flag “modalità di sviluppo”.
Introduzione ai provider
A volte i tuoi tag necessitano di accedere a dati o servizi che non vengono passati direttamente come parametri del template. Ad esempio, determinare se l'applicazione è in modalità di sviluppo, accedere all'oggetto utente o ottenere valori di configurazione. Latte fornisce un meccanismo chiamato provider per questo scopo.
I provider vengono registrati nella tua estensione utilizzando 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 effettivi.
All'interno del codice PHP generato dal metodo print()
del tuo tag, puoi accedere a questi provider tramite una
proprietà speciale dell'oggetto $this->global
. Poiché questa proprietà è condivisa tra tutte le estensioni, è
buona pratica prefissare i nomi dei tuoi provider per evitare potenziali conflitti di nomi con i provider principali di
Latte o provider di altre estensioni di terze parti. Una convenzione comune è utilizzare un prefisso breve e univoco correlato
al tuo produttore o al nome dell'estensione. Per il nostro esempio, useremo il prefisso app
e il flag della
modalità di sviluppo sarà disponibile come $this->global->appDevMode
.
La parola chiave yield
per il parsing del contenuto
Come diciamo al parser di Latte di elaborare il contenuto tra {debug}
e {/debug}
? Qui entra in
gioco la parola chiave yield
.
Quando yield
viene utilizzato nella funzione create()
, la funzione diventa un generatore PHP. La sua esecuzione viene sospesa e il
controllo ritorna al TemplateParser
principale. TemplateParser
continua quindi ad analizzare il
contenuto del template fino a quando non incontra il tag di chiusura corrispondente ({/debug}
nel
nostro caso).
Una volta trovato il tag di chiusura, TemplateParser
riprende l'esecuzione della nostra funzione
create()
subito dopo l'istruzione yield
. Il valore restituito dall'istruzione yield
è un array contenente due elementi:
- Un
AreaNode
che rappresenta il contenuto analizzato tra il tag di apertura e quello di chiusura. - Un oggetto
Tag
che rappresenta il tag di chiusura (ad es.{/debug}
).
Creiamo la classe DebugNode
e il suo metodo create
utilizzando yield
.
<?php
namespace App\Latte;
use Latte\Compiler\Nodes\AreaNode;
use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\PrintContext;
use Latte\Compiler\Tag;
class DebugNode extends StatementNode
{
// Proprietà pubblica per conservare il contenuto interno analizzato
public AreaNode $content;
/**
* Funzione di parsing per il tag accoppiato {debug} ... {/debug}.
*/
public static function create(Tag $tag): \Generator // nota il tipo di ritorno
{
$node = $tag->node = new self;
// Sospendere il parsing, ottenere il contenuto interno e il tag finale quando viene trovato {/debug}
[$node->content, $endTag] = yield;
return $node;
}
// ... print() e getIterator() saranno implementati più avanti ...
}
Nota: $endTag
è null
se il tag viene utilizzato come n:attribute, cioè
<div n:debug>...</div>
.
Implementazione di print()
per il rendering
condizionale
Il metodo print()
ora deve generare codice PHP che, a runtime, controlli il provider appDevMode
ed
esegua il codice per il contenuto interno solo se il flag è true.
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 contenente l'AST del contenuto interno
);
}
Questo è semplice. Usiamo PrintContext::format()
per creare un'istruzione PHP if
standard.
All'interno dell'if
, posizioniamo il segnaposto %node
per $this->content
. Latte
chiamerà ricorsivamente $this->content->print($context)
per generare il codice PHP per la parte interna del
tag, ma solo se $this->global->appDevMode
viene valutato come true a runtime.
Implementazione di getIterator()
per il contenuto
Proprio come con il nodo dell'argomento nell'esempio precedente, il nostro DebugNode
ora ha un nodo figlio:
AreaNode $content
. Dobbiamo renderlo accessibile fornendolo in getIterator()
:
public function &getIterator(): \Generator
{
// Fornisce un riferimento al nodo del contenuto
yield $this->content;
}
Ciò consente ai passaggi di compilazione di scendere nel contenuto del nostro tag {debug}
, il che è importante
anche se il contenuto viene renderizzato condizionalmente. Ad esempio, Sandbox deve analizzare il contenuto indipendentemente dal
fatto che appDevMode
sia true o false.
Registrazione e utilizzo
Registra il tag e il provider nella tua estensione:
class MyLatteExtension extends Extension
{
// Supponiamo che $isDevelopmentMode sia determinato da qualche parte (ad es. dalla configurazione)
public function __construct(
private bool $isDevelopmentMode,
) {
}
public function getTags(): array
{
return [
'datetime' => DatetimeNode::create(...),
'debug' => DebugNode::create(...), // Registrazione del nuovo tag
];
}
public function getProviders(): array
{
return [
'appDevMode' => $this->isDevelopmentMode, // Registrazione del provider
];
}
}
// Durante la registrazione dell'estensione:
$isDev = true; // Determina questo in base all'ambiente della tua applicazione
$latte->addExtension(new App\Latte\MyLatteExtension($isDev));
E il suo utilizzo nel template:
<p>Contenuto normale visibile sempre.</p>
{debug}
<div class="debug-panel">
ID utente corrente: {$user->id}
Ora della richiesta: {=time()}
</div>
{/debug}
<p>Altro contenuto normale.</p>
Integrazione di n:attributes
Latte offre una comoda notazione abbreviata per molti tag accoppiati: n:attributes. Se hai un tag accoppiato come
{tag}...{/tag}
e desideri che il suo effetto si applichi direttamente a un singolo elemento HTML, puoi spesso
scriverlo in modo più conciso come attributo n:tag
su quell'elemento.
Per la maggior parte dei tag accoppiati standard che definisci (come il nostro {debug}
), Latte abiliterà
automaticamente la versione dell'attributo n:
corrispondente. Non devi fare nulla di extra durante la
registrazione:
{* Uso standard del tag accoppiato *}
{debug}<div>Informazioni per il debug</div>{/debug}
{* Uso equivalente con n:attribute *}
<div n:debug>Informazioni per il debug</div>
Entrambe le versioni renderizzeranno il <div>
solo se $this->global->appDevMode
è true.
Anche i prefissi inner-
e tag-
funzionano come previsto.
A volte la logica del tuo tag potrebbe dover comportarsi leggermente diversamente a seconda che venga utilizzato come tag
accoppiato standard o come n:attribute, o se viene utilizzato un prefisso come n:inner-tag
o
n:tag-tag
. L'oggetto Latte\Compiler\Tag
, passato alla tua funzione di parsing create()
,
fornisce queste informazioni:
$tag->isNAttribute(): bool
: Restituiscetrue
se il tag viene analizzato come n:attribute$tag->prefix: ?string
: Restituisce il prefisso utilizzato con l'n:attribute, che può esserenull
(non è un n:attribute),Tag::PrefixNone
,Tag::PrefixInner
oTag::PrefixTag
Ora che comprendiamo i tag semplici, il parsing degli argomenti, i tag accoppiati, i provider e gli n:attributes,
affrontiamo uno scenario più complesso che coinvolge tag nidificati all'interno di altri tag, utilizzando il nostro tag
{debug}
come punto di partenza.
Tag intermedi
Alcuni tag accoppiati consentono o addirittura richiedono che altri tag appaiano al loro interno prima del tag di
chiusura finale. Questi sono chiamati tag intermedi. Esempi classici includono
{if}...{elseif}...{else}...{/if}
o {switch}...{case}...{default}...{/switch}
.
Estendiamo il nostro tag {debug}
per supportare una clausola {else}
opzionale, che verrà
renderizzata quando l'applicazione non è in modalità di sviluppo.
Obiettivo: Modificare {debug}
per supportare un tag intermedio opzionale {else}
. La sintassi
finale dovrebbe essere {debug} ... {else} ... {/debug}
.
Parsing dei tag intermedi con yield
Sappiamo già che yield
sospende la funzione di parsing create()
e restituisce il contenuto
analizzato insieme al tag finale. Tuttavia, yield
offre un maggiore controllo: puoi fornirgli un array di nomi di
tag intermedi. Quando il parser incontra uno qualsiasi di questi tag specificati allo stesso livello di nidificazione
(cioè come figli diretti del tag genitore, non all'interno di altri blocchi o tag al suo interno), interrompe anche il
parsing.
Quando il parsing si interrompe a causa di un tag intermedio, interrompe il parsing del contenuto, riprende il generatore
create()
e passa indietro il contenuto parzialmente analizzato e il tag intermedio stesso (invece del tag
finale di chiusura). La nostra funzione create()
può quindi elaborare questo tag intermedio (ad es. analizzare
i suoi argomenti, se ne avesse) e utilizzare nuovamente yield
per analizzare la prossima parte del contenuto
fino al tag finale finale o a un altro tag intermedio atteso.
Modifichiamo DebugNode::create()
per aspettarsi {else}
:
<?php
namespace App\Latte;
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 per la parte {debug}
public AreaNode $thenContent;
// Contenuto opzionale per la parte {else}
public ?AreaNode $elseContent = null;
public static function create(Tag $tag): \Generator
{
$node = $tag->node = new self;
// yield e aspettarsi o {/debug} o {else}
[$node->thenContent, $nextTag] = yield ['else'];
// Controllare se il tag in 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() saranno aggiornati più avanti ...
}
Ora yield ['else']
dice a Latte di interrompere il parsing non solo per {/debug}
, ma anche per
{else}
. Se viene trovato {else}
, $nextTag
conterrà l'oggetto Tag
per
{else}
. Quindi usiamo di nuovo yield
senza argomenti, il che significa che ora ci aspettiamo solo il tag
finale {/debug}
, e memorizziamo il risultato in $node->elseContent
. Se {else}
non è
stato trovato, $nextTag
sarebbe il Tag
per {/debug}
(o null
se usato come
n:attribute) e $node->elseContent
rimarrebbe null
.
Implementazione di print()
con {else}
Il metodo print()
deve riflettere la nuova struttura. Dovrebbe generare un'istruzione PHP if/else
basata sul provider devMode
.
public function print(PrintContext $context): string
{
return $context->format(
<<<'XX'
if ($this->global->appDevMode) %line {
%node // Codice per il ramo 'then' (contenuto {debug})
} else {
%node // Codice per il ramo 'else' (contenuto {else})
}
XX,
$this->position, // Numero di riga per la condizione 'if'
$this->thenContent, // Primo segnaposto %node
$this->elseContent ?? new NopNode, // Secondo segnaposto %node
);
}
Questa è una struttura PHP if/else
standard. Usiamo %node
due volte; format()
sostituisce i nodi forniti in sequenza. Usiamo ?? new NopNode
per evitare errori se
$this->elseContent
è null
– NopNode
semplicemente non stampa nulla.
Implementazione di getIterator()
per entrambi
i contenuti
Ora abbiamo potenzialmente due nodi figli di contenuto ($thenContent
e $elseContent
). Dobbiamo
fornire entrambi, se esistono:
public function &getIterator(): \Generator
{
yield $this->thenContent;
if ($this->elseContent) {
yield $this->elseContent;
}
}
Utilizzo del tag migliorato
Il tag può ora essere utilizzato con la clausola {else}
opzionale:
{debug}
<p>Visualizzazione delle informazioni di debug perché devMode è ON.</p>
{else}
<p>Le informazioni di debug sono nascoste perché devMode è OFF.</p>
{/debug}
Gestione dello stato e della nidificazione
I nostri esempi precedenti ({datetime}
, {debug}
) erano relativamente senza stato all'interno dei
loro metodi print()
. O stampavano direttamente il contenuto o eseguivano un semplice controllo condizionale basato
su un provider globale. Tuttavia, molti tag devono gestire una qualche forma di stato durante il rendering o comportano la
valutazione di espressioni utente che dovrebbero essere eseguite solo una volta per motivi di prestazioni o correttezza. Inoltre,
dobbiamo considerare cosa succede quando i nostri tag personalizzati sono nidificati.
Illustriamo questi concetti creando un tag {repeat $count}...{/repeat}
. Questo tag ripeterà il suo contenuto
interno $count
volte.
Obiettivo: Implementare {repeat $count}
, che ripete il suo contenuto un numero specificato di volte.
La necessità di variabili temporanee e univoche
Immagina che l'utente scriva:
{repeat rand(1, 5)} Contenuto {/repeat}
Se generassimo ingenuamente un ciclo for
PHP in questo modo nel nostro metodo print()
:
// Codice generato semplificato e ERRATO
for ($i = 0; $i < rand(1, 5); $i++) {
// stampa contenuto
}
Questo sarebbe sbagliato! L'espressione rand(1, 5)
verrebbe rivalutata ad ogni iterazione del ciclo,
portando a un numero imprevedibile di ripetizioni. Dobbiamo valutare l'espressione $count
una volta prima
dell'inizio del ciclo e memorizzare il suo risultato.
Genereremo codice PHP che prima valuta l'espressione del conteggio e la memorizza in una variabile temporanea di
runtime. Per evitare collisioni con le variabili definite dall'utente del template e le variabili interne di Latte
(come $ʟ_...
), useremo la convenzione di prefissare le nostre variabili temporanee con $__
(doppio
trattino basso).
Il codice generato apparirebbe quindi così:
$__count = rand(1, 5);
for ($__i = 0; $__i < $__count; $__i++) {
// stampa contenuto
}
Ora consideriamo la nidificazione:
{repeat $countA} {* Ciclo esterno *}
{repeat $countB} {* Ciclo interno *}
...
{/repeat}
{/repeat}
Se sia il tag {repeat}
esterno che quello interno generassero codice utilizzando gli stessi nomi di
variabili temporanee (ad es. $__count
e $__i
), il ciclo interno sovrascriverebbe le variabili del ciclo
esterno, interrompendo la logica.
Dobbiamo garantire che le variabili temporanee generate per ogni istanza del tag {repeat}
siano univoche.
Raggiungiamo questo obiettivo utilizzando PrintContext::generateId()
. Questo metodo restituisce un intero univoco
durante la fase di compilazione. Possiamo aggiungere questo ID ai nomi delle nostre variabili temporanee.
Quindi, invece di $__count
, genereremo $__count_1
per il primo tag repeat, $__count_2
per il secondo, e così via. Allo stesso modo, per il contatore del ciclo, useremo $__i_1
,
$__i_2
, ecc.
Implementazione di RepeatNode
Creiamo la classe del nodo.
<?php
namespace App\Latte;
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 parsing per {repeat $count} ... {/repeat}
*/
public static function create(Tag $tag): \Generator
{
$tag->expectArguments(); // assicura che $count sia fornito
$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 un ciclo PHP 'for' con nomi di variabili univoci.
*/
public function print(PrintContext $context): string
{
// Generazione di nomi di variabili univoci
$id = $context->generateId();
$countVar = '$__count_' . $id; // es. $__count_1, $__count_2, ecc.
$iteratorVar = '$__i_' . $id; // es. $__i_1, $__i_2, ecc.
return $context->format(
<<<'XX'
// Valutazione dell'espressione del conteggio *una volta* e memorizzazione
%raw = (int) (%node);
// Ciclo utilizzando il conteggio memorizzato e una variabile di iterazione univoca
for (%raw = 0; %2.raw < %0.raw; %2.raw++) %line {
%node // Rendering del contenuto interno
}
XX,
$countVar, // %0 - Variabile per memorizzare il conteggio
$this->count, // %1 - Nodo dell'espressione per il conteggio
$iteratorVar, // %2 - Nome della variabile di iterazione del ciclo
$this->position, // %3 - Commento con numero di riga per il ciclo stesso
$this->content // %4 - Nodo del contenuto interno
);
}
/**
* Fornisce i nodi figli (espressione del conteggio e contenuto).
*/
public function &getIterator(): \Generator
{
yield $this->count;
yield $this->content;
}
}
Il metodo create()
analizza l'espressione $count
richiesta usando parseExpression()
.
Prima viene chiamato $tag->expectArguments()
. Ciò garantisce che l'utente abbia fornito qualcosa dopo
{repeat}
. Mentre $tag->parser->parseExpression()
fallirebbe se non venisse fornito nulla, il
messaggio di errore potrebbe riguardare una sintassi imprevista. L'uso di expectArguments()
fornisce un errore molto
più chiaro, affermando specificamente che mancano argomenti per il tag {repeat}
.
Il metodo print()
genera il codice PHP responsabile dell'esecuzione della logica di ripetizione a runtime. Inizia
generando nomi univoci per le variabili PHP temporanee di cui avrà bisogno.
Il metodo $context->format()
viene chiamato con un nuovo segnaposto %raw
, che inserisce la
stringa grezza fornita come argomento corrispondente. Qui inserisce il nome univoco della variabile memorizzato in
$countVar
(ad es. $__count_1
). E che dire di %0.raw
e %2.raw
? Questo dimostra
i segnaposto posizionali. Invece di usare semplicemente %raw
, che prende il prossimo argomento grezzo
disponibile, %2.raw
prende esplicitamente l'argomento all'indice 2 (che è $iteratorVar
) e inserisce il
suo valore stringa grezzo. Ciò ci consente di riutilizzare la stringa $iteratorVar
senza passarla più volte
nell'elenco degli argomenti per format()
.
Questa chiamata format()
attentamente costruita genera un ciclo PHP efficiente e sicuro che gestisce correttamente
l'espressione del conteggio ed evita collisioni di nomi di variabili anche quando i tag {repeat}
sono
nidificati.
Registrazione e utilizzo
Registra il tag nella tua estensione:
use App\Latte\RepeatNode;
class MyLatteExtension extends Extension
{
public function getTags(): array
{
return [
'datetime' => DatetimeNode::create(...),
'debug' => DebugNode::create(...),
'repeat' => RepeatNode::create(...), // Registrazione del tag repeat
];
}
}
Usalo nel template, inclusa la nidificazione:
{var $rows = rand(5, 7)}
{var $cols = rand(3, 5)}
{repeat $rows}
<tr>
{repeat $cols}
<td>Ciclo interno</td>
{/repeat}
</tr>
{/repeat}
Questo esempio dimostra come gestire lo stato (contatori di cicli) e potenziali problemi di nidificazione utilizzando variabili
temporanee con prefisso $__
e univoche con ID da PrintContext::generateId()
.
n:attributes puri
Mentre molti n:attributes
come n:if
o n:foreach
fungono da comode scorciatoie per le
loro controparti nei tag accoppiati ({if}...{/if}
, {foreach}...{/foreach}
), Latte consente anche di
definire tag che esistono solo sotto forma di n:attribute. Questi vengono spesso utilizzati per modificare gli attributi
o il comportamento dell'elemento HTML a cui sono collegati.
Esempi standard integrati in Latte includono n:class
,
che aiuta a costruire dinamicamente l'attributo class
, e n:attr
, che può impostare più attributi arbitrari.
Creiamo il nostro n:attribute puro: n:confirm
, che aggiunge una finestra di dialogo di conferma JavaScript prima
di eseguire un'azione (come seguire un link o inviare un form).
Obiettivo: Implementare n:confirm="'Sei sicuro?'"
, che aggiunge un gestore onclick
per
prevenire l'azione predefinita se l'utente annulla la finestra di dialogo di conferma.
Implementazione di ConfirmNode
Abbiamo bisogno di una classe Node e di una funzione di parsing.
<?php
namespace App\Latte;
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
{
// Assicura l'escaping corretto per i contesti JavaScript e attributo HTML.
return $context->format(
<<<'XX'
echo ' onclick="', LR\Filters::escapeHtmlAttr('return confirm(' . LR\Filters::escapeJs(%node) . ')'), '"' %line;
XX,
$this->message,
$this->position,
);
}
public function &getIterator(): \Generator
{
yield $this->message;
}
}
Il metodo print()
genera codice PHP che alla fine, durante il rendering del template, stamperà l'attributo HTML
onclick="..."
. La gestione dei contesti nidificati (JavaScript all'interno di un attributo HTML) richiede un attento
escaping. Il filtro LR\Filters::escapeJs(%node)
viene chiamato a runtime ed esegue l'escape del messaggio
correttamente per l'uso all'interno di JavaScript (l'output sarebbe come "Sure?"
). Successivamente, il filtro
LR\Filters::escapeHtmlAttr(...)
esegue l'escape dei caratteri speciali negli attributi HTML, quindi cambierebbe
l'output in return confirm("Sure?")
. Questo escaping a due fasi a runtime garantisce che il
messaggio sia sicuro per JavaScript e che il codice JavaScript risultante sia sicuro per l'inserimento nell'attributo HTML
onclick
.
Registrazione e utilizzo
Registra l'n:attribute nella tua estensione. Non dimenticare 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(...), // Registrazione di n:confirm
];
}
}
Ora puoi usare n:confirm
su link, pulsanti o elementi del form:
<a href="delete.php?id=123" n:confirm='"Vuoi davvero eliminare l\'elemento {$id}?"'>Elimina</a>
HTML generato:
<a href="delete.php?id=123" onclick="return confirm("Vuoi davvero eliminare l'elemento 123?")">Elimina</a>
Quando l'utente fa clic sul link, il browser esegue il codice onclick
, visualizza la finestra di dialogo di
conferma e passa a delete.php
solo se l'utente fa clic su “OK”.
Questo esempio dimostra come è possibile creare un n:attribute puro per modificare il comportamento o gli attributi del suo
elemento HTML host generando il codice PHP appropriato nel suo metodo print()
. Non dimenticare il doppio escaping,
che è spesso richiesto: una volta per il contesto di destinazione (JavaScript in questo caso) e di nuovo per il contesto
dell'attributo HTML.
Argomenti avanzati
Mentre le sezioni precedenti coprono i concetti fondamentali, ecco alcuni argomenti più avanzati che potresti incontrare durante la creazione di tag Latte personalizzati.
Modalità di output dei tag
L'oggetto Tag
passato alla tua funzione create()
ha una proprietà outputMode
. Questa
proprietà influenza il modo in cui Latte gestisce gli spazi bianchi e l'indentazione circostanti, specialmente quando il tag
viene utilizzato su una riga propria. Puoi modificare questa proprietà nella tua funzione create()
.
Tag::OutputKeepIndentation
(Predefinito per la maggior parte dei tag come{=...}
): Latte cerca di preservare l'indentazione prima del tag. Le nuove righe dopo il tag vengono generalmente preservate. Questo è adatto per i tag che stampano contenuto in linea.Tag::OutputRemoveIndentation
(Predefinito per i tag di blocco come{if}
,{foreach}
): Latte rimuove l'indentazione iniziale e potenzialmente una nuova riga successiva. Questo aiuta a mantenere il codice PHP generato più pulito e previene ulteriori righe vuote nell'output HTML causate dal tag stesso. Usalo per i tag che rappresentano strutture di controllo o blocchi che non dovrebbero aggiungere spazi bianchi da soli.Tag::OutputNone
(Utilizzato da tag come{var}
,{default}
): Simile aRemoveIndentation
, ma segnala più fortemente che il tag stesso non produce output diretto, influenzando potenzialmente l'elaborazione degli spazi bianchi intorno ad esso in modo ancora più aggressivo. Adatto per tag dichiarativi o di impostazione.
Scegli la modalità che meglio si adatta allo scopo del tuo tag. Per la maggior parte dei tag strutturali o di controllo,
OutputRemoveIndentation
è solitamente appropriato.
Accesso ai tag genitore/più vicini
A volte il comportamento di un tag deve dipendere dal contesto in cui viene utilizzato, specificamente in quale tag
genitore(i) si trova. L'oggetto Tag
passato alla tua funzione create()
fornisce il metodo
closestTag(array $classes, ?callable $condition = null): ?Tag
esattamente per questo scopo.
Questo metodo cerca verso l'alto nella gerarchia dei tag attualmente aperti (inclusi gli elementi HTML rappresentati
internamente durante il parsing) e restituisce l'oggetto Tag
dell'antenato più vicino che corrisponde a criteri
specifici. Se non viene trovato alcun antenato corrispondente, restituisce null
.
L'array $classes
specifica quale tipo di tag antenato stai cercando. Controlla se il nodo associato del tag
antenato ($ancestorTag->node
) è un'istanza di questa classe.
function create(Tag $tag)
{
// Ricerca del tag antenato più vicino il cui nodo è un'istanza di ForeachNode
$foreachTag = $tag->closestTag([ForeachNode::class]);
if ($foreachTag) {
// Possiamo accedere all'istanza ForeachNode stessa:
$foreachNode = $foreachTag->node;
}
}
Nota $foreachTag->node
: Funziona solo perché è una convenzione nello sviluppo dei tag Latte assegnare
immediatamente il nodo creato a $tag->node
all'interno del metodo create()
, come abbiamo
sempre fatto.
A volte il semplice confronto del tipo di nodo non è sufficiente. Potrebbe essere necessario controllare una proprietà
specifica del potenziale tag antenato o del suo nodo. Il secondo argomento opzionale per closestTag()
è un callable
che riceve il potenziale oggetto Tag
antenato e dovrebbe 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(),
);
}
L'uso di closestTag()
consente di creare tag consapevoli del contesto e di imporre un uso corretto all'interno
della struttura del tuo template, portando a template più robusti e comprensibili.
Segnaposto di PrintContext::format()
Abbiamo spesso usato PrintContext::format()
per generare codice PHP nei metodi print()
dei nostri
nodi. Accetta una stringa di maschera e argomenti successivi che sostituiscono i segnaposto nella maschera. Ecco un riepilogo dei
segnaposto disponibili:
%node
: L'argomento deve essere un'istanza diNode
. Chiama il metodoprint()
del nodo e inserisce la stringa di codice PHP risultante.%dump
: L'argomento è un qualsiasi valore PHP. Esporta il valore in codice PHP valido. Adatto per 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. Usare con cautela, principalmente per inserire frammenti di codice PHP pregenerati o nomi di variabili.$context->format('%raw = 1;', '$variableName')
→$variableName = 1;
%args
: L'argomento deve essereExpression\ArrayNode
. Stampa gli elementi dell'array formattati come argomenti per una chiamata di funzione o metodo (separati da virgole, gestisce argomenti nominati se presenti).$argsNode = new ArrayNode([...]);
$context->format('myFunc(%args);', $argsNode)
→myFunc(1, name: 'Joe');
%line
: L'argomento deve essere un oggettoPosition
(solitamente$this->position
). Inserisce un commento PHP/* line X */
che indica il numero di riga della sorgente.$context->format('echo "Hi" %line;', $this->position)
→echo "Hi" /* line 42 */;
%escape(...)
: Genera codice PHP che a runtime esegue l'escape dell'espressione interna utilizzando le attuali regole di escaping consapevoli del contesto.$context->format('echo %escape(%node);', $variableNode)
%modify(...)
: L'argomento deve essereModifierNode
. Genera codice PHP che applica i filtri specificati nelModifierNode
al contenuto interno, incluso l'escaping consapevole del contesto se non disabilitato con|noescape
.$context->format('%modify(%node);', $modifierNode, $variableNode)
%modifyContent(...)
: Simile a%modify
, ma destinato alla modifica di blocchi di contenuto catturato (spesso HTML).
Puoi fare riferimento esplicitamente agli argomenti tramite il loro indice (a partire da zero): %0.node
,
%1.dump
, %2.raw
, ecc. Ciò consente 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 parsing complesso degli argomenti
Mentre parseExpression()
, parseArguments()
, ecc., coprono molti casi, a volte è necessaria una
logica di parsing più complessa utilizzando il TokenStream
di livello inferiore disponibile tramite
$tag->parser->stream
.
Obiettivo: Creare un tag {embedYoutube $videoID, width: 640, height: 480}
. Vogliamo analizzare l'ID video
richiesto (stringa o variabile) seguito da coppie chiave-valore opzionali per le dimensioni.
<?php
namespace App\Latte;
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;
// Analisi dell'ID video richiesto
$node->videoId = $tag->parser->parseExpression();
// Analisi delle coppie chiave-valore opzionali
$stream = $tag->parser->stream; // Ottenimento del flusso di token
while ($stream->tryConsume(',')) { // Richiede la separazione con virgola
// Attesa dell'identificatore 'width' o 'height'
$keyToken = $stream->consume(Token::Php_Identifier);
$key = strtolower($keyToken->text);
$stream->consume(':'); // Attesa del separatore due punti
$value = $tag->parser->parseExpression(); // Analisi dell'espressione del valore
if ($key === 'width') {
$node->width = $value;
} elseif ($key === 'height') {
$node->height = $value;
} else {
throw new CompileException("Argomento sconosciuto '$key'. Atteso 'width' o 'height'.", $keyToken->position);
}
}
return $node;
}
}
Questo livello di controllo consente di definire sintassi molto specifiche e complesse per i tuoi tag personalizzati interagendo direttamente con il flusso di token.
Utilizzo di AuxiliaryNode
Latte fornisce nodi “ausiliari” generici per situazioni speciali durante la generazione del codice o all'interno dei
passaggi di compilazione. Sono AuxiliaryNode
e Php\Expression\AuxiliaryNode
.
Considera AuxiliaryNode
come un nodo contenitore flessibile che delega le sue funzionalità principali –
generazione del codice ed esposizione dei nodi figli – agli argomenti forniti nel suo costruttore:
- Delega di
print()
: Il primo argomento del costruttore è una closure PHP. Quando Latte chiama il metodoprint()
suAuxiliaryNode
, esegue questa closure fornita. La closure ricevePrintContext
e qualsiasi nodo passato nel secondo argomento del costruttore, consentendoti di definire una logica di generazione del codice PHP completamente personalizzata a runtime. - Delega di
getIterator()
: Il secondo argomento del costruttore è un array di oggettiNode
. Quando Latte deve attraversare i figli diAuxiliaryNode
(ad es. durante i passaggi di compilazione), il suo metodogetIterator()
fornisce semplicemente i nodi elencati 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 sono forniti dal metodo getIterator() e passati alla closure sopra
[$argumentNode1, $argumentNode2]
);
Latte fornisce due tipi distinti basati su dove è necessario inserire il codice generato:
Latte\Compiler\Nodes\Php\Expression\AuxiliaryNode
: Usalo quando devi generare un pezzo di codice PHP che rappresenta un'espressioneLatte\Compiler\Nodes\AuxiliaryNode
: Usalo per scopi più generali, quando devi inserire un blocco di codice PHP che rappresenta una o più istruzioni
Un motivo importante per utilizzare AuxiliaryNode
invece dei nodi standard (come
StaticMethodCallNode
) all'interno del tuo metodo print()
o del passaggio di compilazione è il
controllo della visibilità per i passaggi di compilazione successivi, in particolare quelli relativi alla sicurezza come
Sandbox.
Considera uno scenario: il tuo passaggio di compilazione deve avvolgere un'espressione fornita dall'utente
($userExpr
) con una chiamata a una funzione ausiliaria specifica e affidabile
myInternalSanitize($userExpr)
. Se crei un nodo standard
new FunctionCallNode('myInternalSanitize', [$userExpr])
, sarà completamente visibile per l'attraversamento dell'AST.
Se il passaggio Sandbox viene eseguito successivamente e myInternalSanitize
non è nella sua lista consentita,
Sandbox potrebbe bloccare o modificare questa chiamata, potenzialmente interrompendo la logica interna del tuo tag, anche
se tu, l'autore del tag, sai che questa specifica chiamata è sicura e necessaria. Puoi quindi generare la chiamata
direttamente all'interno della closure di AuxiliaryNode
.
use Latte\Compiler\Nodes\Php\Expression\AuxiliaryNode;
// ... all'interno di print() o di un passaggio di compilazione ...
$wrappedNode = new AuxiliaryNode(
fn(PrintContext $context, $userExpr) => $context->format(
'myInternalSanitize(%node)', // Generazione diretta del codice PHP
$userExpr,
),
// IMPORTANTE: Passa comunque il nodo originale dell'espressione utente qui!
[$userExpr],
);
In questo caso, il passaggio Sandbox vede AuxiliaryNode
, ma non analizza il codice PHP generato dalla sua
closure. Non può bloccare direttamente la chiamata myInternalSanitize
generata all'interno della
closure.
Mentre il codice PHP generato stesso è nascosto ai passaggi, gli input a questo codice (nodi che rappresentano dati
o espressioni utente) devono essere ancora attraversabili. Pertanto, il secondo argomento del costruttore di
AuxiliaryNode
è cruciale. Devi passare un array contenente tutti i nodi originali (come
$userExpr
nell'esempio sopra) che la tua closure utilizza. getIterator()
di AuxiliaryNode
fornirà questi nodi, consentendo ai passaggi di compilazione come Sandbox di analizzarli per potenziali problemi.
Best practice
- Scopo chiaro: Assicurati che il tuo tag abbia uno scopo chiaro e necessario. Non creare tag per compiti che possono essere facilmente risolti con filtri o funzioni.
- Implementa correttamente
getIterator()
: Implementa sempregetIterator()
e fornisci riferimenti (&
) a tutti i nodi figli (argomenti, contenuto) che sono stati analizzati dal template. Questo è essenziale per i passaggi di compilazione, la sicurezza (Sandbox) e potenziali ottimizzazioni future. - Proprietà pubbliche per i nodi: Rendi pubbliche le proprietà contenenti nodi figli, in modo che i passaggi di compilazione possano modificarle se necessario.
- Usa
PrintContext::format()
: Sfrutta il metodoformat()
per generare codice PHP. Gestisce le virgolette, esegue correttamente l'escape dei segnaposto e aggiunge automaticamente commenti con il numero di riga. - Variabili temporanee (
$__
): Quando generi codice PHP di runtime che necessita di variabili temporanee (ad es. per memorizzare subtotali, contatori di cicli), usa la convenzione del prefisso$__
per evitare collisioni con le variabili utente e le variabili interne di Latte$ʟ_
. - Nidificazione e ID univoci: Se il tuo tag può essere nidificato o necessita di uno stato specifico dell'istanza a
runtime, usa
$context->generateId()
all'interno del tuo metodoprint()
per creare suffissi univoci per le tue variabili temporanee$__
. - Provider per dati esterni: Usa i provider (registrati tramite
Extension::getProviders()
) per accedere a dati o servizi di runtime ($this->global->…) invece di hardcodare valori o fare affidamento sullo stato globale. Usa prefissi del produttore per i nomi dei provider. - Considera gli n:attributes: Se il tuo tag accoppiato opera logicamente su un singolo elemento HTML, Latte probabilmente
fornisce supporto automatico per
n:attribute
. Tienilo a mente per la comodità dell'utente. Se stai creando un tag che modifica un attributo, considera se unn:attribute
puro è la forma più appropriata. - Test: Scrivi test per i tuoi tag, coprendo sia il parsing di diversi input sintattici sia la correttezza dell'output del codice PHP generato.
Seguendo queste linee guida, puoi creare tag personalizzati potenti, robusti e manutenibili che si integrano perfettamente con l'engine di template Latte.
Studiare le classi dei nodi che fanno parte di Latte è il modo migliore per imparare tutti i dettagli del processo di parsing.