Nette Documentation Preview

syntax
Creare un'estensione
********************

.[perex]
Un'estensione è una classe riutilizzabile che può definire tag personalizzati, filtri, funzioni, provider, ecc.

Creiamo estensioni quando vogliamo riutilizzare le nostre personalizzazioni di Latte in progetti diversi o condividerle con altri.
È anche utile creare un'estensione per ogni progetto web, che conterrà tutti i tag e i filtri specifici che si vogliono usare nei modelli di progetto.


Classe di estensione .[#toc-extension-class]
============================================

L'estensione è una classe che eredita da [api:Latte\Extension]. Viene registrata con Latte usando `addExtension()` (o tramite il [file di configurazione |application:configuration#Latte]):

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

Se si registrano più estensioni e queste definiscono tag, filtri o funzioni con nomi identici, vince l'ultima estensione aggiunta. Questo implica anche che le estensioni possono sovrascrivere tag/filtri/funzioni nativi.

Ogni volta che si apporta una modifica a una classe e l'aggiornamento automatico non è disattivato, Latte ricompila automaticamente i modelli.

Una classe può implementare uno dei seguenti metodi:

```php
abstract class Extension
{
	/**
	 * Initializes before template is compiler.
	 */
	public function beforeCompile(Engine $engine): void;

	/**
	 * Returns a list of parsers for Latte tags.
	 * @return array<string, callable>
	 */
	public function getTags(): array;

	/**
	 * Returns a list of compiler passes.
	 * @return array<string, callable>
	 */
	public function getPasses(): array;

	/**
	 * Returns a list of |filters.
	 * @return array<string, callable>
	 */
	public function getFilters(): array;

	/**
	 * Returns a list of functions used in templates.
	 * @return array<string, callable>
	 */
	public function getFunctions(): array;

	/**
	 * Returns a list of providers.
	 * @return array<mixed>
	 */
	public function getProviders(): array;

	/**
	 * Returns a value to distinguish multiple versions of the template.
	 */
	public function getCacheKey(Engine $engine): mixed;

	/**
	 * Initializes before template is rendered.
	 */
	public function beforeRender(Template $template): void;
}
```

Per avere un'idea dell'aspetto dell'estensione, dare un'occhiata al built-in "CoreExtension":https://github.com/nette/latte/blob/master/src/Latte/Essential/CoreExtension.php.


beforeCompile(Latte\Engine $engine): void .[method]
---------------------------------------------------

Richiamato prima della compilazione del template. Il metodo può essere usato per le inizializzazioni legate alla compilazione, ad esempio.


getTags(): array .[method]
--------------------------

Richiamato quando il template viene compilato. Restituisce un array associativo *nome tag => callable*, che sono [funzioni di parsing dei tag |#Tag Parsing Function].

```php
public function getTags(): array
{
	return [
		'foo' => [FooNode::class, 'create'],
		'bar' => [BarNode::class, 'create'],
		'n:baz' => [NBazNode::class, 'create'],
		// ...
	];
}
```

Il tag `n:baz` rappresenta un attributo n:puro, cioè un tag che può essere scritto solo come attributo.

Nel caso dei tag `foo` e `bar`, Latte riconosce automaticamente se si tratta di coppie e, in tal caso, può scriverli automaticamente usando n:attributes, comprese le varianti con i prefissi `n:inner-foo` e `n:tag-foo`.

L'ordine di esecuzione di tali n:attributi è determinato dal loro ordine nell'array restituito da `getTags()`. Pertanto, `n:foo` viene sempre eseguito prima di `n:bar`, anche se gli attributi sono elencati in ordine inverso nel tag HTML come `<div n:bar="..." n:foo="...">`.

Se è necessario determinare l'ordine di n:attributi tra più estensioni, si può usare il metodo di aiuto `order()`, dove il parametro `before` xor `after` determina quali tag sono ordinati prima o dopo il tag.

```php
public function getTags(): array
{
	return [
		'foo' => self::order([FooNode::class, 'create'], before: 'bar')]
		'bar' => self::order([BarNode::class, 'create'], after: ['block', 'snippet'])]
	];
}
```


getPasses(): array .[method]
----------------------------

Viene richiamato quando il template viene compilato. Restituisce un array associativo *nome-pass => callable*, che sono funzioni che rappresentano i cosiddetti [passaggi del compilatore |#compiler passes] che attraversano e modificano l'AST.

Anche in questo caso, si può usare il metodo helper `order()`. Il valore dei parametri `before` o `after` può essere `*` con il significato di prima/dopo tutto.

```php
public function getPasses(): array
{
	return [
		'optimize' => [Passes::class, 'optimizePass'],
		'sandbox' => self::order([$this, 'sandboxPass'], before: '*'),
		// ...
	];
}
```


beforeRender(Latte\Engine $engine): void .[method]
--------------------------------------------------

Viene richiamato prima di ogni rendering del template. Il metodo può essere usato, ad esempio, per inizializzare le variabili utilizzate durante il rendering.


getFilters(): array .[method]
-----------------------------

Viene richiamato prima che il template sia reso. Restituisce i [filtri |extending-latte#filters] come array associativo *nome filtro => callable*.

```php
public function getFilters(): array
{
	return [
		'batch' => [$this, 'batchFilter'],
		'trim' => [$this, 'trimFilter'],
		// ...
	];
}
```


getFunctions(): array .[method]
-------------------------------

Viene richiamato prima che il modello sia reso. Restituisce le [funzioni |extending-latte#functions] come array associativo *nome funzione => callable*.

```php
public function getFunctions(): array
{
	return [
		'clamp' => [$this, 'clampFunction'],
		'divisibleBy' => [$this, 'divisibleByFunction'],
		// ...
	];
}
```


getProviders(): array .[method]
-------------------------------

Viene richiamato prima che il template sia reso. Restituisce un array di fornitori, che di solito sono oggetti che usano i tag in fase di esecuzione. Vi si accede tramite `$this->global->...`.

```php
public function getProviders(): array
{
	return [
		'myFoo' => $this->foo,
		'myBar' => $this->bar,
		// ...
	];
}
```


getCacheKey(Latte\Engine $engine): mixed .[method]
--------------------------------------------------

Viene richiamato prima che il template venga reso. Il valore di ritorno diventa parte della chiave il cui hash è contenuto nel nome del file del template compilato. Pertanto, per valori di ritorno diversi, Latte genererà file di cache diversi.


Come funziona Latte? .[#toc-how-does-latte-work]
================================================

Per capire come definire tag personalizzati o passaggi del compilatore, è essenziale capire come funziona Latte sotto il cofano.

La compilazione dei template in Latte funziona semplicisticamente in questo modo:

- Per prima cosa, il **lexer** tokenizza il codice sorgente del template in piccoli pezzi (tokens) per facilitarne l'elaborazione.
- Poi, il **parser** converte il flusso di token in un albero di nodi significativo (l'Abstract Syntax Tree, AST).
- Infine, il compilatore **genera** una classe PHP dall'AST che rende il template e lo mette in cache.

In realtà, la compilazione è un po' più complicata. Latte **ha due** lexer e parser: uno per il modello HTML e uno per il codice PHP all'interno dei tag. Inoltre, il parsing non viene eseguito dopo la tokenizzazione, ma il lexer e il parser vengono eseguiti in parallelo in due "thread" e si coordinano. Si tratta di scienza missilistica :-)

Inoltre, tutti i tag hanno le proprie routine di parsing. Quando il parser incontra un tag, chiama la sua funzione di parsing (restituisce [Extension::getTags() |#getTags]).
Il suo compito è analizzare gli argomenti del tag e, nel caso di tag accoppiati, il contenuto interno. Restituisce un *nodo* che diventa parte dell'AST. Per maggiori dettagli, vedere la [funzione di parsing dei tag |#Tag parsing function].

Quando il parser finisce il suo lavoro, abbiamo un AST completo che rappresenta il template. Il nodo radice è `Latte\Compiler\Nodes\TemplateNode`. I singoli nodi all'interno dell'albero rappresentano non solo i tag, ma anche gli elementi HTML, i loro attributi, le espressioni utilizzate all'interno dei tag, ecc.

A questo punto, entrano in gioco i cosiddetti [Compiler pass |#Compiler passes], che sono funzioni (restituite da [Extension::getPasses() |#getPasses]) che modificano l'AST.

L'intero processo, dal caricamento del contenuto del modello, al parsing, fino alla generazione del file risultante, può essere messo in sequenza con questo codice, che si può sperimentare e scaricare i risultati intermedi:

```php
$latte = new Latte\Engine;
$source = $latte->getLoader()->getContent($file);
$ast = $latte->parse($source);
$latte->applyPasses($ast);
$code = $latte->generate($ast, $file);
```


Esempio di AST .[#toc-example-of-ast]
-------------------------------------

Per avere un'idea più precisa dell'AST, aggiungiamo un esempio. Questo è il modello sorgente:

```latte
{foreach $category->getItems() as $item}
	<li>{$item->name|upper}</li>
	{else}
	no items found
{/foreach}
```

E questa è la sua rappresentazione sotto forma di AST:

/--pre
Latte\Compiler\Nodes\<b>TemplateNode</b>(
   Latte\Compiler\Nodes\<b>FragmentNode</b>(
      - Latte\Essential\Nodes\<b>ForeachNode</b>(
           expression: Latte\Compiler\Nodes\Php\Expression\<b>MethodCallNode</b>(
              object: Latte\Compiler\Nodes\Php\Expression\<b>VariableNode</b>('$category')
              name: Latte\Compiler\Nodes\Php\<b>IdentifierNode</b>('getItems')
           )
           value: Latte\Compiler\Nodes\Php\Expression\<b>VariableNode</b>('$item')
           content: Latte\Compiler\Nodes\<b>FragmentNode</b>(
              - Latte\Compiler\Nodes\<b>TextNode</b>('  ')
              - Latte\Compiler\Nodes\<b>Html\ElementNode</b>('li')(
                   content: Latte\Essential\Nodes\<b>PrintNode</b>(
                      expression: Latte\Compiler\Nodes\Php\Expression\<b>PropertyFetchNode</b>(
                         object: Latte\Compiler\Nodes\Php\Expression\<b>VariableNode</b>('$item')
                         name: Latte\Compiler\Nodes\Php\<b>IdentifierNode</b>('name')
                      )
                      modifier: Latte\Compiler\Nodes\Php\<b>ModifierNode</b>(
                         filters:
                            - Latte\Compiler\Nodes\Php\<b>FilterNode</b>('upper')
                      )
                   )
                )
            )
            else: Latte\Compiler\Nodes\<b>FragmentNode</b>(
               - Latte\Compiler\Nodes\<b>TextNode</b>('no items found')
            )
        )
   )
)
\--


Tag personalizzati .[#toc-custom-tags]
======================================

Per definire un nuovo tag sono necessari tre passaggi:

- definizione della [funzione di parsing del tag |#tag parsing function] (responsabile del parsing del tag in un nodo)
- creazione di una classe nodo (responsabile della [generazione del codice PHP |#generating PHP code] e dell'[attraversamento dell'AST |#AST traversing])
- registrare il tag usando [Extension::getTags() |#getTags]


Funzione di parsing del tag .[#toc-tag-parsing-function]
--------------------------------------------------------

L'analisi dei tag è gestita dalla funzione di parsing (quella restituita da [Extension::getTags() |#getTags]). Il suo compito è quello di analizzare e controllare qualsiasi argomento all'interno del tag (utilizza TagParser per farlo).
Inoltre, se il tag è una coppia, chiederà a TemplateParser di analizzare e restituire il contenuto interno.
La funzione crea e restituisce un nodo, di solito figlio di `Latte\Compiler\Nodes\StatementNode`, che diventa parte dell'AST.

Creiamo una classe per ogni nodo, cosa che faremo ora, e inseriamo elegantemente la funzione di parsing in essa come factory statica. Come esempio, proviamo a creare il noto tag `{foreach}`:

```php
use Latte\Compiler\Nodes\StatementNode;

class ForeachNode extends StatementNode
{
	// una funzione di parsing che per ora crea solo un nodo
	public static function create(Latte\Compiler\Tag $tag): self
	{
		$node = $tag->node = new self;
		return $node;
	}

	public function print(Latte\Compiler\PrintContext $context): string
	{
		// il codice sarà aggiunto in seguito
	}

	public function &getIterator(): \Generator
	{
		// il codice sarà aggiunto in seguito
	}
}
```

[api:Latte\Compiler\TagParser] `$tag->parser`Alla funzione di parsing `create()` viene passato un oggetto [api:Latte\Compiler\Tag], che contiene informazioni di base sul tag (se si tratta di un tag classico o di un n:attributo, su quale riga si trova, ecc.

Se il tag deve avere degli argomenti, si controlla la loro esistenza chiamando `$tag->expectArguments()`. I metodi dell'oggetto `$tag->parser` sono disponibili per analizzarli:

- `parseExpression(): ExpressionNode` per un'espressione simile a quella di PHP (per esempio `10 + 3`)
- `parseUnquotedStringOrExpression(): ExpressionNode` per un'espressione o una stringa non quotata
- `parseArguments(): ArrayNode` contenuto dell'array (ad es. `10, true, foo => bar`)
- `parseModifier(): ModifierNode` per un modificatore (ad es. `|upper|truncate:10`)
- `parseType(): expressionNode` per un suggerimento di tipo (ad es. `int|string` o `Foo\Bar[]`)

e un [api:Latte\Compiler\TokenStream] di basso livello che opera direttamente con i token:

- `$tag->parser->stream->consume(...): Token`
- `$tag->parser->stream->tryConsume(...): ?Token`

Latte estende la sintassi di PHP in piccoli modi, ad esempio aggiungendo modificatori, operatori ternari abbreviati o permettendo di scrivere semplici stringhe alfanumeriche senza virgolette. Per questo motivo si usa il termine *PHP-like* invece di PHP. Così, il metodo `parseExpression()` analizza `foo` come `'foo'`, per esempio.
Inoltre, *stringa non quotata* è un caso speciale di stringa che non ha bisogno di essere quotata, ma allo stesso tempo non ha bisogno di essere alfanumerica. Ad esempio, è il percorso di un file nel tag `{include ../file.latte}`. Per analizzarla si usa il metodo `parseUnquotedStringOrExpression()`.

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

Torniamo al tag `{foreach}`. In esso ci aspettiamo argomenti della forma `expression + 'as' + second expression`, che analizziamo come segue:

```php
use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\Nodes\Php\ExpressionNode;
use Latte\Compiler\Nodes\AreaNode;

class ForeachNode extends StatementNode
{
	public ExpressionNode $expression;
	public ExpressionNode $value;

	public static function create(Latte\Compiler\Tag $tag): self
	{
		$tag->expectArguments();
		$node = $tag->node = new self;
		$node->expression = $tag->parser->parseExpression();
		$tag->parser->stream->consume('as');
		$node->value = $parser->parseExpression();
		return $node;
	}
}
```

Le espressioni che abbiamo scritto nelle variabili `$expression` e `$value` rappresentano dei sottonodi.

.[tip]
Definire le variabili con i sottonodi come **pubbliche**, in modo che possano essere modificate in [ulteriori fasi di elaborazione |#Compiler Passes], se necessario. È anche necessario **renderle disponibili** per l'[attraversamento |#AST Traversing].

Per i tag accoppiati, come il nostro, il metodo deve anche permettere a TemplateParser di analizzare il contenuto interno del tag. Questo viene gestito da `yield`, che restituisce una coppia ''[contenuto interno, tag finale]''. Il contenuto interno viene memorizzato nella variabile `$node->content`.

```php
public AreaNode $content;

public static function create(Latte\Compiler\Tag $tag): \Generator
{
	// ...
	[$node->content, $endTag] = yield;
	return $node;
}
```

La parola chiave `yield` fa terminare il metodo `create()`, restituendo il controllo al TemplateParser, che continua ad analizzare il contenuto finché non raggiunge il tag finale. A questo punto, passa il controllo a `create()`, che continua da dove si era interrotto. L'uso del metodo `yield`, restituisce automaticamente `Generator`.

Si può anche passare a `yield` un array di nomi di tag per i quali si vuole interrompere l'analisi se si verificano prima del tag finale. Questo ci aiuta a implementare il costrutto `{foreach}...{else}...{/foreach}` . Se si verifica `{else}`, si analizza il contenuto dopo di esso in `$node->elseContent`:

```php
public AreaNode $content;
public ?AreaNode $elseContent = null;

public static function create(Latte\Compiler\Tag $tag): \Generator
{
	// ...
	[$node->content, $nextTag] = yield ['else'];
	if ($nextTag?->name === 'else') {
		[$node->elseContent] = yield;
	}

	return $node;
}
```

Il nodo di ritorno completa l'analisi dei tag.


Generazione del codice PHP .[#toc-generating-php-code]
------------------------------------------------------

Ogni nodo deve implementare il metodo `print()`. Restituisce il codice PHP che rende la parte data del template (codice di runtime). Viene passato come parametro un oggetto [api:Latte\Compiler\PrintContext], che ha un utile metodo `format()` che semplifica l'assemblaggio del codice risultante.

Il metodo `format(string $mask, ...$args)` accetta i seguenti segnaposto nella maschera:
- `%node` stampa Nodo
- `%dump` esporta il valore in PHP
- `%raw` inserisce il testo direttamente senza alcuna trasformazione
- `%args` stampa ArrayNode come argomenti della chiamata di funzione
- `%line` stampa un commento con un numero di riga
- `%escape(...)` esegue l'escape del contenuto
- `%modify(...)` applica un modificatore
- `%modifyContent(...)` applica un modificatore ai blocchi


La nostra funzione `print()` potrebbe avere questo aspetto (trascuriamo il ramo `else` per semplicità):

```php
public function print(Latte\Compiler\PrintContext $context): string
{
	return $context->format(
		<<<'XX'
			foreach (%node as %node) %line {
				%node
			}

			XX,
		$this->expression,
		$this->value,
		$this->position,
		$this->content,
	);
}
```

La variabile `$this->position` è già definita dalla classe [api:Latte\Compiler\Node] e viene impostata dal parser. Contiene un oggetto [api:Latte\Compiler\Position] con la posizione del tag nel codice sorgente sotto forma di numero di riga e di colonna.

Il codice di runtime può utilizzare variabili ausiliarie. Per evitare collisioni con le variabili usate dal template stesso, è convenzione prefissarle con i caratteri `$ʟ__`.

Può anche utilizzare valori arbitrari in fase di esecuzione, che vengono passati al template sotto forma di provider utilizzando il metodo [Extension::getProviders() |#getProviders]. Si accede ad essi usando `$this->global->...`.


Attraversamento dell'AST .[#toc-ast-traversing]
-----------------------------------------------

Per attraversare l'albero AST in profondità, è necessario implementare il metodo `getIterator()`. Questo metodo consente di accedere ai sottonodi:

```php
public function &getIterator(): \Generator
{
	yield $this->expression;
	yield $this->value;
	yield $this->content;
	if ($this->elseContent) {
		yield $this->elseContent;
	}
}
```

Si noti che `getIterator()` restituisce un riferimento. Questo è ciò che consente ai visitatori dei nodi di sostituire i singoli nodi con altri nodi.

.[warning]
Se un nodo ha dei sottonodi, è necessario implementare questo metodo e rendere disponibili tutti i sottonodi. In caso contrario, si potrebbe creare una falla nella sicurezza. Ad esempio, la modalità sandbox non sarebbe in grado di controllare i sottonodi e di garantire che in essi non vengano richiamati costrutti non consentiti.

Poiché la parola chiave `yield` deve essere presente nel corpo del metodo anche se non ha nodi figli, scriverlo come segue:

```php
public function &getIterator(): \Generator
{
	if (false) {
		yield;
	}
}
```


Nodo ausiliario .[#toc-auxiliarynode]
-------------------------------------

Se si sta creando un nuovo tag per Latte, è consigliabile creare una classe di nodi dedicata, che lo rappresenti nell'albero AST (si veda la classe `ForeachNode` nell'esempio precedente). In alcuni casi, potrebbe essere utile la classe di nodi ausiliari [AuxiliaryNode |api:Latte\Compiler\Nodes\Php\Expression\AuxiliaryNode], che consente di passare il corpo del metodo `print()` e l'elenco dei nodi resi accessibili dal metodo `getIterator()` come parametri del costruttore:

```php
// Latte\Compiler\Nodes\Php\Expression\AuxiliaryNode
// or Latte\Compiler\Nodes\AuxiliaryNode

$node = new AuxiliaryNode(
	// body of the print() method:
	fn(PrintContext $context, $argNode) => $context->format('myFunc(%node)', $argNode),
	// nodes accessed via getIterator() and also passed into the print() method:
	[$argNode],
);
```


Il compilatore passa .[#toc-compiler-passes]
============================================

I passi del compilatore sono funzioni che modificano gli AST o raccolgono informazioni in essi. Sono restituiti dal metodo [Extension::getPasses() |#getPasses].


Traverser di nodi .[#toc-node-traverser]
----------------------------------------

Il modo più comune di lavorare con l'AST è quello di utilizzare un [api:Latte\Compiler\NodeTraverser]:

```php
use Latte\Compiler\Node;
use Latte\Compiler\NodeTraverser;

$ast = (new NodeTraverser)->traverse(
	$ast,
	enter: fn(Node $node) => ...,
	leave: fn(Node $node) => ...,
);
```

La funzione *enter* (cioè visitatore) viene chiamata quando si incontra per la prima volta un nodo, prima che i suoi sottonodi vengano elaborati. La funzione *leave* viene chiamata dopo che tutti i sottonodi sono stati visitati.
Uno schema comune è che *enter* viene usato per raccogliere alcune informazioni e poi *leave* esegue modifiche in base a queste. Nel momento in cui viene chiamata la funzione *leave*, tutto il codice all'interno del nodo sarà già stato visitato e saranno state raccolte le informazioni necessarie.

Come modificare l'AST? Il modo più semplice è cambiare semplicemente le proprietà dei nodi. Il secondo modo è quello di sostituire interamente il nodo, restituendo un nuovo nodo. Esempio: il codice seguente cambierà tutti gli interi nell'AST in stringhe (ad esempio, 42 sarà cambiato in `'42'`).

```php
use Latte\Compiler\Nodes\Php;

$ast = (new NodeTraverser)->traverse(
	$ast,
	leave: function (Node $node) {
		if ($node instanceof Php\Scalar\IntegerNode) {
            return new Php\Scalar\StringNode((string) $node->value);
        }
	},
);
```

Un AST può facilmente contenere migliaia di nodi, e la loro esplorazione può essere lenta. In alcuni casi, è possibile evitare una traversata completa.

Se si cercano tutti i `Html\ElementNode` in un albero, si sa che una volta visto `Php\ExpressionNode`, non ha senso controllare anche tutti i suoi nodi figli, perché l'HTML non può essere contenuto nelle espressioni. In questo caso, si può istruire il traverser a non ricorrervi all'interno del nodo della classe:

```php
$ast = (new NodeTraverser)->traverse(
	$ast,
	enter: function (Node $node) {
		if ($node instanceof Php\ExpressionNode) {
			return NodeTraverser::DontTraverseChildren;
        }
        // ...
	},
);
```

Se si cerca solo un nodo specifico, è anche possibile interrompere completamente l'attraversamento dopo averlo trovato.

```php
$ast = (new NodeTraverser)->traverse(
	$ast,
	enter: function (Node $node) {
		if ($node instanceof Nodes\ParametersNode) {
			return NodeTraverser::StopTraversal;
        }
        // ...
	},
);
```


Aiutanti dei nodi .[#toc-node-helpers]
--------------------------------------

La classe [api:Latte\Compiler\NodeHelpers] fornisce alcuni metodi che possono trovare nodi AST che soddisfano un certo callback, ecc. Vengono mostrati un paio di esempi:

```php
use Latte\Compiler\NodeHelpers;

// trova tutti i nodi degli elementi HTML
$elements = NodeHelpers::find($ast, fn(Node $node) => $node instanceof Nodes\Html\ElementNode);

// Trova il primo nodo di testo
$text = NodeHelpers::findFirst($ast, fn(Node $node) => $node instanceof Nodes\TextNode);

// converte il nodo valore PHP in valore reale
$value = NodeHelpers::toValue($node);

// converte il nodo testuale statico in stringa
$text = NodeHelpers::toText($node);
```

Creare un'estensione

Un'estensione è una classe riutilizzabile che può definire tag personalizzati, filtri, funzioni, provider, ecc.

Creiamo estensioni quando vogliamo riutilizzare le nostre personalizzazioni di Latte in progetti diversi o condividerle con altri. È anche utile creare un'estensione per ogni progetto web, che conterrà tutti i tag e i filtri specifici che si vogliono usare nei modelli di progetto.

Classe di estensione

L'estensione è una classe che eredita da Latte\Extension. Viene registrata con Latte usando addExtension() (o tramite il file di configurazione):

$latte = new Latte\Engine;
$latte->addExtension(new MyLatteExtension);

Se si registrano più estensioni e queste definiscono tag, filtri o funzioni con nomi identici, vince l'ultima estensione aggiunta. Questo implica anche che le estensioni possono sovrascrivere tag/filtri/funzioni nativi.

Ogni volta che si apporta una modifica a una classe e l'aggiornamento automatico non è disattivato, Latte ricompila automaticamente i modelli.

Una classe può implementare uno dei seguenti metodi:

abstract class Extension
{
	/**
	 * Initializes before template is compiler.
	 */
	public function beforeCompile(Engine $engine): void;

	/**
	 * Returns a list of parsers for Latte tags.
	 * @return array<string, callable>
	 */
	public function getTags(): array;

	/**
	 * Returns a list of compiler passes.
	 * @return array<string, callable>
	 */
	public function getPasses(): array;

	/**
	 * Returns a list of |filters.
	 * @return array<string, callable>
	 */
	public function getFilters(): array;

	/**
	 * Returns a list of functions used in templates.
	 * @return array<string, callable>
	 */
	public function getFunctions(): array;

	/**
	 * Returns a list of providers.
	 * @return array<mixed>
	 */
	public function getProviders(): array;

	/**
	 * Returns a value to distinguish multiple versions of the template.
	 */
	public function getCacheKey(Engine $engine): mixed;

	/**
	 * Initializes before template is rendered.
	 */
	public function beforeRender(Template $template): void;
}

Per avere un'idea dell'aspetto dell'estensione, dare un'occhiata al built-in CoreExtension.

beforeCompile(Latte\Engine $engine)void

Richiamato prima della compilazione del template. Il metodo può essere usato per le inizializzazioni legate alla compilazione, ad esempio.

getTags(): array

Richiamato quando il template viene compilato. Restituisce un array associativo nome tag ⇒ callable, che sono funzioni di parsing dei tag.

public function getTags(): array
{
	return [
		'foo' => [FooNode::class, 'create'],
		'bar' => [BarNode::class, 'create'],
		'n:baz' => [NBazNode::class, 'create'],
		// ...
	];
}

Il tag n:baz rappresenta un attributo n:puro, cioè un tag che può essere scritto solo come attributo.

Nel caso dei tag foo e bar, Latte riconosce automaticamente se si tratta di coppie e, in tal caso, può scriverli automaticamente usando n:attributes, comprese le varianti con i prefissi n:inner-foo e n:tag-foo.

L'ordine di esecuzione di tali n:attributi è determinato dal loro ordine nell'array restituito da getTags(). Pertanto, n:foo viene sempre eseguito prima di n:bar, anche se gli attributi sono elencati in ordine inverso nel tag HTML come <div n:bar="..." n:foo="...">.

Se è necessario determinare l'ordine di n:attributi tra più estensioni, si può usare il metodo di aiuto order(), dove il parametro before xor after determina quali tag sono ordinati prima o dopo il tag.

public function getTags(): array
{
	return [
		'foo' => self::order([FooNode::class, 'create'], before: 'bar')]
		'bar' => self::order([BarNode::class, 'create'], after: ['block', 'snippet'])]
	];
}

getPasses(): array

Viene richiamato quando il template viene compilato. Restituisce un array associativo nome-pass ⇒ callable, che sono funzioni che rappresentano i cosiddetti passaggi del compilatore che attraversano e modificano l'AST.

Anche in questo caso, si può usare il metodo helper order(). Il valore dei parametri before o after può essere * con il significato di prima/dopo tutto.

public function getPasses(): array
{
	return [
		'optimize' => [Passes::class, 'optimizePass'],
		'sandbox' => self::order([$this, 'sandboxPass'], before: '*'),
		// ...
	];
}

beforeRender(Latte\Engine $engine)void

Viene richiamato prima di ogni rendering del template. Il metodo può essere usato, ad esempio, per inizializzare le variabili utilizzate durante il rendering.

getFilters(): array

Viene richiamato prima che il template sia reso. Restituisce i filtri come array associativo nome filtro ⇒ callable.

public function getFilters(): array
{
	return [
		'batch' => [$this, 'batchFilter'],
		'trim' => [$this, 'trimFilter'],
		// ...
	];
}

getFunctions(): array

Viene richiamato prima che il modello sia reso. Restituisce le funzioni come array associativo nome funzione ⇒ callable.

public function getFunctions(): array
{
	return [
		'clamp' => [$this, 'clampFunction'],
		'divisibleBy' => [$this, 'divisibleByFunction'],
		// ...
	];
}

getProviders(): array

Viene richiamato prima che il template sia reso. Restituisce un array di fornitori, che di solito sono oggetti che usano i tag in fase di esecuzione. Vi si accede tramite $this->global->....

public function getProviders(): array
{
	return [
		'myFoo' => $this->foo,
		'myBar' => $this->bar,
		// ...
	];
}

getCacheKey(Latte\Engine $engine)mixed

Viene richiamato prima che il template venga reso. Il valore di ritorno diventa parte della chiave il cui hash è contenuto nel nome del file del template compilato. Pertanto, per valori di ritorno diversi, Latte genererà file di cache diversi.

Come funziona Latte?

Per capire come definire tag personalizzati o passaggi del compilatore, è essenziale capire come funziona Latte sotto il cofano.

La compilazione dei template in Latte funziona semplicisticamente in questo modo:

  • Per prima cosa, il lexer tokenizza il codice sorgente del template in piccoli pezzi (tokens) per facilitarne l'elaborazione.
  • Poi, il parser converte il flusso di token in un albero di nodi significativo (l'Abstract Syntax Tree, AST).
  • Infine, il compilatore genera una classe PHP dall'AST che rende il template e lo mette in cache.

In realtà, la compilazione è un po' più complicata. Latte ha due lexer e parser: uno per il modello HTML e uno per il codice PHP all'interno dei tag. Inoltre, il parsing non viene eseguito dopo la tokenizzazione, ma il lexer e il parser vengono eseguiti in parallelo in due „thread“ e si coordinano. Si tratta di scienza missilistica :-)

Inoltre, tutti i tag hanno le proprie routine di parsing. Quando il parser incontra un tag, chiama la sua funzione di parsing (restituisce Extension::getTags()). Il suo compito è analizzare gli argomenti del tag e, nel caso di tag accoppiati, il contenuto interno. Restituisce un nodo che diventa parte dell'AST. Per maggiori dettagli, vedere la funzione di parsing dei tag.

Quando il parser finisce il suo lavoro, abbiamo un AST completo che rappresenta il template. Il nodo radice è Latte\Compiler\Nodes\TemplateNode. I singoli nodi all'interno dell'albero rappresentano non solo i tag, ma anche gli elementi HTML, i loro attributi, le espressioni utilizzate all'interno dei tag, ecc.

A questo punto, entrano in gioco i cosiddetti Compiler pass, che sono funzioni (restituite da Extension::getPasses()) che modificano l'AST.

L'intero processo, dal caricamento del contenuto del modello, al parsing, fino alla generazione del file risultante, può essere messo in sequenza con questo codice, che si può sperimentare e scaricare i risultati intermedi:

$latte = new Latte\Engine;
$source = $latte->getLoader()->getContent($file);
$ast = $latte->parse($source);
$latte->applyPasses($ast);
$code = $latte->generate($ast, $file);

Esempio di AST

Per avere un'idea più precisa dell'AST, aggiungiamo un esempio. Questo è il modello sorgente:

{foreach $category->getItems() as $item}
	<li>{$item->name|upper}</li>
	{else}
	no items found
{/foreach}

E questa è la sua rappresentazione sotto forma di AST:

Latte\Compiler\Nodes\TemplateNode(
   Latte\Compiler\Nodes\FragmentNode(
      - Latte\Essential\Nodes\ForeachNode(
           expression: Latte\Compiler\Nodes\Php\Expression\MethodCallNode(
              object: Latte\Compiler\Nodes\Php\Expression\VariableNode('$category')
              name: Latte\Compiler\Nodes\Php\IdentifierNode('getItems')
           )
           value: Latte\Compiler\Nodes\Php\Expression\VariableNode('$item')
           content: Latte\Compiler\Nodes\FragmentNode(
              - Latte\Compiler\Nodes\TextNode('  ')
              - Latte\Compiler\Nodes\Html\ElementNode('li')(
                   content: Latte\Essential\Nodes\PrintNode(
                      expression: Latte\Compiler\Nodes\Php\Expression\PropertyFetchNode(
                         object: Latte\Compiler\Nodes\Php\Expression\VariableNode('$item')
                         name: Latte\Compiler\Nodes\Php\IdentifierNode('name')
                      )
                      modifier: Latte\Compiler\Nodes\Php\ModifierNode(
                         filters:
                            - Latte\Compiler\Nodes\Php\FilterNode('upper')
                      )
                   )
                )
            )
            else: Latte\Compiler\Nodes\FragmentNode(
               - Latte\Compiler\Nodes\TextNode('no items found')
            )
        )
   )
)

Tag personalizzati

Per definire un nuovo tag sono necessari tre passaggi:

Funzione di parsing del tag

L'analisi dei tag è gestita dalla funzione di parsing (quella restituita da Extension::getTags()). Il suo compito è quello di analizzare e controllare qualsiasi argomento all'interno del tag (utilizza TagParser per farlo). Inoltre, se il tag è una coppia, chiederà a TemplateParser di analizzare e restituire il contenuto interno. La funzione crea e restituisce un nodo, di solito figlio di Latte\Compiler\Nodes\StatementNode, che diventa parte dell'AST.

Creiamo una classe per ogni nodo, cosa che faremo ora, e inseriamo elegantemente la funzione di parsing in essa come factory statica. Come esempio, proviamo a creare il noto tag {foreach}:

use Latte\Compiler\Nodes\StatementNode;

class ForeachNode extends StatementNode
{
	// una funzione di parsing che per ora crea solo un nodo
	public static function create(Latte\Compiler\Tag $tag): self
	{
		$node = $tag->node = new self;
		return $node;
	}

	public function print(Latte\Compiler\PrintContext $context): string
	{
		// il codice sarà aggiunto in seguito
	}

	public function &getIterator(): \Generator
	{
		// il codice sarà aggiunto in seguito
	}
}

Latte\Compiler\TagParser $tag->parserAlla funzione di parsing create() viene passato un oggetto Latte\Compiler\Tag, che contiene informazioni di base sul tag (se si tratta di un tag classico o di un n:attributo, su quale riga si trova, ecc.

Se il tag deve avere degli argomenti, si controlla la loro esistenza chiamando $tag->expectArguments(). I metodi dell'oggetto $tag->parser sono disponibili per analizzarli:

  • parseExpression(): ExpressionNode per un'espressione simile a quella di PHP (per esempio 10 + 3)
  • parseUnquotedStringOrExpression(): ExpressionNode per un'espressione o una stringa non quotata
  • parseArguments(): ArrayNode contenuto dell'array (ad es. 10, true, foo => bar)
  • parseModifier(): ModifierNode per un modificatore (ad es. |upper|truncate:10)
  • parseType(): expressionNode per un suggerimento di tipo (ad es. int|string o Foo\Bar[])

e un Latte\Compiler\TokenStream di basso livello che opera direttamente con i token:

  • $tag->parser->stream->consume(...): Token
  • $tag->parser->stream->tryConsume(...): ?Token

Latte estende la sintassi di PHP in piccoli modi, ad esempio aggiungendo modificatori, operatori ternari abbreviati o permettendo di scrivere semplici stringhe alfanumeriche senza virgolette. Per questo motivo si usa il termine PHP-like invece di PHP. Così, il metodo parseExpression() analizza foo come 'foo', per esempio. Inoltre, stringa non quotata è un caso speciale di stringa che non ha bisogno di essere quotata, ma allo stesso tempo non ha bisogno di essere alfanumerica. Ad esempio, è il percorso di un file nel tag {include ../file.latte}. Per analizzarla si usa il metodo parseUnquotedStringOrExpression().

Studiare le classi di nodi che fanno parte di Latte è il modo migliore per imparare tutti i dettagli del processo di parsing.

Torniamo al tag {foreach}. In esso ci aspettiamo argomenti della forma expression + 'as' + second expression, che analizziamo come segue:

use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\Nodes\Php\ExpressionNode;
use Latte\Compiler\Nodes\AreaNode;

class ForeachNode extends StatementNode
{
	public ExpressionNode $expression;
	public ExpressionNode $value;

	public static function create(Latte\Compiler\Tag $tag): self
	{
		$tag->expectArguments();
		$node = $tag->node = new self;
		$node->expression = $tag->parser->parseExpression();
		$tag->parser->stream->consume('as');
		$node->value = $parser->parseExpression();
		return $node;
	}
}

Le espressioni che abbiamo scritto nelle variabili $expression e $value rappresentano dei sottonodi.

Definire le variabili con i sottonodi come pubbliche, in modo che possano essere modificate in ulteriori fasi di elaborazione, se necessario. È anche necessario renderle disponibili per l'attraversamento.

Per i tag accoppiati, come il nostro, il metodo deve anche permettere a TemplateParser di analizzare il contenuto interno del tag. Questo viene gestito da yield, che restituisce una coppia [contenuto interno, tag finale]. Il contenuto interno viene memorizzato nella variabile $node->content.

public AreaNode $content;

public static function create(Latte\Compiler\Tag $tag): \Generator
{
	// ...
	[$node->content, $endTag] = yield;
	return $node;
}

La parola chiave yield fa terminare il metodo create(), restituendo il controllo al TemplateParser, che continua ad analizzare il contenuto finché non raggiunge il tag finale. A questo punto, passa il controllo a create(), che continua da dove si era interrotto. L'uso del metodo yield, restituisce automaticamente Generator.

Si può anche passare a yield un array di nomi di tag per i quali si vuole interrompere l'analisi se si verificano prima del tag finale. Questo ci aiuta a implementare il costrutto {foreach}...{else}...{/foreach} . Se si verifica {else}, si analizza il contenuto dopo di esso in $node->elseContent:

public AreaNode $content;
public ?AreaNode $elseContent = null;

public static function create(Latte\Compiler\Tag $tag): \Generator
{
	// ...
	[$node->content, $nextTag] = yield ['else'];
	if ($nextTag?->name === 'else') {
		[$node->elseContent] = yield;
	}

	return $node;
}

Il nodo di ritorno completa l'analisi dei tag.

Generazione del codice PHP

Ogni nodo deve implementare il metodo print(). Restituisce il codice PHP che rende la parte data del template (codice di runtime). Viene passato come parametro un oggetto Latte\Compiler\PrintContext, che ha un utile metodo format() che semplifica l'assemblaggio del codice risultante.

Il metodo format(string $mask, ...$args) accetta i seguenti segnaposto nella maschera:

  • %node stampa Nodo
  • %dump esporta il valore in PHP
  • %raw inserisce il testo direttamente senza alcuna trasformazione
  • %args stampa ArrayNode come argomenti della chiamata di funzione
  • %line stampa un commento con un numero di riga
  • %escape(...) esegue l'escape del contenuto
  • %modify(...) applica un modificatore
  • %modifyContent(...) applica un modificatore ai blocchi

La nostra funzione print() potrebbe avere questo aspetto (trascuriamo il ramo else per semplicità):

public function print(Latte\Compiler\PrintContext $context): string
{
	return $context->format(
		<<<'XX'
			foreach (%node as %node) %line {
				%node
			}

			XX,
		$this->expression,
		$this->value,
		$this->position,
		$this->content,
	);
}

La variabile $this->position è già definita dalla classe Latte\Compiler\Node e viene impostata dal parser. Contiene un oggetto Latte\Compiler\Position con la posizione del tag nel codice sorgente sotto forma di numero di riga e di colonna.

Il codice di runtime può utilizzare variabili ausiliarie. Per evitare collisioni con le variabili usate dal template stesso, è convenzione prefissarle con i caratteri $ʟ__.

Può anche utilizzare valori arbitrari in fase di esecuzione, che vengono passati al template sotto forma di provider utilizzando il metodo Extension::getProviders(). Si accede ad essi usando $this->global->....

Attraversamento dell'AST

Per attraversare l'albero AST in profondità, è necessario implementare il metodo getIterator(). Questo metodo consente di accedere ai sottonodi:

public function &getIterator(): \Generator
{
	yield $this->expression;
	yield $this->value;
	yield $this->content;
	if ($this->elseContent) {
		yield $this->elseContent;
	}
}

Si noti che getIterator() restituisce un riferimento. Questo è ciò che consente ai visitatori dei nodi di sostituire i singoli nodi con altri nodi.

Se un nodo ha dei sottonodi, è necessario implementare questo metodo e rendere disponibili tutti i sottonodi. In caso contrario, si potrebbe creare una falla nella sicurezza. Ad esempio, la modalità sandbox non sarebbe in grado di controllare i sottonodi e di garantire che in essi non vengano richiamati costrutti non consentiti.

Poiché la parola chiave yield deve essere presente nel corpo del metodo anche se non ha nodi figli, scriverlo come segue:

public function &getIterator(): \Generator
{
	if (false) {
		yield;
	}
}

Nodo ausiliario

Se si sta creando un nuovo tag per Latte, è consigliabile creare una classe di nodi dedicata, che lo rappresenti nell'albero AST (si veda la classe ForeachNode nell'esempio precedente). In alcuni casi, potrebbe essere utile la classe di nodi ausiliari AuxiliaryNode, che consente di passare il corpo del metodo print() e l'elenco dei nodi resi accessibili dal metodo getIterator() come parametri del costruttore:

// Latte\Compiler\Nodes\Php\Expression\AuxiliaryNode
// or Latte\Compiler\Nodes\AuxiliaryNode

$node = new AuxiliaryNode(
	// body of the print() method:
	fn(PrintContext $context, $argNode) => $context->format('myFunc(%node)', $argNode),
	// nodes accessed via getIterator() and also passed into the print() method:
	[$argNode],
);

Il compilatore passa

I passi del compilatore sono funzioni che modificano gli AST o raccolgono informazioni in essi. Sono restituiti dal metodo Extension::getPasses().

Traverser di nodi

Il modo più comune di lavorare con l'AST è quello di utilizzare un Latte\Compiler\NodeTraverser:

use Latte\Compiler\Node;
use Latte\Compiler\NodeTraverser;

$ast = (new NodeTraverser)->traverse(
	$ast,
	enter: fn(Node $node) => ...,
	leave: fn(Node $node) => ...,
);

La funzione enter (cioè visitatore) viene chiamata quando si incontra per la prima volta un nodo, prima che i suoi sottonodi vengano elaborati. La funzione leave viene chiamata dopo che tutti i sottonodi sono stati visitati. Uno schema comune è che enter viene usato per raccogliere alcune informazioni e poi leave esegue modifiche in base a queste. Nel momento in cui viene chiamata la funzione leave, tutto il codice all'interno del nodo sarà già stato visitato e saranno state raccolte le informazioni necessarie.

Come modificare l'AST? Il modo più semplice è cambiare semplicemente le proprietà dei nodi. Il secondo modo è quello di sostituire interamente il nodo, restituendo un nuovo nodo. Esempio: il codice seguente cambierà tutti gli interi nell'AST in stringhe (ad esempio, 42 sarà cambiato in '42').

use Latte\Compiler\Nodes\Php;

$ast = (new NodeTraverser)->traverse(
	$ast,
	leave: function (Node $node) {
		if ($node instanceof Php\Scalar\IntegerNode) {
            return new Php\Scalar\StringNode((string) $node->value);
        }
	},
);

Un AST può facilmente contenere migliaia di nodi, e la loro esplorazione può essere lenta. In alcuni casi, è possibile evitare una traversata completa.

Se si cercano tutti i Html\ElementNode in un albero, si sa che una volta visto Php\ExpressionNode, non ha senso controllare anche tutti i suoi nodi figli, perché l'HTML non può essere contenuto nelle espressioni. In questo caso, si può istruire il traverser a non ricorrervi all'interno del nodo della classe:

$ast = (new NodeTraverser)->traverse(
	$ast,
	enter: function (Node $node) {
		if ($node instanceof Php\ExpressionNode) {
			return NodeTraverser::DontTraverseChildren;
        }
        // ...
	},
);

Se si cerca solo un nodo specifico, è anche possibile interrompere completamente l'attraversamento dopo averlo trovato.

$ast = (new NodeTraverser)->traverse(
	$ast,
	enter: function (Node $node) {
		if ($node instanceof Nodes\ParametersNode) {
			return NodeTraverser::StopTraversal;
        }
        // ...
	},
);

Aiutanti dei nodi

La classe Latte\Compiler\NodeHelpers fornisce alcuni metodi che possono trovare nodi AST che soddisfano un certo callback, ecc. Vengono mostrati un paio di esempi:

use Latte\Compiler\NodeHelpers;

// trova tutti i nodi degli elementi HTML
$elements = NodeHelpers::find($ast, fn(Node $node) => $node instanceof Nodes\Html\ElementNode);

// Trova il primo nodo di testo
$text = NodeHelpers::findFirst($ast, fn(Node $node) => $node instanceof Nodes\TextNode);

// converte il nodo valore PHP in valore reale
$value = NodeHelpers::toValue($node);

// converte il nodo testuale statico in stringa
$text = NodeHelpers::toText($node);