Création d'une extension

Une extension est une classe réutilisable qui permet de définir des balises, des filtres, des fonctions, des fournisseurs, etc. personnalisés.

Nous créons des extensions lorsque nous voulons réutiliser nos personnalisations Latte dans différents projets ou les partager avec d'autres. Il est également utile de créer une extension pour chaque projet Web qui contiendra toutes les balises et tous les filtres spécifiques que vous souhaitez utiliser dans les modèles de projet.

Classe d'extension

Extension est une classe héritant de Latte\Extension. Elle est enregistrée dans Latte à l'aide de addExtension() (ou via le fichier de configuration) :

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

Si vous enregistrez plusieurs extensions et qu'elles définissent des balises, des filtres ou des fonctions de même nom, la dernière extension ajoutée l'emporte. Cela implique également que vos extensions peuvent remplacer les tags/filtres/fonctions natifs.

Chaque fois que vous apportez une modification à une classe et que l'actualisation automatique n'est pas désactivée, Latte recompile automatiquement vos modèles.

Une classe peut mettre en œuvre l'une des méthodes suivantes :

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;
}

Pour avoir une idée de ce à quoi ressemble l'extension, jetez un coup d'œil à la CoreExtension intégrée.

beforeCompile(Latte\Engine $engine)void

Appelé avant que le modèle ne soit compilé. Cette méthode peut être utilisée pour les initialisations liées à la compilation, par exemple.

getTags(): array

Appelé lorsque le modèle est compilé. Retourne un tableau associatif nom de la balise ⇒ appelable, qui sont des fonctions d'analyse de la balise.

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

La balise n:baz représente un pur n:attribut, c'est-à-dire une balise qui ne peut être écrite que comme un attribut.

Dans le cas des balises foo et bar, Latte reconnaîtra automatiquement s'il s'agit de paires, et si c'est le cas, elles peuvent être écrites automatiquement en utilisant des n:attributs, y compris les variantes avec les préfixes n:inner-foo et n:tag-foo.

L'ordre d'exécution de ces n:attributes est déterminé par leur ordre dans le tableau renvoyé par getTags(). Ainsi, n:foo est toujours exécuté avant n:bar, même si les attributs sont listés dans l'ordre inverse dans la balise HTML comme <div n:bar="..." n:foo="...">.

Si vous devez déterminer l'ordre des n:attributs sur plusieurs extensions, utilisez la méthode d'aide order(), où le paramètre before xor after détermine quelles balises sont ordonnées avant ou après la balise .

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

getPasses(): array

Elle est appelée lorsque le modèle est compilé. Elle renvoie un tableau associatif name pass ⇒ callable, qui sont des fonctions représentant ce qu'on appelle des passes de compilation qui traversent et modifient l'AST.

Là encore, la méthode d'aide order() peut être utilisée. La valeur des paramètres before ou after peut être * avec la signification avant/après tout.

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

beforeRender(Latte\Engine $engine)void

Elle est appelée avant chaque rendu de modèle. La méthode peut être utilisée, par exemple, pour initialiser les variables utilisées pendant le rendu.

getFilters(): array

Il est appelé avant que le modèle soit rendu. Retourne les filtres sous la forme d'un tableau associatif nom du filtre ⇒ appelable.

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

getFunctions(): array

Il est appelé avant que le modèle ne soit rendu. Retourne les fonctions sous la forme d'un tableau associatif nom de la fonction ⇒ appelable.

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

getProviders(): array

Elle est appelée avant le rendu du modèle. Renvoie un tableau de fournisseurs, qui sont généralement des objets qui utilisent des balises au moment de l'exécution. Ils sont accessibles via $this->global->....

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

getCacheKey(Latte\Engine $engine)mixed

Elle est appelée avant le rendu du modèle. La valeur de retour fait partie de la clé dont le hachage est contenu dans le nom du fichier de modèle compilé. Ainsi, pour différentes valeurs de retour, Latte générera différents fichiers de cache.

Comment fonctionne Latte ?

Pour comprendre comment définir des balises personnalisées ou des passages de compilateur, il est essentiel de comprendre comment Latte fonctionne sous le capot.

