Création de balises personnalisées

Cette page fournit un guide complet pour créer des balises personnalisées dans Latte. Nous aborderons tout, des balises simples aux scénarios plus complexes avec du contenu imbriqué et des besoins d'analyse spécifiques, en nous basant sur votre compréhension de la manière dont Latte compile les templates.

Les balises personnalisées offrent le plus haut niveau de contrôle sur la syntaxe du template et la logique de rendu, mais elles constituent également le point d'extension le plus complexe. Avant de décider de créer votre propre balise, envisagez toujours s'il n'existe pas de solution plus simple ou si une balise appropriée n'existe pas déjà dans l'ensemble standard. N'utilisez des balises personnalisées que lorsque les alternatives plus simples ne suffisent pas à vos besoins.

Comprendre le processus de compilation

Pour créer efficacement des balises personnalisées, il est utile d'expliquer comment Latte traite les templates. Comprendre ce processus clarifie pourquoi les balises sont structurées de cette manière et comment elles s'intègrent dans le contexte plus large.

La compilation d'un template dans Latte, de manière simplifiée, comprend ces étapes clés :

  1. Analyse lexicale : Le lexer lit le code source du template (fichier .latte) et le divise en une séquence de petites parties distinctes appelées tokens (par exemple, {, foreach, $variable, }, texte HTML, etc.).
  2. Analyse syntaxique (Parsing) : Le parser prend ce flux de tokens et construit à partir de celui-ci une structure arborescente significative représentant la logique et le contenu du template. Cet arbre est appelé arbre syntaxique abstrait (AST).
  3. Passes de compilation : Avant de générer le code PHP, Latte exécute des passes de compilation. Ce sont des fonctions qui parcourent l'ensemble de l'AST et peuvent le modifier ou collecter des informations. Cette étape est cruciale pour des fonctionnalités telles que la sécurité (Sandbox) ou l'optimisation.
  4. Génération de code : Enfin, le compilateur parcourt l'AST (potentiellement modifié) et génère le code de classe PHP correspondant. Ce code PHP est ce qui rend réellement le template lors de l'exécution.
  5. Mise en cache : Le code PHP généré est stocké sur le disque, ce qui rend les rendus ultérieurs très rapides, car les étapes 1 à 4 sont ignorées.

En réalité, la compilation est un peu plus complexe. Latte a deux lexers et parseurs : un pour le template HTML et un autre pour le code de type PHP à l'intérieur des balises. De plus, l'analyse syntaxique ne se déroule pas après la tokenisation, mais le lexer et le parseur s'exécutent en parallèle dans deux “threads” et se coordonnent. Croyez-moi, programmer cela relevait de la science des fusées :-)

L'ensemble du processus, du chargement du contenu du template, en passant par l'analyse syntaxique, jusqu'à la génération du fichier résultant, peut être séquencé avec ce code, avec lequel vous pouvez expérimenter et afficher 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);

Anatomie d'une balise

La création d'une balise personnalisée entièrement fonctionnelle dans Latte implique plusieurs parties interconnectées. Avant de nous lancer dans l'implémentation, comprenons les concepts et la terminologie de base, en utilisant une analogie avec HTML et le Document Object Model (DOM).

Balises vs Nœuds (Analogie avec HTML)

En HTML, nous écrivons des balises comme <p> ou <div>...</div>. Ces balises constituent la syntaxe dans le code source. Lorsque le navigateur analyse ce HTML, il crée une représentation en mémoire appelée Document Object Model (DOM). Dans le DOM, les balises HTML sont représentées par des nœuds (spécifiquement des nœuds Element dans la terminologie du DOM JavaScript). Nous travaillons programmatiquement avec ces nœuds (par exemple, en utilisant document.getElementById(...) en JavaScript, qui renvoie un nœud Element). Une balise n'est qu'une représentation textuelle dans le fichier source ; un nœud est une représentation objet dans l'arbre logique.

Latte fonctionne de manière similaire :

  • Dans le fichier de template .latte, vous écrivez des balises Latte, comme {foreach ...} et {/foreach}. C'est la syntaxe avec laquelle vous, en tant qu'auteur du template, travaillez.
  • Lorsque Latte analyse le template, il construit un Arbre Syntaxique Abstrait (AST). Cet arbre est composé de nœuds. Chaque balise Latte, élément HTML, morceau de texte ou expression dans le template devient un ou plusieurs nœuds dans cet arbre.
  • La classe de base pour tous les nœuds dans l'AST est Latte\Compiler\Node. Tout comme le DOM a différents types de nœuds (Element, Text, Comment), l'AST de Latte a différents types de nœuds. Vous rencontrerez Latte\Compiler\Nodes\TextNode pour le texte statique, Latte\Compiler\Nodes\Html\ElementNode pour les éléments HTML, Latte\Compiler\Nodes\Php\ExpressionNode pour les expressions à l'intérieur des balises, et de manière cruciale pour les balises personnalisées, des nœuds héritant de Latte\Compiler\Nodes\StatementNode.

Pourquoi StatementNode ?

Les éléments HTML (Html\ElementNode) représentent principalement la structure et le contenu. Les expressions PHP (Php\ExpressionNode) représentent des valeurs ou des calculs. Mais qu'en est-il des balises Latte comme {if}, {foreach} ou notre propre {datetime} ? Ces balises effectuent des actions, contrôlent le flux du programme ou génèrent une sortie basée sur la logique. Ce sont des unités fonctionnelles qui font de Latte un puissant moteur de template, et non pas seulement un langage de balisage.

En programmation, de telles unités effectuant des actions sont souvent appelées “statements” (instructions). Par conséquent, les nœuds représentant ces balises Latte fonctionnelles héritent généralement de Latte\Compiler\Nodes\StatementNode. Cela les distingue des nœuds purement structurels (comme les éléments HTML) ou des nœuds représentant des valeurs (comme les expressions).

Les composants clés

Passons en revue les principaux composants nécessaires pour créer une balise personnalisée :

