Nette Documentation Preview

syntax
Creare tag personalizzati
*************************

.[perex]
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 |extending-latte#Modi per estendere Latte] oppure se un tag adatto esiste già nell'[insieme standard |tags]. 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:

1.  **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).
2.  **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)**.
3.  **Compiler pass:** prima di generare il codice PHP, Latte esegue i [compiler pass|compiler-passes]. Sono funzioni che attraversano l'intero AST e possono modificarlo o raccogliere informazioni. Questo passaggio è essenziale per funzionalità come la sicurezza ([Sandbox|sandbox]) o le ottimizzazioni.
4.  **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.
5.  **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:

```php
$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 `.latte` scrivete 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. Incontrerete `Latte\Compiler\Nodes\TextNode` per il testo statico, `Latte\Compiler\Nodes\Html\ElementNode` per gli elementi HTML, `Latte\Compiler\Nodes\Php\ExpressionNode` per le espressioni dentro i tag e, cosa fondamentale per i tag personalizzati, i nodi che ereditano da `Latte\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 [api:Latte\Compiler\Tag], e come secondo argomento il [api: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 [api:Latte\Compiler\TagParser], accessibile tramite `$tag->parser` (è un parser diverso da quello che analizza l'intero template).
- Per i tag di tipo pari usa `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 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 esempio `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 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|compiler-passes]. 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 un `getIterator()` completo, è essenziale: permette a funzionalità potenti come la [Sandbox|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 |extending-latte#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 esempio `MyNamespace\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
<?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 [api: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 |extending-latte#Estensione di Latte] (per esempio `MyLatteExtension.php`) e registrate il tag nel suo metodo `getTags()`.

```php
<?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:

```php
$latte = new Latte\Engine;
$latte->addExtension(new App\Templating\MyLatteExtension);
```

Create il template:

```latte
<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, [api: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 esempio `foo` viene 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 uno `StringNode`.
- `parseArguments(): ArrayNode`: analizza argomenti separati da virgole, eventualmente con chiavi, come `10, name: 'John', true`.
- `parseModifier(): ModifierNode`: analizza i filtri come `|upper|truncate:10`.
- `parseType(): ?SuperiorTypeNode`: analizza le dichiarazioni di tipo di PHP, come `int`, `?string`, `array|Foo`.

Per esigenze di analisi più complesse o di basso livello potete interagire direttamente con il [flusso di token|api:Latte\Compiler\TokenStream] 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 esempio `Token::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 una `CompileException`. 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 restituisce `null`. 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
<?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'`.

```php
	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.

```php
	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:

```latte
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 |extending-latte#Estensione di Latte] 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 |https://www.php.net/manual/en/language.generators.overview.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:

1.  Un `AreaNode` che rappresenta il contenuto analizzato tra il tag di apertura e quello di chiusura.
2.  L'oggetto `Tag` che rappresenta il tag di chiusura (per esempio `{/debug}`).

Creiamo la classe `DebugNode` e il suo metodo `create` usando `yield`.

```php
<?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.

```php
	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()`:

```php
	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:

```php
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:

```latte
<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 |syntax#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:

```latte
{* 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`: restituisce `true` se il tag viene analizzato come n:attributo
- `$tag->prefix: ?string`: restituisce il prefisso usato con l'n:attributo, che può essere `null` (non è un n:attributo), `Tag::PrefixNone`, `Tag::PrefixInner` oppure `Tag::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
<?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`.

```php
	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:

```php
	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:

```latte
{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:

```latte
{repeat rand(1, 5)} Contenuto {/repeat}
```

Se nel nostro metodo `print()` generassimo ingenuamente un ciclo PHP `for` come questo:

```php
// 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:

```php
$__count = rand(1, 5);
for ($__i = 0; $__i < $__count; $__i++) {
	// stampa il contenuto
}
```

Consideriamo ora l'annidamento:

```latte
{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
<?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:

```php
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:

```latte
{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` |tags#n:class], che aiuta a costruire dinamicamente l'attributo `class`, e [`n:attr` |tags#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
<?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(&quot;Sicuro?&quot;)`. 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:

```php
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:

```latte
<a href="delete.php?id=123" n:confirm='"Volete davvero eliminare l\'elemento {$id}?"'>Elimina</a>
```

HTML generato:

```latte
<a href="delete.php?id=123" onclick="return confirm(&quot;Volete davvero eliminare l'elemento 123?&quot;)">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 con `OutputRemoveIndentation`, 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.

```php
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.

```php
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 di `Node`. Ne chiama il metodo `print()` 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 un `Expression\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 oggetto `Position` (o `Range`), 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 un `ModifierNode`. Genera codice PHP che applica al contenuto interno i filtri indicati nel `ModifierNode`, 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
<?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 metodo `print()` di un `AuxiliaryNode`, esegue questa closure. La closure riceve il `PrintContext` e 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 oggetti `Node`**. Quando Latte deve attraversare i figli di un `AuxiliaryNode` (per esempio durante i compiler pass), il suo metodo `getIterator()` restituisce semplicemente i nodi contenuti in questo array.

Esempio:

```php
$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'**espressione**
- `Latte\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`.

```php
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|custom-filters] o con le [funzioni|custom-functions].
- **Implementate correttamente `getIterator()`:** implementate sempre `getIterator()` 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 metodo `format()` 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 metodo `print()` 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 un `n:attributo` puro 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.

.[note]
Studiare le classi dei nodi che fanno parte di Latte è il modo migliore per imparare tutti i dettagli più minuti del processo di analisi.

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:

  1. 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).
  2. 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).
  3. 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.
  4. 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.
  5. 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 .latte scrivete 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. Incontrerete Latte\Compiler\Nodes\TextNode per il testo statico, Latte\Compiler\Nodes\Html\ElementNode per gli elementi HTML, Latte\Compiler\Nodes\Php\ExpressionNode per le espressioni dentro i tag e, cosa fondamentale per i tag personalizzati, i nodi che ereditano da Latte\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 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 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 esempio 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 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 un getIterator() 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 esempio MyNamespace\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 esempio foo viene 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 uno StringNode.
  • parseArguments(): ArrayNode: analizza argomenti separati da virgole, eventualmente con chiavi, come 10, name: 'John', true.
  • parseModifier(): ModifierNode: analizza i filtri come |upper|truncate:10.
  • parseType(): ?SuperiorTypeNode: analizza le dichiarazioni di tipo di PHP, come int, ?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 esempio Token::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 una CompileException. 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 restituisce null. 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:

  1. Un AreaNode che rappresenta il contenuto analizzato tra il tag di apertura e quello di chiusura.
  2. L'oggetto Tag che 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: restituisce true se il tag viene analizzato come n:attributo
  • $tag->prefix: ?string: restituisce il prefisso usato con l'n:attributo, che può essere null (non è un n:attributo), Tag::PrefixNone, Tag::PrefixInner oppure Tag::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(&quot;Sicuro?&quot;). 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(&quot;Volete davvero eliminare l'elemento 123?&quot;)">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 con OutputRemoveIndentation, 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 di Node. Ne chiama il metodo print() 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 un Expression\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 oggetto Position (o Range), 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 un ModifierNode. Genera codice PHP che applica al contenuto interno i filtri indicati nel ModifierNode, 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 metodo print() di un AuxiliaryNode, esegue questa closure. La closure riceve il PrintContext e 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 oggetti Node. Quando Latte deve attraversare i figli di un AuxiliaryNode (per esempio durante i compiler pass), il suo metodo getIterator() 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'espressione
  • Latte\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 sempre getIterator() 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 metodo format() 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 metodo print() 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 un n:attributo puro 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.