La compilation de modèles dans Latte fonctionne de manière simpliste comme suit :

  • Tout d'abord, le lexer segmente le code source du modèle en petits morceaux (tokens) pour faciliter le traitement.
  • Ensuite, le parser convertit le flux de tokens en un arbre de nœuds significatif (l'arbre de syntaxe abstraite, AST).
  • Enfin, le compilateur génère une classe PHP à partir de l'AST qui rend le modèle et le met en cache.

En fait, la compilation est un peu plus compliquée. Latte a deux lexers et parsers : un pour le modèle HTML et un pour le code PHP à l'intérieur des balises. De plus, l'analyse syntaxique ne s'exécute pas après la tokénisation, mais le lexer et l'analyse syntaxique s'exécutent en parallèle dans deux “threads” et se coordonnent. C'est de la science-fiction :-)

En outre, toutes les balises ont leurs propres routines d'analyse. Lorsque l'analyseur rencontre une balise, il appelle sa fonction d'analyse (il renvoie Extension::getTags()). Son travail consiste à analyser les arguments de la balise et, dans le cas de balises appariées, le contenu interne. Elle renvoie un node qui devient une partie de l'AST. Voir Fonction d'analyse syntaxique des balises pour plus de détails.

Lorsque l'analyseur syntaxique termine son travail, nous avons un AST complet représentant le modèle. Le nœud racine est Latte\Compiler\Nodes\TemplateNode. Les nœuds individuels à l'intérieur de l'arbre représentent non seulement les balises, mais aussi les éléments HTML, leurs attributs, les expressions utilisées à l'intérieur des balises, etc.

Ensuite, les passes du compilateur entrent en jeu. Il s'agit de fonctions (renvoyées par Extension::getPasses()) qui modifient l'AST.

L'ensemble du processus, depuis le chargement du contenu du modèle, en passant par l'analyse syntaxique, jusqu'à la génération du fichier résultant, peut être séquencé à l'aide de ce code, avec lequel vous pouvez expérimenter et vidanger les résultats intermédiaires :

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

Exemple d'AST

Pour avoir une meilleure idée de l'AST, nous ajoutons un exemple. Il s'agit du modèle source :

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

Et voici sa représentation sous forme d'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')
            )
        )
   )
)

Balises personnalisées

Trois étapes sont nécessaires pour définir une nouvelle balise :

Fonction d'analyse syntaxique des balises

L'analyse des balises est gérée par leur fonction d'analyse (celle renvoyée par Extension::getTags()). Son travail consiste à analyser et à vérifier les arguments contenus dans la balise (elle utilise TagParser pour ce faire). De plus, si la balise est une paire, elle demandera à TemplateParser d'analyser et de retourner le contenu interne. La fonction crée et renvoie un nœud, qui est généralement un enfant de Latte\Compiler\Nodes\StatementNode, et qui devient une partie de l'AST.

Nous créons une classe pour chaque nœud, ce que nous allons faire maintenant, et nous y plaçons élégamment la fonction d'analyse en tant que fabrique statique. À titre d'exemple, essayons de créer la balise familière {foreach}:

use Latte\Compiler\Nodes\StatementNode;

class ForeachNode extends StatementNode
{
	// une fonction d'analyse syntaxique qui crée simplement un nœud pour l'instant
	public static function create(Latte\Compiler\Tag $tag): self
	{
		$node = $tag->node = new self;
		return $node;
	}

	public function print(Latte\Compiler\PrintContext $context): string
	{
		// le code sera ajouté plus tard
	}

	public function &getIterator(): \Generator
	{
		// le code sera ajouté plus tard
	}
}

La fonction d'analyse syntaxique create() reçoit un objet Latte\Compiler\Tag, qui contient des informations de base sur la balise (s'il s'agit d'une balise classique ou d'un n:attribut, sur quelle ligne elle se trouve, etc.) et accède principalement à Latte\Compiler\TagParser dans $tag->parser.