Fonction d'analyse de la balise

  • Cette fonction callable PHP analyse la syntaxe de la balise Latte ({...}) dans le template source.
  • Elle reçoit des informations sur la balise (comme son nom, sa position et s'il s'agit d'un n:attribut) via l'objet Latte\Compiler\Tag.
  • Son outil principal pour analyser les arguments et les expressions à l'intérieur des délimiteurs de la balise est l'objet Latte\Compiler\TagParser, accessible via $tag->parser (c'est un parseur différent de celui qui analyse l'ensemble du template).
  • Pour les balises appariées, elle utilise yield pour signaler à Latte d'analyser le contenu interne entre la balise ouvrante et la balise fermante.
  • L'objectif final de la fonction d'analyse est de créer et de retourner une instance de la classe de nœud, qui est ajoutée à l'AST.
  • Il est courant (bien que non obligatoire) d'implémenter la fonction d'analyse comme une méthode statique (souvent nommée create) directement dans la classe de nœud correspondante. Cela maintient la logique d'analyse et la représentation du nœud proprement regroupées, permet l'accès aux éléments privés/protégés de la classe si nécessaire, et améliore l'organisation.

Classe de nœud

  • Représente la fonction logique de votre balise dans l'Arbre Syntaxique Abstrait (AST).
  • Contient les informations analysées (comme les arguments ou le contenu) en tant que propriétés publiques. Ces propriétés contiennent souvent d'autres instances de Node (par exemple, ExpressionNode pour les arguments analysés, AreaNode pour le contenu analysé).
  • La méthode print(PrintContext $context): string génère le code PHP (une instruction ou une série d'instructions) qui exécute l'action de la balise pendant le rendu du template.
  • La méthode getIterator(): \Generator expose les nœuds enfants (arguments, contenu) pour le parcours par les passes de compilation. Elle doit fournir des références (&) pour permettre aux passes de potentiellement modifier ou remplacer les sous-nœuds.
  • Une fois que l'ensemble du template est analysé en AST, Latte exécute une série de passes de compilation. Ces passes parcourent l'ensemble de l'AST en utilisant la méthode getIterator() fournie par chaque nœud. Elles peuvent inspecter les nœuds, collecter des informations et même modifier l'arbre (par exemple, en changeant les propriétés publiques des nœuds ou en remplaçant complètement des nœuds). Cette conception, nécessitant un getIterator() complet, est cruciale. Elle permet à des fonctionnalités puissantes comme Sandbox d'analyser et de potentiellement modifier le comportement de n'importe quelle partie du template, y compris vos propres balises, garantissant la sécurité et la cohérence.

Enregistrement via une extension

  • Vous devez informer Latte de votre nouvelle balise et de la fonction d'analyse à utiliser pour celle-ci. Cela se fait dans le cadre d'une extension Latte.
  • À l'intérieur de votre classe d'extension, vous implémentez la méthode getTags(): array. Cette méthode retourne un tableau associatif où les clés sont les noms des balises (par exemple, 'mytag', 'n:myattribute') et les valeurs sont les fonctions PHP callable représentant leurs fonctions d'analyse respectives (par exemple, MyNamespace\DatetimeNode::create(...)).

Résumé : La fonction d'analyse de la balise transforme le code source du template de votre balise en un nœud AST. La classe de nœud peut ensuite transformer elle-même en code PHP exécutable pour le template compilé et expose ses sous-nœuds pour les passes de compilation via getIterator(). L'enregistrement via une extension lie le nom de la balise à la fonction d'analyse et en informe Latte.

Explorons maintenant comment implémenter ces composants étape par étape.

Création d'une balise simple

Lançons-nous dans la création de votre première balise Latte personnalisée. Nous commencerons par un exemple très simple : une balise nommée {datetime} qui affiche la date et l'heure actuelles. Initialement, cette balise n'acceptera aucun argument, mais nous l'améliorerons plus tard dans la section “Analyse des arguments de la balise”. Elle n'a pas non plus de contenu interne.

Cet exemple vous guidera à travers les étapes de base : définir la classe de nœud, implémenter ses méthodes print() et getIterator(), créer la fonction d'analyse, et enfin enregistrer la balise.

Objectif : Implémenter {datetime} pour afficher la date et l'heure actuelles en utilisant la fonction PHP date().

Création de la classe de nœud

Tout d'abord, nous avons besoin d'une classe pour représenter notre balise dans l'Arbre Syntaxique Abstrait (AST). Comme discuté précédemment, nous héritons de Latte\Compiler\Nodes\StatementNode.

Créez un fichier (par exemple, DatetimeNode.php) et définissez la classe :

<?php

namespace App\Latte;

use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\PrintContext;
use Latte\Compiler\Tag;

class DatetimeNode extends StatementNode
{
	/**
	 * Fonction d'analyse de la balise, appelée lorsque {datetime} est trouvé.
	 */
	public static function create(Tag $tag): self
	{
		// Notre balise simple n'accepte actuellement aucun argument, nous n'avons donc rien à analyser
		$node = $tag->node = new self;
		return $node;
	}

	/**
	 * Génère le code PHP qui sera exécuté lors du rendu du template.
	 */
	public function print(PrintContext $context): string
	{
		return $context->format(
			'echo date(\'Y-m-d H:i:s\') %line;',
			$this->position,
		);
	}

	/**
	 * Fournit l'accès aux nœuds enfants pour les passes de compilation de Latte.
	 */
	public function &getIterator(): \Generator
	{
		false && yield;
	}
}

Lorsque Latte rencontre {datetime} dans un template, il appelle la fonction d'analyse create(). Sa tâche est de retourner une instance de DatetimeNode.

La méthode print() génère le code PHP qui sera exécuté lors du rendu du template. Nous appelons la méthode $context->format(), qui assemble la chaîne de code PHP résultante pour le template compilé. Le premier argument, 'echo date('Y-m-d H:i:s') %line;', est un masque dans lequel les paramètres suivants sont insérés. Le placeholder %line indique à la méthode format() d'utiliser le deuxième argument, qui est $this->position, et d'insérer un commentaire comme /* line 15 */, qui relie le code PHP généré à la ligne originale du template, ce qui est crucial pour le débogage.

La propriété $this->position est héritée de la classe de base Node et est automatiquement définie par le parseur Latte. Elle contient un objet Latte\Compiler\Position qui indique où la balise a été trouvée dans le fichier source .latte.

La méthode getIterator() est cruciale pour les passes de compilation. Elle doit fournir tous les nœuds enfants, mais notre simple DatetimeNode n'a actuellement ni arguments ni contenu, donc pas de nœuds enfants. Cependant, la méthode doit toujours exister et être un générateur, c'est-à-dire que le mot-clé yield doit être présent d'une manière ou d'une autre dans le corps de la méthode.

Enregistrement via une extension

Enfin, informons Latte de la nouvelle balise. Créez une classe d'extension (par exemple, MyLatteExtension.php) et enregistrez la balise dans sa méthode getTags().

<?php

namespace App\Latte;

use Latte\Extension;

class MyLatteExtension extends Extension
{
	/**
	 * Retourne la liste des balises fournies par cette extension.
	 * @return array<string, callable> Map: 'nom-balise' => fonction-analyse
	 */
	public function getTags(): array
	{
		return [
			'datetime' => DatetimeNode::create(...),
			// Enregistrez plus de balises ici plus tard
		];
	}
}

Ensuite, enregistrez cette extension dans le moteur Latte :

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

Créez un template :

<p>Page générée : {datetime}</p>

Sortie attendue : <p>Page générée : 2023-10-27 11:00:00</p>

Résumé de cette phase

Nous avons créé avec succès une balise personnalisée de base {datetime}. Nous avons défini sa représentation dans l'AST (DatetimeNode), géré son analyse (create()), spécifié comment il devait générer le code PHP (print()), assuré que ses enfants sont accessibles pour le parcours (getIterator()), et l'avons enregistré dans Latte.

Dans la section suivante, nous améliorerons cette balise pour accepter des arguments et montrerons comment analyser les expressions et gérer les nœuds enfants.

Analyse des arguments de la balise

Notre balise simple {datetime} fonctionne, mais n'est pas très flexible. Améliorons-la pour qu'elle accepte un argument optionnel : une chaîne de formatage pour la fonction date(). La syntaxe requise sera {datetime $format}.

Objectif : Modifier {datetime} pour accepter une expression PHP optionnelle comme argument, qui sera utilisée comme chaîne de formatage pour date().

Présentation de TagParser

Avant de modifier le code, il est important de comprendre l'outil que nous allons utiliser Latte\Compiler\TagParser. Lorsque le parseur principal de Latte (TemplateParser) rencontre une balise Latte comme {datetime ...} ou un n:attribut, il délègue l'analyse du contenu à l'intérieur de la balise (la partie entre { et } ou la valeur de l'attribut) à un TagParser spécialisé.

Ce TagParser travaille exclusivement avec les arguments de la balise. Sa tâche est de traiter les tokens représentant ces arguments. Il est crucial qu'il doive traiter tout le contenu qui lui est fourni. Si votre fonction d'analyse se termine mais que le TagParser n'a pas atteint la fin des arguments (vérifié via $tag->parser->isEnd()), Latte lèvera une exception, car cela indique qu'il reste des tokens inattendus à l'intérieur de la balise. Inversement, si une balise nécessite des arguments, vous devriez appeler $tag->expectArguments() au début de votre fonction d'analyse. Cette méthode vérifie si des arguments sont présents et lève une exception utile si la balise a été utilisée sans aucun argument.

TagParser offre des méthodes utiles pour analyser différents types d'arguments :

  • parseExpression(): ExpressionNode: Analyse une expression de type PHP (variables, littéraux, opérateurs, appels de fonctions/méthodes, etc.). Gère le sucre syntaxique de Latte, comme le traitement des chaînes alphanumériques simples comme des chaînes entre guillemets (par exemple, foo est analysé comme s'il s'agissait de 'foo').
  • parseUnquotedStringOrExpression(): ExpressionNode: Analyse soit une expression standard, soit une chaîne non guillemetée. Les chaînes non guillemetées sont des séquences autorisées par Latte sans guillemets, souvent utilisées pour des choses comme les chemins de fichiers (par exemple, {include ../file.latte}). S'il analyse une chaîne non guillemetée, il retourne un StringNode.
  • parseArguments(): ArrayNode: Analyse les arguments séparés par des virgules, potentiellement avec des clés, comme 10, name: 'John', true.
  • parseModifier(): ModifierNode: Analyse les filtres comme |upper|truncate:10.
  • parseType(): ?SuperiorTypeNode: Analyse les indications de type PHP comme int, ?string, array|Foo.

Pour des besoins d'analyse plus complexes ou de bas niveau, vous pouvez interagir directement avec le flux de tokens via $tag->parser->stream. Cet objet fournit des méthodes pour inspecter et traiter les tokens individuels :

  • $tag->parser->stream->is(...): bool: Vérifie si le token actuel correspond à l'un des types spécifiés (par exemple, Token::Php_Variable) ou des valeurs littérales (par exemple, 'as') sans le consommer. Utile pour regarder en avant.
  • $tag->parser->stream->consume(...): Token: Consomme le token actuel et avance la position du flux. Si des types/valeurs de tokens attendus sont fournis comme arguments et que le token actuel ne correspond pas, lève une CompileException. Utilisez ceci lorsque vous attendez un certain token.
  • $tag->parser->stream->tryConsume(...): ?Token: Tente de consommer le token actuel uniquement si il correspond à l'un des types/valeurs spécifiés. S'il correspond, consomme le token et le retourne. S'il ne correspond pas, laisse la position du flux inchangée et retourne null. Utilisez ceci pour les tokens optionnels ou lorsque vous choisissez entre différentes voies syntaxiques.

Mise à jour de la fonction d'analyse create()

Avec cette compréhension, modifions la méthode create() dans DatetimeNode pour analyser l'argument de format optionnel en utilisant $tag->parser.

<?php

namespace App\Latte;

use Latte\Compiler\Nodes\Php\ExpressionNode;
use Latte\Compiler\Nodes\Php\Scalar\StringNode;
use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\PrintContext;
use Latte\Compiler\Tag;

class DatetimeNode extends StatementNode
{
	// Ajoutons une propriété publique pour stocker le nœud d'expression de format analysé
	public ?ExpressionNode $format = null;

	public static function create(Tag $tag): self
	{
		$node = $tag->node = new self;

		// Vérifions s'il y a des tokens
		if (!$tag->parser->isEnd()) {
			// Analysons l'argument comme une expression de type PHP en utilisant TagParser.
			$node->format = $tag->parser->parseExpression();
		}

		return $node;
	}

	// ... les méthodes print() et getIterator() seront mises à jour ensuite ...
}

Nous avons ajouté une propriété publique $format. Dans create(), nous utilisons maintenant $tag->parser->isEnd() pour vérifier si des arguments existent. Si c'est le cas, $tag->parser->parseExpression() traite les tokens pour l'expression. Étant donné que TagParser doit traiter tous les tokens d'entrée, Latte lèvera automatiquement une erreur si l'utilisateur écrit quelque chose d'inattendu après l'expression de format (par exemple, {datetime 'Y-m-d', unexpected}).

Mise à jour de la méthode print()

Modifions maintenant la méthode print() pour utiliser l'expression de format analysée stockée dans $this->format. Si aucun format n'a été fourni ($this->format est null), nous devrions utiliser une chaîne de formatage par défaut, par exemple 'Y-m-d H:i:s'.

	public function print(PrintContext $context): string
	{
		$formatNode = $this->format ?? new StringNode('Y-m-d H:i:s');

		// %node imprime la représentation en code PHP de $formatNode.
		return $context->format(
			'echo date(%node) %line;',
			$formatNode,
			$this->position
		);
	}

Dans la variable $formatNode, nous stockons le nœud AST représentant la chaîne de formatage pour la fonction PHP date(). Nous utilisons ici l'opérateur de coalescence nulle (??). Si l'utilisateur a fourni un argument dans le template (par exemple, {datetime 'd.m.Y'}), alors la propriété $this->format contient le nœud correspondant (dans ce cas, un StringNode avec la valeur 'd.m.Y'), et ce nœud est utilisé. Si l'utilisateur n'a pas fourni d'argument (il a juste écrit {datetime}), la propriété $this->format est null, et à la place, nous créons un nouveau StringNode avec le format par défaut 'Y-m-d H:i:s'. Cela garantit que $formatNode contient toujours un nœud AST valide pour le format.

Dans le masque 'echo date(%node) %line;', un nouveau placeholder %node est utilisé, qui indique à la méthode format() de prendre le premier argument suivant (qui est notre $formatNode), d'appeler sa méthode print() (qui retournera sa représentation en code PHP) et d'insérer le résultat à la position du placeholder.

Implémentation de getIterator() pour les sous-nœuds

Notre DatetimeNode a maintenant un nœud enfant : l'expression $format. Nous devons rendre ce nœud enfant accessible aux passes de compilation en le fournissant dans la méthode getIterator(). N'oubliez pas de fournir une référence (&) pour permettre aux passes de potentiellement remplacer le nœud.

	public function &getIterator(): \Generator
	{
		if ($this->format) {
			yield $this->format;
		}
	}

Pourquoi est-ce crucial ? Imaginez une passe Sandbox qui doit vérifier si l'argument $format ne contient pas un appel de fonction interdit (par exemple, {datetime dangerousFunction()}). Si getIterator() ne fournit pas $this->format, la passe Sandbox ne verrait jamais l'appel dangerousFunction() à l'intérieur de l'argument de notre balise, créant ainsi une faille de sécurité potentielle. En le fournissant, nous permettons à Sandbox (et aux autres passes) d'inspecter et potentiellement de modifier le nœud d'expression $format.

Utilisation de la balise améliorée

La balise gère maintenant correctement l'argument optionnel :

Format par défaut : {datetime}
Format personnalisé : {datetime 'd.m.Y'}
Utilisation d'une variable : {datetime $userDateFormatPreference}

{* Ceci provoquerait une erreur après l'analyse de 'd.m.Y', car ", foo" est inattendu *}
{* {datetime 'd.m.Y', foo} *}

Ensuite, nous examinerons la création de balises appariées qui traitent le contenu entre elles.

Gestion des balises appariées

Jusqu'à présent, notre balise {datetime} était auto-fermante (conceptuellement). Elle n'a pas de contenu entre une balise ouvrante et une balise fermante. Cependant, de nombreuses balises utiles fonctionnent avec un bloc de contenu de template. Celles-ci sont appelées balises appariées. Les exemples incluent {if}...{/if}, {block}...{/block} ou une balise personnalisée que nous allons créer maintenant : {debug}...{/debug}.

Cette balise nous permettra d'inclure des informations de débogage dans nos templates, qui ne devraient être visibles qu'en cours de développement.

Objectif : Créer une balise appariée {debug} dont le contenu n'est rendu que lorsqu'un indicateur spécifique de “mode développement” est actif.

Présentation des fournisseurs

Parfois, vos balises ont besoin d'accéder à des données ou des services qui ne sont pas passés directement comme paramètres de template. Par exemple, déterminer si l'application est en mode développement, accéder à l'objet utilisateur ou obtenir des valeurs de configuration. Latte fournit un mécanisme appelé fournisseurs (Providers) à cet effet.

Les fournisseurs sont enregistrés dans votre extension en utilisant la méthode getProviders(). Cette méthode retourne un tableau associatif où les clés sont les noms sous lesquels les fournisseurs seront accessibles dans le code d'exécution du template, et les valeurs sont les données ou objets réels.

À l'intérieur du code PHP généré par la méthode print() de votre balise, vous pouvez accéder à ces fournisseurs via la propriété spéciale de l'objet $this->global. Comme cette propriété est partagée entre toutes les extensions, il est de bonne pratique de préfixer les noms de vos fournisseurs pour éviter les collisions de noms potentielles avec les fournisseurs clés de Latte ou les fournisseurs d'autres extensions tierces. Une convention courante consiste à utiliser un préfixe court et unique lié à votre fournisseur ou au nom de l'extension. Pour notre exemple, nous utiliserons le préfixe app et l'indicateur de mode développement sera disponible sous $this->global->appDevMode.

Le mot-clé yield pour l'analyse du contenu

Comment disons-nous au parseur Latte de traiter le contenu entre {debug} et {/debug} ? C'est là qu'intervient le mot-clé yield.

Lorsque yield est utilisé dans la fonction create(), la fonction devient un générateur PHP. Son exécution est suspendue et le contrôle est rendu au TemplateParser principal. Le TemplateParser continue alors à analyser le contenu du template jusqu'à ce qu'il rencontre la balise fermante correspondante ({/debug} dans notre cas).

Une fois la balise fermante trouvée, le TemplateParser reprend l'exécution de notre fonction create() juste après l'instruction yield. La valeur retournée par l'instruction yield est un tableau contenant deux éléments :

  1. Un AreaNode représentant le contenu analysé entre les balises ouvrante et fermante.
  2. L'objet Tag représentant la balise fermante (par exemple, {/debug}).

Créons la classe DebugNode et sa méthode create utilisant yield.

<?php

namespace App\Latte;

use Latte\Compiler\Nodes\AreaNode;
use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\PrintContext;
use Latte\Compiler\Tag;

class DebugNode extends StatementNode
{
	// Propriété publique pour stocker le contenu interne analysé
	public AreaNode $content;

	/**
	 * Fonction d'analyse pour la balise appariée {debug} ... {/debug}.
	 */
	public static function create(Tag $tag): \Generator // notez le type de retour
	{
		$node = $tag->node = new self;

		// Suspendre l'analyse, obtenir le contenu interne et la balise fermante lorsque {/debug} est trouvé
		[$node->content, $endTag] = yield;

		return $node;
	}

	// ... print() et getIterator() seront implémentés ensuite ...
}

Note : $endTag est null si la balise est utilisée comme n:attribut, c'est-à-dire <div n:debug>...</div>.

Implémentation de print() pour le rendu conditionnel

La méthode print() doit maintenant générer du code PHP qui vérifie le fournisseur appDevMode à l'exécution et n'exécute le code pour le contenu interne que si l'indicateur est vrai.

	public function print(PrintContext $context): string
	{
		// Génère une instruction PHP 'if' qui vérifie le fournisseur à l'exécution
		return $context->format(
			<<<'XX'
				if ($this->global->appDevMode) %line {
					// Si en mode développement, imprime le contenu interne
					%node
				}

				XX,
			$this->position, // Pour le commentaire %line
			$this->content,  // Le nœud contenant l'AST du contenu interne
		);
	}

C'est simple. Nous utilisons PrintContext::format() pour créer une instruction PHP if standard. À l'intérieur de l'if, nous plaçons le placeholder %node pour $this->content. Latte appellera récursivement $this->content->print($context) pour générer le code PHP pour la partie interne de la balise, mais seulement si $this->global->appDevMode est évalué à vrai à l'exécution.

Implémentation de getIterator() pour le contenu

Comme pour le nœud d'argument dans l'exemple précédent, notre DebugNode a maintenant un nœud enfant : AreaNode $content. Nous devons le rendre accessible en le fournissant dans getIterator() :

	public function &getIterator(): \Generator
	{
		// Fournit une référence au nœud de contenu
		yield $this->content;
	}

Cela permet aux passes de compilation de descendre dans le contenu de notre balise {debug}, ce qui est important même si le contenu est rendu conditionnellement. Par exemple, Sandbox doit analyser le contenu, que appDevMode soit vrai ou faux.

Enregistrement et utilisation

Enregistrez la balise et le fournisseur dans votre extension :

class MyLatteExtension extends Extension
{
	// Supposons que $isDevelopmentMode est déterminé quelque part (par ex. depuis la configuration)
	public function __construct(
		private bool $isDevelopmentMode,
	) {
	}

	public function getTags(): array
	{
		return [
			'datetime' => DatetimeNode::create(...),
			'debug' => DebugNode::create(...), // Enregistrement de la nouvelle balise
		];
	}

	public function getProviders(): array
	{
		return [
			'appDevMode' => $this->isDevelopmentMode, // Enregistrement du fournisseur
		];
	}
}

// Lors de l'enregistrement de l'extension :
$isDev = true; // Déterminez ceci en fonction de l'environnement de votre application
$latte->addExtension(new App\Latte\MyLatteExtension($isDev));

Et son utilisation dans le template :

<p>Contenu normal toujours visible.</p>

{debug}
	<div class="debug-panel">
		ID de l'utilisateur actuel : {$user->id}
		Heure de la requête : {=time()}
	</div>
{/debug}

<p>Autre contenu normal.</p>

Intégration des n:attributs

Latte offre une notation abrégée pratique pour de nombreuses balises appariées : les n:attributs. Si vous avez une balise appariée comme {tag}...{/tag} et que vous souhaitez que son effet s'applique directement à un seul élément HTML, vous pouvez souvent l'écrire de manière plus concise comme un attribut n:tag sur cet élément.

Pour la plupart des balises appariées standard que vous définissez (comme notre {debug}), Latte activera automatiquement la version d'attribut n: correspondante. Vous n'avez rien à faire de plus lors de l'enregistrement :

{* Utilisation standard de la balise appariée *}
{debug}<div>Informations de débogage</div>{/debug}

{* Utilisation équivalente avec n:attribut *}
<div n:debug>Informations de débogage</div>

Les deux versions rendront le <div> uniquement si $this->global->appDevMode est vrai. Les préfixes inner- et tag- fonctionnent également comme prévu.

Parfois, la logique de votre balise peut avoir besoin de se comporter légèrement différemment selon qu'elle est utilisée comme une balise appariée standard ou comme un n:attribut, ou si un préfixe comme n:inner-tag ou n:tag-tag est utilisé. L'objet Latte\Compiler\Tag, passé à votre fonction d'analyse create(), fournit ces informations :

  • $tag->isNAttribute(): bool: Retourne true si la balise est analysée comme un n:attribut
  • $tag->prefix: ?string: Retourne le préfixe utilisé avec le n:attribut, qui peut être null (pas un n:attribut), Tag::PrefixNone, Tag::PrefixInner ou Tag::PrefixTag

Maintenant que nous comprenons les balises simples, l'analyse des arguments, les balises appariées, les fournisseurs et les n:attributs, abordons un scénario plus complexe impliquant des balises imbriquées dans d'autres balises, en utilisant notre balise {debug} comme point de départ.

Balises intermédiaires

Certaines balises appariées permettent ou même nécessitent que d'autres balises apparaissent à l'intérieur d'elles avant la balise fermante finale. Celles-ci sont appelées balises intermédiaires. Les exemples classiques incluent {if}...{elseif}...{else}...{/if} ou {switch}...{case}...{default}...{/switch}.

Étendons notre balise {debug} pour prendre en charge une clause {else} optionnelle, qui sera rendue lorsque l'application n'est pas en mode développement.

Objectif : Modifier {debug} pour prendre en charge une balise intermédiaire optionnelle {else}. La syntaxe finale devrait être {debug} ... {else} ... {/debug}.

Analyse des balises intermédiaires avec yield

Nous savons déjà que yield suspend la fonction d'analyse create() et retourne le contenu analysé ainsi que la balise fermante. Cependant, yield offre plus de contrôle : vous pouvez lui fournir un tableau de noms de balises intermédiaires. Lorsque le parseur rencontre l'une de ces balises spécifiées au même niveau d'imbrication (c'est-à-dire comme enfants directs de la balise parente, pas à l'intérieur d'autres blocs ou balises à l'intérieur), il arrête également l'analyse.

Lorsque l'analyse s'arrête à cause d'une balise intermédiaire, elle arrête d'analyser le contenu, reprend le générateur create() et renvoie le contenu partiellement analysé et la balise intermédiaire elle-même (au lieu de la balise fermante finale). Notre fonction create() peut alors traiter cette balise intermédiaire (par exemple, analyser ses arguments si elle en avait) et utiliser à nouveau yield pour analyser la partie suivante du contenu jusqu'à la balise fermante finale ou une autre balise intermédiaire attendue.

Modifions DebugNode::create() pour attendre {else} :

<?php

namespace App\Latte;

use Latte\Compiler\Nodes\AreaNode;
use Latte\Compiler\Nodes\NopNode;
use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\PrintContext;
use Latte\Compiler\Tag;

class DebugNode extends StatementNode
{
	// Contenu pour la partie {debug}
	public AreaNode $thenContent;
	// Contenu optionnel pour la partie {else}
	public ?AreaNode $elseContent = null;

	public static function create(Tag $tag): \Generator
	{
		$node = $tag->node = new self;

		// yield et attendre soit {/debug} soit {else}
		[$node->thenContent, $nextTag] = yield ['else'];

		// Vérifier si la balise à laquelle nous nous sommes arrêtés était {else}
		if ($nextTag?->name === 'else') {
			// Yield à nouveau pour analyser le contenu entre {else} et {/debug}
			[$node->elseContent, $endTag] = yield;
		}

		return $node;
	}

	// ... print() et getIterator() seront mis à jour ensuite ...
}

Maintenant, yield ['else'] dit à Latte d'arrêter l'analyse non seulement pour {/debug}, mais aussi pour {else}. Si {else} est trouvé, $nextTag contiendra l'objet Tag pour {else}. Ensuite, nous utilisons à nouveau yield sans arguments, ce qui signifie que nous n'attendons maintenant que la balise finale {/debug}, et nous stockons le résultat dans $node->elseContent. Si {else} n'a pas été trouvé, $nextTag serait le Tag pour {/debug} (ou null s'il est utilisé comme n:attribut) et $node->elseContent resterait null.

Implémentation de print() avec {else}

La méthode print() doit refléter la nouvelle structure. Elle devrait générer une instruction PHP if/else basée sur le fournisseur devMode.

	public function print(PrintContext $context): string
	{
		return $context->format(
			<<<'XX'
				if ($this->global->appDevMode) %line {
					%node // Code pour la branche 'then' (contenu {debug})
				} else {
					%node // Code pour la branche 'else' (contenu {else})
				}

				XX,
			$this->position,    // Numéro de ligne pour la condition 'if'
			$this->thenContent, // Premier placeholder %node
			$this->elseContent ?? new NopNode, // Deuxième placeholder %node
		);
	}

Ceci est une structure PHP if/else standard. Nous utilisons %node deux fois ; format() remplace les nœuds fournis séquentiellement. Nous utilisons ?? new NopNode pour éviter les erreurs si $this->elseContent est null – NopNode n'imprime simplement rien.

Implémentation de getIterator() pour les deux contenus

Nous avons maintenant potentiellement deux nœuds enfants de contenu ($thenContent et $elseContent). Nous devons fournir les deux s'ils existent :

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

Utilisation de la balise améliorée

La balise peut maintenant être utilisée avec la clause {else} optionnelle :

{debug}
	<p>Affichage des informations de débogage car devMode est ACTIVÉ.</p>
{else}
	<p>Les informations de débogage sont masquées car devMode est DÉSACTIVÉ.</p>
{/debug}

Gestion de l'état et de l'imbrication

Nos exemples précédents ({datetime}, {debug}) étaient relativement sans état dans leurs méthodes print(). Ils imprimaient directement le contenu ou effectuaient une simple vérification conditionnelle basée sur un fournisseur global. Cependant, de nombreuses balises doivent gérer une certaine forme d'état pendant le rendu ou impliquent l'évaluation d'expressions utilisateur qui ne devraient être exécutées qu'une seule fois pour des raisons de performance ou d'exactitude. De plus, nous devons considérer ce qui se passe lorsque nos balises personnalisées sont imbriquées.

Illustrons ces concepts en créant une balise {repeat $count}...{/repeat}. Cette balise répétera son contenu interne $count fois.

Objectif : Implémenter {repeat $count} qui répète son contenu le nombre de fois spécifié.

Le besoin de variables temporaires & uniques

Imaginez que l'utilisateur écrive :

{repeat rand(1, 5)} Contenu {/repeat}

Si nous générions naïvement une boucle PHP for de cette manière dans notre méthode print() :

// Code généré simplifié et INCORRECT
for ($i = 0; $i < rand(1, 5); $i++) {
	// imprimer le contenu
}

Ce serait faux ! L'expression rand(1, 5) serait réévaluée à chaque itération de la boucle, conduisant à un nombre imprévisible de répétitions. Nous devons évaluer l'expression $count une fois avant le début de la boucle et stocker son résultat.

Nous générerons du code PHP qui évalue d'abord l'expression de comptage et la stocke dans une variable temporaire d'exécution. Pour éviter les collisions avec les variables définies par l'utilisateur du template et les variables internes de Latte (comme $ʟ_...), nous utiliserons la convention de préfixer nos variables temporaires par $__ (double soulignement).

Le code généré ressemblerait alors à ceci :

$__count = rand(1, 5);
for ($__i = 0; $__i < $__count; $__i++) {
	// imprimer le contenu
}

Considérons maintenant l'imbrication :

{repeat $countA}       {* Boucle externe *}
	{repeat $countB}   {* Boucle interne *}
		...
	{/repeat}
{/repeat}

Si les balises {repeat} externe et interne généraient du code utilisant les mêmes noms de variables temporaires (par exemple, $__count et $__i), la boucle interne écraserait les variables de la boucle externe, brisant la logique.

Nous devons nous assurer que les variables temporaires générées pour chaque instance de la balise {repeat} sont uniques. Nous y parvenons en utilisant PrintContext::generateId(). Cette méthode retourne un entier unique pendant la phase de compilation. Nous pouvons ajouter cet ID aux noms de nos variables temporaires.

Ainsi, au lieu de $__count, nous générerons $__count_1 pour la première balise repeat, $__count_2 pour la deuxième, etc. De même, pour le compteur de boucle, nous utiliserons $__i_1, $__i_2, etc.

Implémentation de RepeatNode

Créons la classe de nœud.

<?php

namespace App\Latte;

use Latte\CompileException;
use Latte\Compiler\Nodes\AreaNode;
use Latte\Compiler\Nodes\Php\ExpressionNode;
use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\PrintContext;
use Latte\Compiler\Tag;

class RepeatNode extends StatementNode
{
	public ExpressionNode $count;
	public AreaNode $content;

	/**
	 * Fonction d'analyse pour {repeat $count} ... {/repeat}
	 */
	public static function create(Tag $tag): \Generator
	{
		$tag->expectArguments(); // s'assure que $count est fourni
		$node = $tag->node = new self;
		// Analyse l'expression de comptage
		$node->count = $tag->parser->parseExpression();
		// Obtention du contenu interne
		[$node->content] = yield;
		return $node;
	}

	/**
	 * Génère une boucle PHP 'for' avec des noms de variables uniques.
	 */
	public function print(PrintContext $context): string
	{
		// Génération de noms de variables uniques
		$id = $context->generateId();
		$countVar = '$__count_' . $id; // par ex. $__count_1, $__count_2, etc.
		$iteratorVar = '$__i_' . $id;  // par ex. $__i_1, $__i_2, etc.

		return $context->format(
			<<<'XX'
				// Évaluation de l'expression de comptage *une fois* et stockage
				%raw = (int) (%node);
				// Boucle utilisant le comptage stocké et une variable d'itération unique
				for (%raw = 0; %2.raw < %0.raw; %2.raw++) %line {
					%node // Rendu du contenu interne
				}

				XX,
			$countVar,          // %0 - Variable pour stocker le comptage
			$this->count,       // %1 - Nœud d'expression pour le comptage
			$iteratorVar,       // %2 - Nom de la variable d'itération de la boucle
			$this->position,    // %3 - Commentaire avec le numéro de ligne pour la boucle elle-même
			$this->content      // %4 - Nœud du contenu interne
		);
	}

	/**
	 * Fournit les nœuds enfants (expression de comptage et contenu).
	 */
	public function &getIterator(): \Generator
	{
		yield $this->count;
		yield $this->content;
	}
}

La méthode create() analyse l'expression $count requise en utilisant parseExpression(). D'abord, $tag->expectArguments() est appelé. Cela garantit que l'utilisateur a fourni quelque chose après {repeat}. Bien que $tag->parser->parseExpression() échouerait si rien n'était fourni, le message d'erreur pourrait concerner une syntaxe inattendue. L'utilisation de expectArguments() fournit une erreur beaucoup plus claire, indiquant spécifiquement que les arguments manquent pour la balise {repeat}.

La méthode print() génère le code PHP responsable de l'exécution de la logique de répétition à l'exécution. Elle commence par générer des noms uniques pour les variables PHP temporaires dont elle aura besoin.

La méthode $context->format() est appelée avec un nouveau placeholder %raw, qui insère la chaîne brute fournie comme argument correspondant. Ici, elle insère le nom de variable unique stocké dans $countVar (par exemple, $__count_1). Et qu'en est-il de %0.raw et %2.raw ? Ceci démontre les placeholders positionnels. Au lieu de simplement %raw, qui prend le prochain argument brut disponible, %2.raw prend explicitement l'argument à l'index 2 (qui est $iteratorVar) et insère sa valeur de chaîne brute. Cela nous permet de réutiliser la chaîne $iteratorVar sans la passer plusieurs fois dans la liste d'arguments de format().

Cet appel format() soigneusement construit génère une boucle PHP efficace et sûre qui gère correctement l'expression de comptage et évite les collisions de noms de variables même lorsque les balises {repeat} sont imbriquées.

Enregistrement et utilisation

Enregistrez la balise dans votre extension :

use App\Latte\RepeatNode;

class MyLatteExtension extends Extension
{
	public function getTags(): array
	{
		return [
			'datetime' => DatetimeNode::create(...),
			'debug' => DebugNode::create(...),
			'repeat' => RepeatNode::create(...), // Enregistrement de la balise repeat
		];
	}
}

Utilisez-la dans le template, y compris l'imbrication :

{var $rows = rand(5, 7)}
{var $cols = rand(3, 5)}

{repeat $rows}
	<tr>
		{repeat $cols}
			<td>Boucle interne</td>
		{/repeat}
	</tr>
{/repeat}

Cet exemple démontre comment gérer l'état (compteurs de boucle) et les problèmes potentiels d'imbrication en utilisant des variables temporaires préfixées par $__ et rendues uniques avec un ID de PrintContext::generateId().

n:attributs purs

Alors que de nombreux n:attributs comme n:if ou n:foreach servent de raccourcis pratiques pour leurs homologues de balises appariées ({if}...{/if}, {foreach}...{/foreach}), Latte permet également de définir des balises qui n'existent que sous forme de n:attribut. Ceux-ci sont souvent utilisés pour modifier les attributs ou le comportement de l'élément HTML auquel ils sont attachés.

Les exemples standard intégrés à Latte incluent n:class, qui aide à construire dynamiquement l'attribut class, et n:attr, qui peut définir plusieurs attributs arbitraires.

Créons notre propre n:attribut pur : n:confirm, qui ajoutera une boîte de dialogue de confirmation JavaScript avant d'exécuter une action (comme suivre un lien ou soumettre un formulaire).

Objectif : Implémenter n:confirm="'Êtes-vous sûr ?'" qui ajoute un gestionnaire onclick pour empêcher l'action par défaut si l'utilisateur annule la boîte de dialogue de confirmation.

Implémentation de ConfirmNode

Nous avons besoin d'une classe Node et d'une fonction d'analyse.

<?php

namespace App\Latte;

use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\PrintContext;
use Latte\Compiler\Tag;
use Latte\Compiler\Nodes\Php\ExpressionNode;
use Latte\Compiler\Nodes\Php\Scalar\StringNode;

class ConfirmNode extends StatementNode
{
	public ExpressionNode $message;

	public static function create(Tag $tag): self
	{
		$tag->expectArguments();
		$node = $tag->node = new self;
		$node->message = $tag->parser->parseExpression();
		return $node;
	}

	/**
	 * Génère le code de l'attribut 'onclick' avec un échappement correct.
	 */
	public function print(PrintContext $context): string
	{
		// Assure un échappement correct pour les contextes d'attribut JavaScript et HTML.
		return $context->format(
			<<<'XX'
				echo ' onclick="', LR\Filters::escapeHtmlAttr('return confirm(' . LR\Filters::escapeJs(%node) . ')'), '"' %line;
				XX,
			$this->message,
			$this->position,
		);
	}

	public function &getIterator(): \Generator
	{
		yield $this->message;
	}
}

La méthode print() génère du code PHP qui finira par imprimer l'attribut HTML onclick="..." pendant le rendu du template. La gestion des contextes imbriqués (JavaScript à l'intérieur d'un attribut HTML) nécessite un échappement soigneux. Le filtre LR\Filters::escapeJs(%node) est appelé à l'exécution et échappe correctement le message pour une utilisation à l'intérieur de JavaScript (la sortie serait comme "Sure?"). Ensuite, le filtre LR\Filters::escapeHtmlAttr(...) échappe les caractères qui sont spéciaux dans les attributs HTML, ce qui changerait la sortie en return confirm(&quot;Sure?&quot;). Cet échappement d'exécution en deux étapes garantit que le message est sûr pour JavaScript et que le code JavaScript résultant est sûr pour être intégré dans l'attribut HTML onclick.

Enregistrement et utilisation

Enregistrez le n:attribut dans votre extension. N'oubliez pas le préfixe n: dans la clé :

class MyLatteExtension extends Extension
{
	public function getTags(): array
	{
		return [
			'datetime' => DatetimeNode::create(...),
			'debug' => DebugNode::create(...),
			'repeat' => RepeatNode::create(...),
			'n:confirm' => ConfirmNode::create(...), // Enregistrement de n:confirm
		];
	}
}

Vous pouvez maintenant utiliser n:confirm sur des liens, des boutons ou des éléments de formulaire :

<a href="delete.php?id=123" n:confirm='"Voulez-vous vraiment supprimer l\'élément {$id} ?"'>Supprimer</a>

HTML généré :

<a href="delete.php?id=123" onclick="return confirm(&quot;Voulez-vous vraiment supprimer l'élément 123 ?&quot;)">Supprimer</a>

Lorsque l'utilisateur clique sur le lien, le navigateur exécute le code onclick, affiche la boîte de dialogue de confirmation et ne navigue vers delete.php que si l'utilisateur clique sur “OK”.

Cet exemple démontre comment un n:attribut pur peut être créé pour modifier le comportement ou les attributs de son élément HTML hôte en générant le code PHP approprié dans sa méthode print(). N'oubliez pas le double échappement souvent requis : une fois pour le contexte cible (JavaScript dans ce cas) et à nouveau pour le contexte de l'attribut HTML.

Sujets avancés

Alors que les sections précédentes couvrent les concepts fondamentaux, voici quelques sujets plus avancés que vous pourriez rencontrer lors de la création de balises Latte personnalisées.

Modes de sortie des balises

L'objet Tag passé à votre fonction create() a une propriété outputMode. Cette propriété affecte la manière dont Latte traite les espaces et l'indentation environnants, en particulier lorsqu'une balise est utilisée sur sa propre ligne. Vous pouvez modifier cette propriété dans votre fonction create().

  • Tag::OutputKeepIndentation (Par défaut pour la plupart des balises comme {=...}) : Latte essaie de préserver l'indentation avant la balise. Les nouvelles lignes après la balise sont généralement préservées. Ceci convient aux balises qui impriment du contenu en ligne.
  • Tag::OutputRemoveIndentation (Par défaut pour les balises de bloc comme {if}, {foreach}) : Latte supprime l'indentation initiale et potentiellement une nouvelle ligne suivante. Cela aide à garder le code PHP généré plus propre et évite les lignes vides supplémentaires dans la sortie HTML causées par la balise elle-même. Utilisez ceci pour les balises qui représentent des structures de contrôle ou des blocs qui ne devraient pas ajouter d'espaces eux-mêmes.
  • Tag::OutputNone (Utilisé par des balises comme {var}, {default}) : Similaire à RemoveIndentation, mais signale plus fortement que la balise elle-même ne produit pas de sortie directe, affectant potentiellement le traitement des espaces autour d'elle de manière encore plus agressive. Convient aux balises déclaratives ou de configuration.

Choisissez le mode qui correspond le mieux à l'objectif de votre balise. Pour la plupart des balises structurelles ou de contrôle, OutputRemoveIndentation est généralement approprié.

Accès aux balises parentes/les plus proches

Parfois, le comportement d'une balise doit dépendre du contexte dans lequel elle est utilisée, en particulier dans quelle(s) balise(s) parente(s) elle se trouve. L'objet Tag passé à votre fonction create() fournit la méthode closestTag(array $classes, ?callable $condition = null): ?Tag précisément à cet effet.

Cette méthode recherche vers le haut dans la hiérarchie des balises actuellement ouvertes (y compris les éléments HTML représentés en interne pendant l'analyse) et retourne l'objet Tag de l'ancêtre le plus proche qui correspond aux critères spécifiques. Si aucun ancêtre correspondant n'est trouvé, elle retourne null.

Le tableau $classes spécifie le type de balises ancêtres que vous recherchez. Il vérifie si le nœud associé de la balise ancêtre ($ancestorTag->node) est une instance de cette classe.

function create(Tag $tag)
{
	// Recherche de la balise ancêtre la plus proche dont le nœud est une instance de ForeachNode
	$foreachTag = $tag->closestTag([ForeachNode::class]);
	if ($foreachTag) {
		// Nous pouvons accéder à l'instance ForeachNode elle-même :
		$foreachNode = $foreachTag->node;
	}
}

Notez $foreachTag->node : Cela ne fonctionne que parce qu'il est conventionnel dans le développement de balises Latte d'assigner immédiatement le nœud créé à $tag->node dans la méthode create(), comme nous l'avons toujours fait.

Parfois, la simple comparaison du type de nœud ne suffit pas. Vous pourriez avoir besoin de vérifier une propriété spécifique de la balise ancêtre potentielle ou de son nœud. Le deuxième argument optionnel de closestTag() est un callable qui reçoit l'objet Tag de l'ancêtre potentiel et doit retourner s'il s'agit d'une correspondance valide.

function create(Tag $tag)
{
	$dynamicBlockTag = $tag->closestTag(
		[BlockNode::class],
		// Condition : le bloc doit être dynamique
		fn(Tag $blockTag) => $blockTag->node->block->isDynamic(),
	);
}

L'utilisation de closestTag() permet de créer des balises contextuelles et d'imposer une utilisation correcte dans la structure de votre template, conduisant à des templates plus robustes et compréhensibles.

Placeholders PrintContext::format()

Nous avons souvent utilisé PrintContext::format() pour générer du code PHP dans les méthodes print() de nos nœuds. Il accepte une chaîne de masque et des arguments suivants qui remplacent les placeholders dans le masque. Voici un résumé des placeholders disponibles :

  • %node: L'argument doit être une instance de Node. Appelle la méthode print() du nœud et insère la chaîne de code PHP résultante.
  • %dump: L'argument est n'importe quelle valeur PHP. Exporte la valeur en code PHP valide. Convient aux scalaires, tableaux, null.
    • $context->format('echo %dump;', 'Hello')echo 'Hello';
    • $context->format('$arr = %dump;', [1, 2])$arr = [1, 2];
  • %raw: Insère l'argument directement dans le code PHP de sortie sans aucun échappement ni modification. Utilisez avec prudence, principalement pour insérer des fragments de code PHP pré-générés ou des noms de variables.
    • $context->format('%raw = 1;', '$variableName')$variableName = 1;
  • %args: L'argument doit être un Expression\ArrayNode. Imprime les éléments du tableau formatés comme arguments pour un appel de fonction ou de méthode (séparés par des virgules, gère les arguments nommés s'ils sont présents).
    • $argsNode = new ArrayNode([...]);
    • $context->format('myFunc(%args);', $argsNode)myFunc(1, name: 'Joe');
  • %line: L'argument doit être un objet Position (généralement $this->position). Insère un commentaire PHP /* line X */ indiquant le numéro de ligne source.
    • $context->format('echo "Hi" %line;', $this->position)echo "Hi" /* line 42 */;
  • %escape(...): Génère du code PHP qui échappe à l'exécution l'expression interne en utilisant les règles d'échappement contextuelles actuelles.
    • $context->format('echo %escape(%node);', $variableNode)
  • %modify(...): L'argument doit être un ModifierNode. Génère du code PHP qui applique les filtres spécifiés dans le ModifierNode au contenu interne, y compris l'échappement contextuel, sauf si désactivé avec |noescape.
    • $context->format('%modify(%node);', $modifierNode, $variableNode)
  • %modifyContent(...): Similaire à %modify, mais destiné à modifier des blocs de contenu capturé (souvent HTML).

Vous pouvez explicitement faire référence aux arguments par leur index (à partir de zéro) : %0.node, %1.dump, %2.raw, etc. Cela permet de réutiliser un argument plusieurs fois dans le masque sans le passer à plusieurs reprises à format(). Voir l'exemple de la balise {repeat}%0.raw et %2.raw ont été utilisés.

Exemple d'analyse d'arguments complexes

Alors que parseExpression(), parseArguments(), etc., couvrent de nombreux cas, vous avez parfois besoin d'une logique d'analyse plus complexe utilisant le TokenStream de bas niveau disponible via $tag->parser->stream.

Objectif : Créer une balise {embedYoutube $videoID, width: 640, height: 480}. Nous voulons analyser l'ID vidéo requis (chaîne ou variable) suivi de paires clé-valeur optionnelles pour les dimensions.

<?php
namespace App\Latte;

class YoutubeNode extends StatementNode
{
	public ExpressionNode $videoId;
	public ?ExpressionNode $width = null;
	public ?ExpressionNode $height = null;

	public static function create(Tag $tag): self
	{
		$tag->expectArguments();
		$node = $tag->node = new self;
		// Analyse de l'ID vidéo requis
		$node->videoId = $tag->parser->parseExpression();

		// Analyse des paires clé-valeur optionnelles
		$stream = $tag->parser->stream; // Obtention du flux de tokens
		while ($stream->tryConsume(',')) { // Nécessite une séparation par virgule
			// Attente de l'identifiant 'width' ou 'height'
			$keyToken = $stream->consume(Token::Php_Identifier);
			$key = strtolower($keyToken->text);

			$stream->consume(':'); // Attente du séparateur deux-points

			$value = $tag->parser->parseExpression(); // Analyse de l'expression de valeur

			if ($key === 'width') {
				$node->width = $value;
			} elseif ($key === 'height') {
				$node->height = $value;
			} else {
				throw new CompileException("Argument inconnu '$key'. Attendu 'width' ou 'height'.", $keyToken->position);
			}
		}

		return $node;
	}
}

Ce niveau de contrôle vous permet de définir des syntaxes très spécifiques et complexes pour vos propres balises en interagissant directement avec le flux de tokens.

Utilisation de AuxiliaryNode

Latte fournit des nœuds “auxiliaires” génériques pour des situations spéciales lors de la génération de code ou dans les passes de compilation. Ce sont AuxiliaryNode et Php\Expression\AuxiliaryNode.

Considérez AuxiliaryNode comme un nœud conteneur flexible qui délègue ses fonctionnalités de base – génération de code et exposition des nœuds enfants – aux arguments fournis dans son constructeur :

  • Délégation de print() : Le premier argument du constructeur est une closure PHP. Lorsque Latte appelle la méthode print() sur un AuxiliaryNode, il exécute cette closure fournie. La closure reçoit un PrintContext et tous les nœuds passés dans le deuxième argument du constructeur, vous permettant de définir une logique de génération de code PHP entièrement personnalisée à la volée.
  • Délégation de getIterator() : Le deuxième argument du constructeur est un tableau d'objets Node. Lorsque Latte a besoin de parcourir les enfants d'un AuxiliaryNode (par exemple, pendant les passes de compilation), sa méthode getIterator() fournit simplement les nœuds listés dans ce tableau.

Exemple :

$node = new AuxiliaryNode(
    // 1. Cette closure devient le corps de print()
    fn(PrintContext $context, $arg1, $arg2) => $context->format('...%node...%node...', $arg1, $arg2),

    // 2. Ces nœuds sont fournis par la méthode getIterator() et passés à la closure ci-dessus
    [$argumentNode1, $argumentNode2]
);

Latte fournit deux types distincts basés sur l'endroit où vous devez insérer le code généré :

  • Latte\Compiler\Nodes\Php\Expression\AuxiliaryNode : Utilisez ceci lorsque vous devez générer un morceau de code PHP qui représente une expression
  • Latte\Compiler\Nodes\AuxiliaryNode : Utilisez ceci à des fins plus générales lorsque vous devez insérer un bloc de code PHP représentant une ou plusieurs instructions

Une raison importante d'utiliser AuxiliaryNode au lieu de nœuds standard (comme StaticMethodCallNode) dans votre méthode print() ou une passe de compilation est le contrôle de la visibilité pour les passes de compilation suivantes, en particulier celles liées à la sécurité comme Sandbox.

Considérez le scénario suivant : Votre passe de compilation doit envelopper une expression fournie par l'utilisateur ($userExpr) dans un appel à une fonction d'aide spécifique et fiable myInternalSanitize($userExpr). Si vous créez un nœud standard new FunctionCallNode('myInternalSanitize', [$userExpr]), il sera entièrement visible pour le parcours de l'AST. Si la passe Sandbox s'exécute plus tard et que myInternalSanitize n'est pas sur sa liste blanche, Sandbox pourrait bloquer ou modifier cet appel, perturbant potentiellement la logique interne de votre balise, même si vous, l'auteur de la balise, savez que cet appel spécifique est sûr et nécessaire. Vous pouvez donc générer l'appel directement dans la closure de AuxiliaryNode.

use Latte\Compiler\Nodes\Php\Expression\AuxiliaryNode;

// ... à l'intérieur de print() ou d'une passe de compilation ...
$wrappedNode = new AuxiliaryNode(
	fn(PrintContext $context, $userExpr) => $context->format(
		'myInternalSanitize(%node)', // Génération directe du code PHP
		$userExpr,
	),
	// IMPORTANT : Passez toujours le nœud d'expression utilisateur original ici !
	[$userExpr],
);

Dans ce cas, la passe Sandbox voit l'AuxiliaryNode, mais n'analyse pas le code PHP généré par sa closure. Elle ne peut pas bloquer directement l'appel myInternalSanitize généré à l'intérieur de la closure.

Alors que le code PHP généré lui-même est caché aux passes, les entrées de ce code (les nœuds représentant les données ou expressions utilisateur) doivent toujours être parcourables. C'est pourquoi le deuxième argument du constructeur AuxiliaryNode est crucial. Vous devez passer un tableau contenant tous les nœuds originaux (comme $userExpr dans l'exemple ci-dessus) que votre closure utilise. Le getIterator() de AuxiliaryNode fournira ces nœuds, permettant aux passes de compilation comme Sandbox de les analyser pour des problèmes potentiels.

