Creare un'estensione
Un'estensione è una classe riutilizzabile che può definire tag personalizzati, filtri, funzioni, provider, ecc.
Creiamo estensioni quando vogliamo riutilizzare le nostre personalizzazioni di Latte in progetti diversi o condividerle con altri. È anche utile creare un'estensione per ogni progetto web, che conterrà tutti i tag e i filtri specifici che si vogliono usare nei modelli di progetto.
Classe di estensione
L'estensione è una classe che eredita da Latte\Extension. Viene registrata con Latte usando
addExtension()
(o tramite il file di
configurazione):
$latte = new Latte\Engine;
$latte->addExtension(new MyLatteExtension);
Se si registrano più estensioni e queste definiscono tag, filtri o funzioni con nomi identici, vince l'ultima estensione aggiunta. Questo implica anche che le estensioni possono sovrascrivere tag/filtri/funzioni nativi.
Ogni volta che si apporta una modifica a una classe e l'aggiornamento automatico non è disattivato, Latte ricompila automaticamente i modelli.
Una classe può implementare uno dei seguenti metodi:
abstract class Extension
{
/**
* Initializes before template is compiler.
*/
public function beforeCompile(Engine $engine): void;
/**
* Returns a list of parsers for Latte tags.
* @return array<string, callable>
*/
public function getTags(): array;
/**
* Returns a list of compiler passes.
* @return array<string, callable>
*/
public function getPasses(): array;
/**
* Returns a list of |filters.
* @return array<string, callable>
*/
public function getFilters(): array;
/**
* Returns a list of functions used in templates.
* @return array<string, callable>
*/
public function getFunctions(): array;
/**
* Returns a list of providers.
* @return array<mixed>
*/
public function getProviders(): array;
/**
* Returns a value to distinguish multiple versions of the template.
*/
public function getCacheKey(Engine $engine): mixed;
/**
* Initializes before template is rendered.
*/
public function beforeRender(Template $template): void;
}
Per avere un'idea dell'aspetto dell'estensione, dare un'occhiata al built-in CoreExtension.
beforeCompile (Latte\Engine $engine): void
Richiamato prima della compilazione del template. Il metodo può essere usato per le inizializzazioni legate alla compilazione, ad esempio.
getTags(): array
Richiamato quando il template viene compilato. Restituisce un array associativo nome tag ⇒ callable, che sono funzioni di parsing dei tag.
public function getTags(): array
{
return [
'foo' => [FooNode::class, 'create'],
'bar' => [BarNode::class, 'create'],
'n:baz' => [NBazNode::class, 'create'],
// ...
];
}
Il tag n:baz
rappresenta un attributo n:puro, cioè un tag che può essere scritto solo come attributo.
Nel caso dei tag foo
e bar
, Latte riconosce automaticamente se si tratta di coppie e, in tal caso,
può scriverli automaticamente usando n:attributes, comprese le varianti con i prefissi n:inner-foo
e
n:tag-foo
.
L'ordine di esecuzione di tali n:attributi è determinato dal loro ordine nell'array restituito da getTags()
.
Pertanto, n:foo
viene sempre eseguito prima di n:bar
, anche se gli attributi sono elencati in ordine
inverso nel tag HTML come <div n:bar="..." n:foo="...">
.
Se è necessario determinare l'ordine di n:attributi tra più estensioni, si può usare il metodo di aiuto
order()
, dove il parametro before
xor after
determina quali tag sono ordinati prima o dopo
il tag.
public function getTags(): array
{
return [
'foo' => self::order([FooNode::class, 'create'], before: 'bar')]
'bar' => self::order([BarNode::class, 'create'], after: ['block', 'snippet'])]
];
}
getPasses(): array
Viene richiamato quando il template viene compilato. Restituisce un array associativo nome-pass ⇒ callable, che sono funzioni che rappresentano i cosiddetti passaggi del compilatore che attraversano e modificano l'AST.
Anche in questo caso, si può usare il metodo helper order()
. Il valore dei parametri before
o
after
può essere *
con il significato di prima/dopo tutto.
public function getPasses(): array
{
return [
'optimize' => [Passes::class, 'optimizePass'],
'sandbox' => self::order([$this, 'sandboxPass'], before: '*'),
// ...
];
}
beforeRender (Latte\Engine $engine): void
Viene richiamato prima di ogni rendering del template. Il metodo può essere usato, ad esempio, per inizializzare le variabili utilizzate durante il rendering.
getFilters(): array
Viene richiamato prima che il template sia reso. Restituisce i filtri come array associativo nome filtro ⇒ callable.
public function getFilters(): array
{
return [
'batch' => [$this, 'batchFilter'],
'trim' => [$this, 'trimFilter'],
// ...
];
}
getFunctions(): array
Viene richiamato prima che il modello sia reso. Restituisce le funzioni come array associativo nome funzione ⇒ callable.
public function getFunctions(): array
{
return [
'clamp' => [$this, 'clampFunction'],
'divisibleBy' => [$this, 'divisibleByFunction'],
// ...
];
}
getProviders(): array
Viene richiamato prima che il template sia reso. Restituisce un array di fornitori, che di solito sono oggetti che usano i tag
in fase di esecuzione. Vi si accede tramite $this->global->...
.
public function getProviders(): array
{
return [
'myFoo' => $this->foo,
'myBar' => $this->bar,
// ...
];
}
getCacheKey (Latte\Engine $engine): mixed
Viene richiamato prima che il template venga reso. Il valore di ritorno diventa parte della chiave il cui hash è contenuto nel nome del file del template compilato. Pertanto, per valori di ritorno diversi, Latte genererà file di cache diversi.
Come funziona Latte?
Per capire come definire tag personalizzati o passaggi del compilatore, è essenziale capire come funziona Latte sotto il cofano.
La compilazione dei template in Latte funziona semplicisticamente in questo modo:
- Per prima cosa, il lexer tokenizza il codice sorgente del template in piccoli pezzi (tokens) per facilitarne l'elaborazione.
- Poi, il parser converte il flusso di token in un albero di nodi significativo (l'Abstract Syntax Tree, AST).
- Infine, il compilatore genera una classe PHP dall'AST che rende il template e lo mette in cache.
In realtà, la compilazione è un po' più complicata. Latte ha due lexer e parser: uno per il modello HTML e uno per il codice PHP all'interno dei tag. Inoltre, il parsing non viene eseguito dopo la tokenizzazione, ma il lexer e il parser vengono eseguiti in parallelo in due “thread” e si coordinano. Si tratta di scienza missilistica :-)
Inoltre, tutti i tag hanno le proprie routine di parsing. Quando il parser incontra un tag, chiama la sua funzione di parsing (restituisce Extension::getTags()). Il suo compito è analizzare gli argomenti del tag e, nel caso di tag accoppiati, il contenuto interno. Restituisce un nodo che diventa parte dell'AST. Per maggiori dettagli, vedere la funzione di parsing dei tag.
Quando il parser finisce il suo lavoro, abbiamo un AST completo che rappresenta il template. Il nodo radice è
Latte\Compiler\Nodes\TemplateNode
. I singoli nodi all'interno dell'albero rappresentano non solo i tag, ma anche
gli elementi HTML, i loro attributi, le espressioni utilizzate all'interno dei tag, ecc.
A questo punto, entrano in gioco i cosiddetti Compiler pass, che sono funzioni (restituite da Extension::getPasses()) che modificano l'AST.
L'intero processo, dal caricamento del contenuto del modello, al parsing, fino alla generazione del file risultante, può essere messo in sequenza con questo codice, che si può 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);
Esempio di AST
Per avere un'idea più precisa dell'AST, aggiungiamo un esempio. Questo è il modello sorgente:
{foreach $category->getItems() as $item}
<li>{$item->name|upper}</li>
{else}
no items found
{/foreach}
E questa è la sua rappresentazione sotto forma di AST:
Latte\Compiler\Nodes\TemplateNode( Latte\Compiler\Nodes\FragmentNode( - Latte\Essential\Nodes\ForeachNode( expression: Latte\Compiler\Nodes\Php\Expression\MethodCallNode( object: Latte\Compiler\Nodes\Php\Expression\VariableNode('$category') name: Latte\Compiler\Nodes\Php\IdentifierNode('getItems') ) value: Latte\Compiler\Nodes\Php\Expression\VariableNode('$item') content: Latte\Compiler\Nodes\FragmentNode( - Latte\Compiler\Nodes\TextNode(' ') - Latte\Compiler\Nodes\Html\ElementNode('li')( content: Latte\Essential\Nodes\PrintNode( expression: Latte\Compiler\Nodes\Php\Expression\PropertyFetchNode( object: Latte\Compiler\Nodes\Php\Expression\VariableNode('$item') name: Latte\Compiler\Nodes\Php\IdentifierNode('name') ) modifier: Latte\Compiler\Nodes\Php\ModifierNode( filters: - Latte\Compiler\Nodes\Php\FilterNode('upper') ) ) ) ) else: Latte\Compiler\Nodes\FragmentNode( - Latte\Compiler\Nodes\TextNode('no items found') ) ) ) )
Tag personalizzati
Per definire un nuovo tag sono necessari tre passaggi:
- definizione della funzione di parsing del tag (responsabile del parsing del tag in un nodo)
- creazione di una classe nodo (responsabile della generazione del codice PHP e dell'attraversamento dell'AST)
- registrare il tag usando Extension::getTags()
Funzione di parsing del tag
L'analisi dei tag è gestita dalla funzione di parsing (quella restituita da Extension::getTags()).
Il suo compito è quello di analizzare e controllare qualsiasi argomento all'interno del tag (utilizza TagParser per farlo).
Inoltre, se il tag è una coppia, chiederà a TemplateParser di analizzare e restituire il contenuto interno. La funzione crea e
restituisce un nodo, di solito figlio di Latte\Compiler\Nodes\StatementNode
, che diventa parte dell'AST.
Creiamo una classe per ogni nodo, cosa che faremo ora, e inseriamo elegantemente la funzione di parsing in essa come factory
statica. Come esempio, proviamo a creare il noto tag {foreach}
:
use Latte\Compiler\Nodes\StatementNode;
class ForeachNode extends StatementNode
{
// una funzione di parsing che per ora crea solo un nodo
public static function create(Latte\Compiler\Tag $tag): self
{
$node = $tag->node = new self;
return $node;
}
public function print(Latte\Compiler\PrintContext $context): string
{
// il codice sarà aggiunto in seguito
}
public function &getIterator(): \Generator
{
// il codice sarà aggiunto in seguito
}
}
Latte\Compiler\TagParser
$tag->parser
Alla funzione di parsing create()
viene passato un oggetto Latte\Compiler\Tag, che contiene informazioni di base sul
tag (se si tratta di un tag classico o di un n:attributo, su quale riga si trova, ecc.
Se il tag deve avere degli argomenti, si controlla la loro esistenza chiamando $tag->expectArguments()
.
I metodi dell'oggetto $tag->parser
sono disponibili per analizzarli:
parseExpression(): ExpressionNode
per un'espressione simile a quella di PHP (per esempio10 + 3
)parseUnquotedStringOrExpression(): ExpressionNode
per un'espressione o una stringa non quotataparseArguments(): ArrayNode
contenuto dell'array (ad es.10, true, foo => bar
)parseModifier(): ModifierNode
per un modificatore (ad es.|upper|truncate:10
)parseType(): expressionNode
per un suggerimento di tipo (ad es.int|string
oFoo\Bar[]
)
e un Latte\Compiler\TokenStream di basso livello che opera direttamente con i token:
$tag->parser->stream->consume(...): Token
$tag->parser->stream->tryConsume(...): ?Token
Latte estende la sintassi di PHP in piccoli modi, ad esempio aggiungendo modificatori, operatori ternari abbreviati
o permettendo di scrivere semplici stringhe alfanumeriche senza virgolette. Per questo motivo si usa il termine PHP-like
invece di PHP. Così, il metodo parseExpression()
analizza foo
come 'foo'
, per esempio.
Inoltre, stringa non quotata è un caso speciale di stringa che non ha bisogno di essere quotata, ma allo stesso tempo
non ha bisogno di essere alfanumerica. Ad esempio, è il percorso di un file nel tag {include ../file.latte}
. Per
analizzarla si usa il metodo parseUnquotedStringOrExpression()
.
Studiare le classi di nodi che fanno parte di Latte è il modo migliore per imparare tutti i dettagli del processo di parsing.
Torniamo al tag {foreach}
. In esso ci aspettiamo argomenti della forma
expression + 'as' + second expression
, che analizziamo come segue:
use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\Nodes\Php\ExpressionNode;
use Latte\Compiler\Nodes\AreaNode;
class ForeachNode extends StatementNode
{
public ExpressionNode $expression;
public ExpressionNode $value;
public static function create(Latte\Compiler\Tag $tag): self
{
$tag->expectArguments();
$node = $tag->node = new self;
$node->expression = $tag->parser->parseExpression();
$tag->parser->stream->consume('as');
$node->value = $parser->parseExpression();
return $node;
}
}
Le espressioni che abbiamo scritto nelle variabili $expression
e $value
rappresentano dei
sottonodi.
Definire le variabili con i sottonodi come pubbliche, in modo che possano essere modificate in ulteriori fasi di elaborazione, se necessario. È anche necessario renderle disponibili per l'attraversamento.
Per i tag accoppiati, come il nostro, il metodo deve anche permettere a TemplateParser di analizzare il contenuto interno del
tag. Questo viene gestito da yield
, che restituisce una coppia [contenuto interno, tag finale]. Il contenuto interno
viene memorizzato nella variabile $node->content
.
public AreaNode $content;
public static function create(Latte\Compiler\Tag $tag): \Generator
{
// ...
[$node->content, $endTag] = yield;
return $node;
}
La parola chiave yield
fa terminare il metodo create()
, restituendo il controllo al TemplateParser,
che continua ad analizzare il contenuto finché non raggiunge il tag finale. A questo punto, passa il controllo a
create()
, che continua da dove si era interrotto. L'uso del metodo yield
, restituisce automaticamente
Generator
.
Si può anche passare a yield
un array di nomi di tag per i quali si vuole interrompere l'analisi se si
verificano prima del tag finale. Questo ci aiuta a implementare il costrutto {foreach}...{else}...{/foreach}
. Se si
verifica {else}
, si analizza il contenuto dopo di esso in $node->elseContent
:
public AreaNode $content;
public ?AreaNode $elseContent = null;
public static function create(Latte\Compiler\Tag $tag): \Generator
{
// ...
[$node->content, $nextTag] = yield ['else'];
if ($nextTag?->name === 'else') {
[$node->elseContent] = yield;
}
return $node;
}
Il nodo di ritorno completa l'analisi dei tag.
Generazione del codice PHP
Ogni nodo deve implementare il metodo print()
. Restituisce il codice PHP che rende la parte data del template
(codice di runtime). Viene passato come parametro un oggetto Latte\Compiler\PrintContext, che ha un utile metodo
format()
che semplifica l'assemblaggio del codice risultante.
Il metodo format(string $mask, ...$args)
accetta i seguenti segnaposto nella maschera:
%node
stampa Nodo%dump
esporta il valore in PHP%raw
inserisce il testo direttamente senza alcuna trasformazione%args
stampa ArrayNode come argomenti della chiamata di funzione%line
stampa un commento con un numero di riga%escape(...)
esegue l'escape del contenuto%modify(...)
applica un modificatore%modifyContent(...)
applica un modificatore ai blocchi
La nostra funzione print()
potrebbe avere questo aspetto (trascuriamo il ramo else
per
semplicità):
public function print(Latte\Compiler\PrintContext $context): string
{
return $context->format(
<<<'XX'
foreach (%node as %node) %line {
%node
}
XX,
$this->expression,
$this->value,
$this->position,
$this->content,
);
}
La variabile $this->position
è già definita dalla classe Latte\Compiler\Node e viene impostata dal parser. Contiene
un oggetto Latte\Compiler\Position con la posizione
del tag nel codice sorgente sotto forma di numero di riga e di colonna.
Il codice di runtime può utilizzare variabili ausiliarie. Per evitare collisioni con le variabili usate dal template stesso,
è convenzione prefissarle con i caratteri $ʟ__
.
Può anche utilizzare valori arbitrari in fase di esecuzione, che vengono passati al template sotto forma di provider
utilizzando il metodo Extension::getProviders(). Si accede ad essi usando
$this->global->...
.
Attraversamento dell'AST
Per attraversare l'albero AST in profondità, è necessario implementare il metodo getIterator()
. Questo metodo
consente di accedere ai sottonodi:
public function &getIterator(): \Generator
{
yield $this->expression;
yield $this->value;
yield $this->content;
if ($this->elseContent) {
yield $this->elseContent;
}
}
Si noti che getIterator()
restituisce un riferimento. Questo è ciò che consente ai visitatori dei nodi di
sostituire i singoli nodi con altri nodi.
Se un nodo ha dei sottonodi, è necessario implementare questo metodo e rendere disponibili tutti i sottonodi. In caso contrario, si potrebbe creare una falla nella sicurezza. Ad esempio, la modalità sandbox non sarebbe in grado di controllare i sottonodi e di garantire che in essi non vengano richiamati costrutti non consentiti.
Poiché la parola chiave yield
deve essere presente nel corpo del metodo anche se non ha nodi figli, scriverlo
come segue:
public function &getIterator(): \Generator
{
if (false) {
yield;
}
}
Nodo ausiliario
Se si sta creando un nuovo tag per Latte, è consigliabile creare una classe di nodi dedicata, che lo rappresenti nell'albero
AST (si veda la classe ForeachNode
nell'esempio precedente). In alcuni casi, potrebbe essere utile la classe di nodi
ausiliari AuxiliaryNode,
che consente di passare il corpo del metodo print()
e l'elenco dei nodi resi accessibili dal metodo
getIterator()
come parametri del costruttore:
// Latte\Compiler\Nodes\Php\Expression\AuxiliaryNode
// or Latte\Compiler\Nodes\AuxiliaryNode
$node = new AuxiliaryNode(
// body of the print() method:
fn(PrintContext $context, $argNode) => $context->format('myFunc(%node)', $argNode),
// nodes accessed via getIterator() and also passed into the print() method:
[$argNode],
);
Il compilatore passa
I passi del compilatore sono funzioni che modificano gli AST o raccolgono informazioni in essi. Sono restituiti dal metodo Extension::getPasses().
Traverser di nodi
Il modo più comune di lavorare con l'AST è quello di utilizzare un Latte\Compiler\NodeTraverser:
use Latte\Compiler\Node;
use Latte\Compiler\NodeTraverser;
$ast = (new NodeTraverser)->traverse(
$ast,
enter: fn(Node $node) => ...,
leave: fn(Node $node) => ...,
);
La funzione enter (cioè visitatore) viene chiamata quando si incontra per la prima volta un nodo, prima che i suoi sottonodi vengano elaborati. La funzione leave viene chiamata dopo che tutti i sottonodi sono stati visitati. Uno schema comune è che enter viene usato per raccogliere alcune informazioni e poi leave esegue modifiche in base a queste. Nel momento in cui viene chiamata la funzione leave, tutto il codice all'interno del nodo sarà già stato visitato e saranno state raccolte le informazioni necessarie.
Come modificare l'AST? Il modo più semplice è cambiare semplicemente le proprietà dei nodi. Il secondo modo è quello di
sostituire interamente il nodo, restituendo un nuovo nodo. Esempio: il codice seguente cambierà tutti gli interi nell'AST in
stringhe (ad esempio, 42 sarà cambiato in '42'
).
use Latte\Compiler\Nodes\Php;
$ast = (new NodeTraverser)->traverse(
$ast,
leave: function (Node $node) {
if ($node instanceof Php\Scalar\IntegerNode) {
return new Php\Scalar\StringNode((string) $node->value);
}
},
);
Un AST può facilmente contenere migliaia di nodi, e la loro esplorazione può essere lenta. In alcuni casi, è possibile evitare una traversata completa.
Se si cercano tutti i Html\ElementNode
in un albero, si sa che una volta visto Php\ExpressionNode
,
non ha senso controllare anche tutti i suoi nodi figli, perché l'HTML non può essere contenuto nelle espressioni. In questo
caso, si può istruire il traverser a non ricorrervi all'interno del nodo della classe:
$ast = (new NodeTraverser)->traverse(
$ast,
enter: function (Node $node) {
if ($node instanceof Php\ExpressionNode) {
return NodeTraverser::DontTraverseChildren;
}
// ...
},
);
Se si cerca solo un nodo specifico, è anche possibile interrompere completamente l'attraversamento dopo averlo trovato.
$ast = (new NodeTraverser)->traverse(
$ast,
enter: function (Node $node) {
if ($node instanceof Nodes\ParametersNode) {
return NodeTraverser::StopTraversal;
}
// ...
},
);
Aiutanti dei nodi
La classe Latte\Compiler\NodeHelpers fornisce alcuni metodi che possono trovare nodi AST che soddisfano un certo callback, ecc. Vengono mostrati un paio di esempi:
use Latte\Compiler\NodeHelpers;
// trova tutti i nodi degli elementi HTML
$elements = NodeHelpers::find($ast, fn(Node $node) => $node instanceof Nodes\Html\ElementNode);
// Trova il primo nodo di testo
$text = NodeHelpers::findFirst($ast, fn(Node $node) => $node instanceof Nodes\TextNode);
// converte il nodo valore PHP in valore reale
$value = NodeHelpers::toValue($node);
// converte il nodo testuale statico in stringa
$text = NodeHelpers::toText($node);