Si la balise doit avoir des arguments, vérifiez leur existence en appelant $tag->expectArguments(). Les méthodes de l'objet $tag->parser sont disponibles pour les analyser :

  • parseExpression(): ExpressionNode pour une expression de type PHP (par exemple 10 + 3)
  • parseUnquotedStringOrExpression(): ExpressionNode pour une expression ou une chaîne de caractères non citée
  • parseArguments(): ArrayNode pour le contenu d'un tableau (par exemple, 10, true, foo => bar)
  • parseModifier(): ModifierNode pour un modificateur (par ex. |upper|truncate:10)
  • parseType(): expressionNode pour un indice de type (par exemple, int|string ou Foo\Bar[])

et un bas niveau Latte\Compiler\TokenStream opérant directement avec les jetons :

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

Latte étend la syntaxe de PHP par petites touches, par exemple en ajoutant des modificateurs, des opérateurs ternaires raccourcis, ou en permettant d'écrire des chaînes alphanumériques simples sans guillemets. C'est pourquoi nous utilisons le terme PHP-like au lieu de PHP. Ainsi, la méthode parseExpression() analyse foo comme 'foo', par exemple. En outre, unquoted-string est un cas particulier de chaîne de caractères qui n'a pas besoin d'être citée, mais qui n'a pas besoin d'être alphanumérique. Par exemple, il s'agit du chemin d'accès à un fichier dans la balise {include ../file.latte}. La méthode parseUnquotedStringOrExpression() est utilisée pour l'analyser.

L'étude des classes de nœuds qui font partie de Latte est la meilleure façon d'apprendre tous les détails minutieux du processus d'analyse.

Revenons à la balise {foreach}. Dans cette balise, nous attendons des arguments de la forme expression + 'as' + second expression, que nous analysons comme suit :

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;
	}
}

Les expressions que nous avons écrites dans les variables $expression et $value représentent des sous-nœuds.

Définissez les variables avec les sous-nœuds comme publics afin qu'ils puissent être modifiés dans les étapes de traitement ultérieures si nécessaire. Il est également nécessaire de les rendre disponibles pour la navigation.

Pour les balises appariées, comme la nôtre, la méthode doit également laisser TemplateParser analyser le contenu interne de la balise. Ceci est géré par yield, qui renvoie une paire [contenu interne, balise finale]. Nous stockons le contenu interne dans la variable $node->content.

public AreaNode $content;

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

Le mot-clé yield entraîne la fin de la méthode create(), qui redonne le contrôle au TemplateParser, qui continue à analyser le contenu jusqu'à ce qu'il atteigne la balise de fin. Il transmet ensuite le contrôle à create(), qui reprend là où il s'est arrêté. L'utilisation de la méthode yield, renvoie automatiquement Generator.

Vous pouvez également transmettre à yield un tableau de noms de balises pour lesquelles vous souhaitez arrêter l'analyse s'ils apparaissent avant la balise de fin. Cela nous aide à mettre en œuvre la construction {foreach}...{else}...{/foreach} construction. Si {else} apparaît, nous analysons le contenu qui le suit dans $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;
}

Le nœud de retour termine l'analyse des balises.

Génération du code PHP