Meilleures pratiques

  • Objectif clair : Assurez-vous que votre balise a un objectif clair et nécessaire. Ne créez pas de balises pour des tâches qui peuvent être facilement résolues avec des filtres ou des fonctions.
  • Implémentez correctement getIterator() : Implémentez toujours getIterator() et fournissez des références (&) à tous les nœuds enfants (arguments, contenu) qui ont été analysés à partir du template. Ceci est essentiel pour les passes de compilation, la sécurité (Sandbox) et les optimisations futures potentielles.
  • Propriétés publiques pour les nœuds : Rendez publiques les propriétés contenant des nœuds enfants afin que les passes de compilation puissent les modifier si nécessaire.
  • Utilisez PrintContext::format() : Utilisez la méthode format() pour générer du code PHP. Elle gère les guillemets, échappe correctement les placeholders et ajoute automatiquement les commentaires de numéro de ligne.
  • Variables temporaires ($__) : Lors de la génération de code PHP d'exécution qui nécessite des variables temporaires (par exemple, pour stocker des sous-totaux, des compteurs de boucle), utilisez la convention de préfixe $__ pour éviter les collisions avec les variables utilisateur et les variables internes de Latte $ʟ_.
  • Imbrication et ID uniques : Si votre balise peut être imbriquée ou nécessite un état spécifique à l'instance à l'exécution, utilisez $context->generateId() dans votre méthode print() pour créer des suffixes uniques pour vos variables temporaires $__.
  • Fournisseurs pour les données externes : Utilisez des fournisseurs (enregistrés via Extension::getProviders()) pour accéder aux données ou services d'exécution ($this->global->…) au lieu de coder en dur des valeurs ou de dépendre de l'état global. Utilisez des préfixes de fournisseur pour les noms de fournisseurs.
  • Considérez les n:attributs : Si votre balise appariée opère logiquement sur un seul élément HTML, Latte fournit probablement un support automatique de n:attribut. Gardez cela à l'esprit pour la commodité de l'utilisateur. Si vous créez une balise modifiant un attribut, demandez-vous si un n:attribut pur est la forme la plus appropriée.
  • Tests : Écrivez des tests pour vos balises, couvrant à la fois l'analyse de différentes entrées syntaxiques et l'exactitude de la sortie du code PHP généré.

En suivant ces directives, vous pouvez créer des balises personnalisées puissantes, robustes et maintenables qui s'intègrent de manière transparente au moteur de template Latte.

L'étude des classes de nœuds fournies avec Latte est le meilleur moyen d'apprendre tous les détails du processus d'analyse.

version: 3.0