Chaque nœud doit implémenter la méthode print(). Renvoie le code PHP qui rend la partie donnée du modèle (code d'exécution). On lui passe un objet Latte\Compiler\PrintContext en paramètre, qui possède une méthode utile format() qui simplifie l'assemblage du code résultant.

La méthode format(string $mask, ...$args) accepte les caractères de remplacement suivants dans le masque :

  • %node imprime Node
  • %dump exporte la valeur vers PHP
  • %raw insère le texte directement sans aucune transformation
  • %args imprime ArrayNode comme arguments à l'appel de fonction
  • %line imprime un commentaire avec un numéro de ligne
  • %escape(...) échappe le contenu
  • %modify(...) applique un modificateur
  • %modifyContent(...) applique un modificateur aux blocs

Notre fonction print() pourrait ressembler à ceci (nous négligeons la branche else pour simplifier) :

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 variable $this->position est déjà définie par la classe Latte\Compiler\Node et est définie par l'analyseur syntaxique. Elle contient un objet Latte\Compiler\Position avec la position de la balise dans le code source sous la forme d'un numéro de ligne et de colonne.

Le code d'exécution peut utiliser des variables auxiliaires. Pour éviter toute collision avec les variables utilisées par le modèle lui-même, il est de convention de les préfixer avec les caractères $ʟ__.

Il peut également utiliser des valeurs arbitraires au moment de l'exécution, qui sont transmises au modèle sous la forme de fournisseurs à l'aide de la méthode Extension::getProviders(). Il y accède en utilisant $this->global->....

Traversée de l'AST

Afin de traverser l'arbre AST en profondeur, il est nécessaire d'implémenter la méthode getIterator(). Cela permettra d'accéder aux sous-nœuds :

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

Notez que getIterator() renvoie une référence. C'est ce qui permet aux visiteurs des nœuds de remplacer les nœuds individuels par d'autres nœuds.

Si un nœud a des sous-nœuds, il est nécessaire d'implémenter cette méthode et de rendre tous les sous-nœuds disponibles. Sinon, une faille de sécurité pourrait être créée. Par exemple, le mode sandbox ne serait pas en mesure de contrôler les sous-nœuds et de garantir que les constructions non autorisées ne sont pas appelées dans ces derniers.

Étant donné que le mot-clé yield doit être présent dans le corps de la méthode, même si celle-ci n'a pas de nœuds enfants, écrivez-la comme suit :

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

AuxiliaryNode (nœud auxiliaire)

Si vous créez une nouvelle balise pour Latte, il est conseillé de créer une classe de nœuds dédiée, qui la représentera dans l'arbre AST (voir la classe ForeachNode dans l'exemple ci-dessus). Dans certains cas, la classe de nœuds auxiliaire triviale AuxiliaryNode peut s'avérer utile, car elle permet de passer le corps de la méthode print() et la liste des nœuds rendus accessibles par la méthode getIterator() en tant que paramètres du constructeur :

// 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],
);

Le compilateur passe

Les Compiler Passes sont des fonctions qui modifient les AST ou collectent des informations dans ces derniers. Elles sont renvoyées par la méthode Extension::getPasses().

Traverseur de nœuds

La façon la plus courante de travailler avec l'AST est d'utiliser 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 fonction enter (c'est-à-dire le visiteur) est appelée lorsqu'un nœud est rencontré pour la première fois, avant que ses sous-nœuds ne soient traités. La fonction leave est appelée après que tous les sous-nœuds aient été visités. Un modèle commun est que la fonction enter est utilisée pour collecter certaines informations, puis la fonction leave effectue des modifications sur cette base. Au moment où leave est appelée, tout le code à l'intérieur du nœud aura déjà été visité et les informations nécessaires auront été collectées.

Comment modifier l'AST ? La manière la plus simple est de changer simplement les propriétés des nœuds. La deuxième façon est de remplacer entièrement le nœud en retournant un nouveau nœud. Exemple : le code suivant changera tous les entiers de l'AST en chaînes de caractères (par exemple, 42 sera changé en '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 peut facilement contenir des milliers de nœuds, et les parcourir tous peut être lent. Dans certains cas, il est possible d'éviter une traversée complète.

Si vous cherchez tous les Html\ElementNode dans un arbre, vous savez qu'une fois que vous avez vu Php\ExpressionNode, il est inutile de vérifier également tous ses nœuds enfants, car le HTML ne peut pas être à l'intérieur des expressions. Dans ce cas, vous pouvez demander au traverseur de ne pas faire de récursion dans le nœud de classe :

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

Si vous ne recherchez qu'un seul nœud spécifique, il est également possible d'interrompre entièrement la traversée après l'avoir trouvé.

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

Aides pour les nœuds

La classe Latte\Compiler\NodeHelpers fournit quelques méthodes qui peuvent trouver des noeuds AST qui satisfont un certain callback etc. Quelques exemples sont montrés :

use Latte\Compiler\NodeHelpers;

// trouve tous les nœuds d'éléments HTML
$elements = NodeHelpers::find($ast, fn(Node $node) => $node instanceof Nodes\Html\ElementNode);

// trouve le premier noeud de texte
$text = NodeHelpers::findFirst($ast, fn(Node $node) => $node instanceof Nodes\TextNode);

// convertit le noeud de valeur PHP en valeur réelle
$value = NodeHelpers::toValue($node);

// convertit un noeud textuel statique en chaîne de caractères
$text = NodeHelpers::toText($node);
version: 3.0