Création de balises personnalisées
Cette page fournit un guide complet pour créer des balises personnalisées dans Latte. Nous couvrirons tout, des balises simples aux scénarios plus complexes avec contenu imbriqué et besoins d'analyse particuliers, en nous appuyant sur votre compréhension de la façon dont Latte compile les templates.
Les balises personnalisées offrent le plus haut niveau de contrôle sur la syntaxe des templates et la logique de rendu, mais ce sont aussi le point d'extension le plus complexe. Avant de décider d'en créer une, demandez-vous toujours s'il n'existe pas une solution plus simple ou si une balise adaptée ne figure pas déjà dans le jeu standard. N'utilisez les balises personnalisées que lorsque les alternatives plus simples ne suffisent pas.
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 éclaire la structure des balises et leur place dans l'ensemble.
Simplifiée, la compilation d'un template dans Latte comporte ces étapes clés :
- Analyse lexicale : le lexer lit le code source du template (fichier
.latte) et le découpe en une suite de petits éléments distincts appelés tokens (par ex.{,foreach,$variable,}, du texte HTML, etc.). - Analyse syntaxique : le parser prend ce flux de tokens et construit une structure arborescente signifiante, représentant la logique et le contenu du template. Cet arbre s'appelle l'arbre syntaxique abstrait (AST).
- Passes de compilation : avant de générer le code PHP, Latte exécute les passes de compilation. Ce sont des fonctions qui parcourent tout l'AST et peuvent le modifier ou en tirer des informations. Cette étape est cruciale pour des fonctionnalités comme la sécurité (Sandbox) ou les optimisations.
- Génération du code : enfin, le compilateur parcourt l'AST (éventuellement modifié) et génère le code de la classe PHP correspondante. C'est ce code PHP qui rend réellement le template lorsqu'il est exécuté.
- Mise en cache : le code PHP généré est mis en cache sur le disque, ce qui rend les rendus suivants très rapides, puisque les étapes 1 à 4 sont ignorées.
En réalité, la compilation est un peu plus compliquée. Latte possède deux lexers et parsers : un pour le template HTML et un pour le code proche de PHP situé à l'intérieur des balises. De plus, l'analyse syntaxique ne suit pas la tokenisation : le lexer et le parser tournent en parallèle dans deux “fils” et se coordonnent. Croyez-moi, David Grudl : programmer cela m'a fait l'effet d'une science de fusée :-)
Tout le processus, du chargement du contenu du template jusqu'à la génération du fichier résultant en passant par l'analyse, peut être enchaîné par ce code, avec lequel vous pouvez expérimenter et dumper 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
Créer une balise personnalisée pleinement fonctionnelle dans Latte met en jeu plusieurs parties liées entre elles. Avant de plonger dans l'implémentation, comprenons les concepts et la terminologie de base, par analogie avec le HTML et le Document Object Model (DOM).
Balises et nœuds (analogie avec HTML)
En HTML, nous écrivons des balises comme <p> ou <div>...</div>. Ces balises
sont de la syntaxe dans le code source. Lorsqu'un 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 (plus précisément
des nœuds Element dans la terminologie du DOM JavaScript). C'est avec ces nœuds que nous interagissons par
programme (par ex. document.getElementById(...) en JavaScript retourne un nœud Element). La balise n'est que la
représentation textuelle dans le fichier source ; le nœud est la représentation objet dans l'arbre logique.
Latte fonctionne de façon semblable :
- Dans un fichier de template
.latte, vous écrivez des balises Latte, comme{foreach ...}et{/foreach}. C'est la syntaxe avec laquelle vous interagissez en tant qu'auteur de template. - 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, fragment de texte ou expression du template devient un ou plusieurs nœuds de cet arbre.
- La classe de base de tous les nœuds de l'AST est
Latte\Compiler\Node. Tout comme le DOM connaît différents types de nœuds (Element, Text, Comment), l'AST de Latte en connaît plusieurs. Vous rencontrerezLatte\Compiler\Nodes\TextNodepour le texte statique,Latte\Compiler\Nodes\Html\ElementNodepour les éléments HTML,Latte\Compiler\Nodes\Php\ExpressionNodepour les expressions à l'intérieur des balises et, ce qui est capital pour les balises personnalisées, des nœuds héritant deLatte\Compiler\Nodes\StatementNode.
Pourquoi StatementNode ?
Les éléments HTML (Html\ElementNode) représentent avant tout une structure et un contenu. Les expressions PHP
(Php\ExpressionNode) représentent des valeurs ou des calculs. Mais qu'en est-il de balises Latte comme
{if}, {foreach} ou notre {datetime} personnalisée ? Ces balises accomplissent des
actions, pilotent le flux du programme ou génèrent une sortie à partir d'une logique. Ce sont les unités fonctionnelles
qui font de Latte un moteur de templates puissant, et pas seulement un langage de balisage.
En programmation, de telles unités qui accomplissent des actions sont souvent appelées “instructions” (statements). Les
nœuds représentant ces balises Latte fonctionnelles héritent donc généralement de
Latte\Compiler\Nodes\StatementNode. Cela les distingue des nœuds purement structurels (comme les éléments HTML) ou
de ceux qui représentent des valeurs (comme les expressions).
Les composants clés
Reprenons les principaux composants nécessaires à la création d'une balise personnalisée :
Fonction d'analyse de la balise
- Ce callable PHP analyse la syntaxe de la balise Latte (
{...}) dans le source du template. - Il reçoit des informations sur la balise (nom, position, s'il s'agit d'un n:attribut) via un objet Latte\Compiler\Tag, et le Latte\Compiler\TemplateParser principal en second
argument. Sa signature complète est
callable(Tag, TemplateParser): (Node|\Generator|void). - Son principal outil 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 parser différent de celui qui analyse tout le template). - Pour les balises paires, il utilise
yieldpour signaler à Latte d'analyser le contenu intérieur situé entre la balise ouvrante et la balise fermante. - Le but ultime de la fonction d'analyse est de créer et de retourner une instance de classe de nœud, qui sera ajoutée à l'AST.
- Il est d'usage (sans être 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 garde ensemble la logique d'analyse et la représentation du nœud, permet d'accéder aux éléments privés ou protégés de la classe si besoin, et améliore l'organisation.
Classe de nœud
- Représente la fonction logique de votre balise au sein de l'arbre syntaxique abstrait (AST).
- Elle conserve les informations analysées (arguments, contenu) dans des propriétés publiques. Ces propriétés contiennent
souvent d'autres instances de
Node(par ex.ExpressionNodepour les arguments analysés,AreaNodepour le contenu analysé). - La méthode
print(PrintContext $context): stringgénère le code PHP (une instruction ou une série d'instructions) qui accomplit l'action de la balise lors du rendu du template. - La méthode
getIterator(): \Generatorrend les nœuds enfants (arguments, contenu) accessibles au parcours des passes de compilation. Elle doit produire des références (&) afin que les passes puissent modifier ou remplacer les sous-nœuds. - Une fois tout le template analysé en AST, Latte exécute une série de passes de compilation. Ces passes parcourent l'intégralité de l'AST
à l'aide de la méthode
getIterator()fournie par chaque nœud. Elles peuvent inspecter les nœuds, recueillir des informations et même modifier l'arbre (en changeant les propriétés publiques des nœuds ou en remplaçant entièrement des nœuds). Cette conception, qui exige ungetIterator()exhaustif, est essentielle. Elle permet à des fonctionnalités puissantes comme le Sandbox d'analyser et, le cas échéant, d'altérer le comportement de n'importe quelle partie du template, y compris vos balises personnalisées, ce qui garantit sécurité et cohérence.
Enregistrement via une extension
- Vous devez signaler votre nouvelle balise à Latte et lui indiquer quelle fonction d'analyse employer. Cela se fait dans une extension Latte.
- Dans votre classe d'extension, vous implémentez la méthode
getTags(): array. Elle retourne un tableau associatif dont les clés sont les noms des balises (par ex.'mytag','n:myattribute') et les valeurs les callables PHP représentant leurs fonctions d'analyse respectives (par ex.MyNamespace\DatetimeNode::create(...)).
En résumé : la fonction d'analyse de la balise transforme le code source du template de votre balise en un
nœud de l'AST. La classe de nœud sait ensuite se transformer elle-même en code PHP exécutable
pour le template compilé et rend ses sous-nœuds accessibles aux passes de compilation via getIterator().
L'enregistrement via une extension relie le nom de la balise à la fonction d'analyse et la fait connaître à Latte.
Voyons maintenant comment implémenter ces composants pas à pas.
Création d'une balise simple
Plongeons 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 courantes. Au départ, cette balise n'acceptera aucun
argument, mais nous l'enrichirons plus loin dans la section Analyse des
arguments de balise. Elle n'a pas non plus de contenu intérieur.
Cet exemple vous guidera à travers les étapes essentielles : définir la classe de nœud, implémenter ses méthodes
print() et getIterator(), créer la fonction d'analyse et, pour finir, enregistrer la balise.
Objectif : implémenter {datetime} pour afficher la date et l'heure courantes à l'aide de la fonction PHP
date().
Création de la classe de nœud
Il nous faut d'abord une classe pour représenter notre balise dans l'arbre syntaxique abstrait (AST). Comme évoqué plus
haut, nous héritons de Latte\Compiler\Nodes\StatementNode.
Créez un fichier (par ex. DatetimeNode.php) et définissez la classe :
<?php
namespace App\Templating;
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 quand {datetime} est rencontrée.
*/
public static function create(Tag $tag): self
{
// Notre balise affiche du contenu, gardons donc l'indentation environnante
$tag->outputMode = $tag::OutputKeepIndentation;
// Notre balise simple ne prend pour l'instant aucun argument, 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,
);
}
/**
* Donne accès aux nœuds enfants pour les passes de compilation de Latte.
*/
public function &getIterator(): \Generator
{
false && yield;
}
}
Quand Latte rencontre {datetime} dans un template, il appelle la fonction d'analyse create(). Son
rôle est de retourner une instance de DatetimeNode. Nous fixons aussi $tag->outputMode à
OutputKeepIndentation : comme une balise fonctionne par défaut en mode OutputNone (expliqué dans Modes de sortie des balises), une balise placée avant le premier texte du template
pourrait sinon émettre sa sortie dans la méthode prepare() générée au lieu de main(). Ce mode
garantit que la sortie arrive là où se trouve la balise.
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 le masque dans lequel les paramètres suivants sont substitués. Le
marqueur %line indique à la méthode format() de prendre l'argument suivant, ici
$this->position, et d'insérer un commentaire du type /* pos 15:1 */ qui relie le code PHP généré
à la ligne d'origine 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 automatiquement définie
par le parser de Latte. Elle contient un objet Latte\Compiler\Range (une sous-classe de
Position enrichie d'une length en octets) indiquant où se trouve la balise dans le fichier source
.latte. Pour les balises paires, l'intervalle s'étend de la balise ouvrante à la balise fermante, et les
descendants de StatementNode exposent en outre $this->tagRanges, qui liste le Range de
chaque balise constitutive (ouvrante, intermédiaire comme {else}/{case}, et fermante).
La méthode getIterator() est vitale pour les passes de compilation. Elle doit produire tous les nœuds enfants,
mais notre simple DatetimeNode n'a pour l'instant ni arguments ni contenu, donc aucun nœud enfant. La méthode doit
néanmoins exister et être un générateur, c'est-à-dire que le mot-clé yield doit d'une façon ou d'une autre
figurer dans son corps.
Enregistrement via une extension
Pour finir, signalez la nouvelle balise à Latte. Créez une classe d'extension (par ex.
MyLatteExtension.php) et enregistrez la balise dans sa méthode getTags().
<?php
namespace App\Templating;
use Latte\Extension;
class MyLatteExtension extends Extension
{
/**
* Retourne la liste des balises fournies par cette extension.
* @return array<string, callable> Map : 'nom-de-balise' => fonction-d-analyse
*/
public function getTags(): array
{
return [
'datetime' => DatetimeNode::create(...),
// D'autres balises seront enregistrées ici plus tard
];
}
}
Enregistrez ensuite cette extension auprès du moteur Latte :
$latte = new Latte\Engine;
$latte->addExtension(new App\Templating\MyLatteExtension);
Créez le template :
<p>Page generated on: {datetime}</p>
Sortie attendue : <p>Page generated on: 2023-10-27 11:00:00</p>
Récapitulatif de cette étape
Nous avons créé avec succès une balise personnalisée de base, {datetime}. Nous avons défini sa
représentation dans l'AST (DatetimeNode), pris en charge son analyse (create()), précisé comment elle
doit générer du code PHP (print()), veillé à ce que ses enfants soient parcourables (getIterator())
et enregistré la balise auprès de Latte.
Dans la section suivante, nous enrichirons cette balise pour qu'elle accepte des arguments, ce qui montrera comment analyser des expressions et gérer des nœuds enfants.
Analyse des arguments de balise
Notre simple balise {datetime} fonctionne, mais elle n'est guère souple. Enrichissons-la pour qu'elle accepte un
argument facultatif : une chaîne de format pour la fonction date(). La syntaxe voulue sera
{datetime $format}.
Objectif : modifier {datetime} pour qu'elle accepte une expression PHP facultative en argument, utilisée
comme chaîne de format 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. Quand le parser principal de
Latte (TemplateParser) rencontre une balise Latte comme {datetime ...} ou un n:attribut, il délègue
l'analyse du contenu intérieur de la balise (la partie entre { et }, ou la valeur de l'attribut)
à un TagParser spécialisé.
Ce TagParser opère uniquement sur les arguments de la balise. Son travail est de consommer les tokens
représentant ces arguments. Point capital : il doit analyser tout le contenu qui lui est fourni. Si votre fonction
d'analyse se termine alors que le TagParser n'a pas atteint la fin des arguments (vérifiable via
$tag->parser->isEnd()), Latte lèvera une exception, car cela signale des tokens inattendus restés dans la
balise. À l'inverse, si une balise exige des arguments, vous devriez appeler $tag->expectArguments() au
début de votre fonction d'analyse. Cette méthode vérifie la présence d'arguments et lève une exception explicite si la balise
a été utilisée sans aucun.
TagParser offre des méthodes utiles pour analyser différents types d'arguments :
parseExpression(): ExpressionNode: analyse une expression proche de PHP (variables, littéraux, opérateurs, appels de fonctions ou de méthodes, etc.). Elle gère le sucre syntaxique de Latte, comme le traitement des chaînes alphanumériques simples comme des chaînes entre guillemets (par ex.fooest analysé comme s'il s'agissait de'foo').parseUnquotedStringOrExpression(): ExpressionNode: analyse soit une expression standard, soit une chaîne sans guillemets. Les chaînes sans guillemets sont des séquences que Latte autorise sans guillemets, souvent employées pour des chemins de fichiers (par ex.{include ../file.latte}). Si elle analyse une chaîne sans guillemets, elle retourne unStringNode.parseArguments(): ArrayNode: analyse des arguments séparés par des virgules, éventuellement avec des clés, comme10, name: 'John', true.parseModifier(): ModifierNode: analyse les filtres du type|upper|truncate:10.parseType(): ?SuperiorTypeNode: analyse les déclarations de types PHP commeint,?string,array|Foo.
Pour des besoins d'analyse plus complexes ou de plus bas niveau, vous pouvez interagir directement avec le flux de tokens via
$tag->parser->stream. Cet objet fournit des méthodes pour inspecter et consommer les tokens un à un :
$tag->parser->stream->is(...): bool: vérifie si le token courant correspond à l'un des types indiqués (par ex.Token::Php_Variable) ou à des valeurs littérales (par ex.'as') sans le consommer. Utile pour regarder devant soi.$tag->parser->stream->consume(...): Token: consomme le token courant et fait avancer la position du flux. Si des types ou valeurs attendus sont passés en arguments et que le token courant ne correspond pas, la méthode lève uneCompileException. Utilisez-la quand vous attendez un token donné.$tag->parser->stream->tryConsume(...): ?Token: tente de consommer le token courant seulement s'il correspond à l'un des types ou valeurs indiqués. En cas de correspondance, il consomme le token et le retourne. Sinon, il laisse la position du flux inchangée et retournenull. Utilisez-la pour les tokens facultatifs ou pour choisir entre plusieurs syntaxes.
Mise à jour de la fonction d'analyse create()
Fort de ces éléments, modifions la méthode create() de DatetimeNode pour analyser l'argument de
format facultatif à l'aide de $tag->parser.
<?php
namespace App\Templating;
use Latte\Compiler\Nodes\Php\ExpressionNode;
use Latte\Compiler\Nodes\Php\Scalar\StringNode;
use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\PrintContext;
use Latte\Compiler\Tag;
class DatetimeNode extends StatementNode
{
// Ajout d'une propriété publique pour conserver le nœud de l'expression de format analysée
public ?ExpressionNode $format = null;
public static function create(Tag $tag): self
{
$node = $tag->node = new self;
// Y a-t-il des tokens ?
if (!$tag->parser->isEnd()) {
// Analyse l'argument comme une expression proche de PHP à l'aide du TagParser.
$node->format = $tag->parser->parseExpression();
}
return $node;
}
// ... les méthodes print() et getIterator() seront mises à jour ensuite ...
}
Nous avons ajouté la propriété publique $format. Dans create(), nous utilisons désormais
$tag->parser->isEnd() pour vérifier s'il y a des arguments. Si oui,
$tag->parser->parseExpression() consomme les tokens de l'expression. Comme le TagParser doit
consommer tous ses 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 ex. {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 et stockée dans
$this->format. Si aucun format n'a été fourni ($this->format vaut null), nous
devons employer une chaîne de format 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 affiche 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 de l'AST représentant la chaîne de format 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 ex. {datetime 'd.m.Y'}), la propriété $this->format contient le
nœud correspondant (ici un StringNode de valeur 'd.m.Y') et c'est ce nœud qui est utilisé. Si
l'utilisateur n'a pas fourni d'argument (il a écrit simplement {datetime}), la propriété
$this->format vaut null et nous créons à la place un nouveau StringNode avec le format
par défaut 'Y-m-d H:i:s'. Cela garantit que $formatNode contient toujours un nœud d'AST valide pour le
format.
Dans le masque 'echo date(%node) %line;', le nouveau marqueur %node indique à la méthode
format() de prendre le premier argument suivant (notre $formatNode), d'appeler sa méthode
print() (qui retourne sa représentation en code PHP) et d'insérer le résultat à la place du marqueur.
Implémentation de getIterator() pour les
sous-nœuds
Notre DatetimeNode a désormais un nœud enfant : l'expression $format. Nous devons rendre ce
nœud enfant accessible aux passes de compilation en le produisant dans la méthode getIterator(). N'oubliez pas de
produire une référence (&) pour permettre aux passes de remplacer le nœud.
public function &getIterator(): \Generator
{
if ($this->format) {
yield $this->format;
}
}
Pourquoi est-ce capital ? Imaginez une passe Sandbox qui doit vérifier si l'argument $format contient un appel de
fonction interdit (par ex. {datetime dangerousFunction()}). Si getIterator() ne produit pas
$this->format, la passe Sandbox ne verrait jamais l'appel dangerousFunction() dans l'argument de
notre balise, ce qui ouvrirait une faille de sécurité potentielle. En le produisant, nous permettons au Sandbox (et aux autres
passes) d'inspecter et, le cas échéant, de modifier le nœud de l'expression $format.
Utilisation de la balise enrichie
La balise gère désormais correctement un argument facultatif :
Default format: {datetime}
Custom format: {datetime 'd.m.Y'}
Using variable: {datetime $userDateFormatPreference}
{* Ceci provoquerait une erreur après l'analyse de 'd.m.Y', car ", foo" est inattendu *}
{* {datetime 'd.m.Y', foo} *}
Voyons ensuite comment créer des balises paires qui traitent le contenu situé entre elles.
Balises paires
Jusqu'ici, notre balise {datetime} est auto-fermante (conceptuellement). Elle n'a aucun contenu entre une
balise ouvrante et une balise fermante. Beaucoup de balises utiles opèrent toutefois sur un bloc de contenu du template. On les
appelle balises paires. {if}...{/if}, {block}...{/block} en sont des exemples, tout comme la
balise personnalisée que nous allons construire : {debug}...{/debug}.
Cette balise nous permettra d'inclure dans nos templates des informations de débogage qui ne doivent être visibles que pendant le développement.
Objectif : créer une balise paire {debug} dont le contenu n'est rendu que si un drapeau “mode
développement” est actif.
Présentation des providers
Il arrive que vos balises aient besoin d'accéder à des données ou services qui ne sont pas passés directement comme paramètres de template : déterminer si l'application est en mode développement, accéder à un objet utilisateur ou obtenir des valeurs de configuration, par exemple. Latte propose pour cela un mécanisme appelé providers.
Les providers s'enregistrent dans votre Extension
à l'aide de la méthode getProviders(). Cette méthode retourne un tableau associatif dont les clés sont les noms
sous lesquels les providers seront accessibles dans le code d'exécution du template, et les valeurs les données ou objets
eux-mêmes.
Dans le code PHP généré par la méthode print() de votre balise, vous accédez ensuite à ces providers via la
propriété d'objet spéciale $this->global. Comme cette propriété est partagée par toutes les extensions, il
est de bonne pratique de préfixer les noms de vos providers pour éviter les collisions avec les providers du cœur de
Latte ou d'autres extensions tierces. La convention courante est d'utiliser un préfixe court et unique, lié à votre nom
d'éditeur ou d'extension. Pour notre exemple, prenons le préfixe app : le drapeau de mode développement sera
disponible sous $this->global->appDevMode.
Le mot-clé yield pour analyser le contenu
Comment dire au parser de Latte de traiter le contenu situé entre {debug} et {/debug} ? C'est
là qu'entre en jeu le mot-clé yield.
Lorsque yield est utilisé dans la fonction create(), celle-ci devient un générateur PHP. Son exécution se met en pause et la
main revient au TemplateParser principal. Le TemplateParser poursuit alors l'analyse du contenu du
template jusqu'à rencontrer 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 yield est un
tableau de deux éléments :
- un
AreaNodereprésentant le contenu analysé entre la balise ouvrante et la balise fermante ; - l'objet
Tagreprésentant la balise fermante (par ex.{/debug}).
Créons la classe DebugNode et sa méthode create avec yield.
<?php
namespace App\Templating;
use Latte\Compiler\Nodes\AreaNode;
use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\PrintContext;
use Latte\Compiler\Tag;
class DebugNode extends StatementNode
{
// Propriété publique pour stocker le contenu intérieur analysé
public AreaNode $content;
/**
* Fonction d'analyse de la balise paire {debug} ... {/debug}.
*/
public static function create(Tag $tag): \Generator // notez le type de retour
{
$node = $tag->node = new self;
// Met l'analyse en pause, récupère le contenu intérieur et la balise fermante quand {/debug} est trouvée
[$node->content, $endTag] = yield;
return $node;
}
// ... print() et getIterator() seront implémentées ensuite ...
}
Remarque : $endTag vaut null si la balise est utilisée comme n:attribut, c'est-à-dire
<div n:debug>...</div>.
Une balise paire peut aussi se fermer par une barre oblique, comme {debug/} (ou
<div n:debug/>). Elle n'a alors pas de contenu intérieur : le générateur reçoit
[$emptyFragmentNode, $startTag], où le second élément est la balise ouvrante elle-même, et non
null.
Implémentation de print() pour un rendu
conditionnel
La méthode print() doit maintenant générer un code PHP qui interroge le provider appDevMode à
l'exécution et n'exécute le code du contenu intérieur que si le drapeau est vrai.
public function print(PrintContext $context): string
{
// Génère une instruction PHP 'if' qui interroge le provider à l'exécution
return $context->format(
<<<'XX'
if ($this->global->appDevMode) %line {
// En mode dev, affiche le contenu intérieur
%node
}
XX,
$this->position, // Pour le commentaire %line
$this->content, // Le nœud contenant l'AST du contenu intérieur
);
}
C'est très direct. Nous utilisons PrintContext::format() pour créer une instruction PHP if
standard. À l'intérieur du if, nous plaçons le marqueur %node pour $this->content.
Latte appellera récursivement $this->content->print($context) afin de générer le code PHP de la partie
intérieure de la balise, mais seulement si $this->global->appDevMode vaut vrai à l'exécution.
Implémentation de getIterator() pour le contenu
Comme avec le nœud d'argument de l'exemple précédent, notre DebugNode a désormais un nœud enfant :
l'AreaNode $content. Nous devons le rendre parcourable en le produisant dans getIterator() :
public function &getIterator(): \Generator
{
// Produit la 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 compte même si
ce contenu n'est rendu que sous condition. Le Sandbox, par exemple, doit analyser le contenu que appDevMode soit vrai
ou faux.
Enregistrement et utilisation
Enregistrez la balise et le provider dans votre extension :
class MyLatteExtension extends Extension
{
// On suppose 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(...), // Enregistre la nouvelle balise
];
}
public function getProviders(): array
{
return [
'appDevMode' => $this->isDevelopmentMode, // Enregistre le provider
];
}
}
// Lors de l'enregistrement de l'extension :
$isDev = true; // À déterminer selon l'environnement de votre application
$latte->addExtension(new MyLatteExtension($isDev));
Et utilisez-la dans un template :
<p>Contenu ordinaire, toujours visible.</p>
{debug}
<div class="debug-panel">
ID de l'utilisateur courant : {$user->id}
Heure de la requête : {=time()}
</div>
{/debug}
<p>Encore du contenu ordinaire.</p>
Intégration des n:attributs
Latte propose un raccourci pratique pour de nombreuses balises paires : les n:attributs. Si vous avez une balise paire du type
{tag}...{/tag} et que vous voulez que son effet porte directement sur un seul élément HTML, vous pouvez souvent
l'écrire plus brièvement comme un attribut n:tag sur cet élément.
Pour la plupart des balises paires que vous définissez (comme notre {debug}), Latte active automatiquement la
version n: correspondante. Vous n'avez rien à faire de plus lors de l'enregistrement :
{* Utilisation standard de la balise paire *}
{debug}<div>Debug info</div>{/debug}
{* Utilisation équivalente avec un n:attribut *}
<div n:debug>Debug info</div>
Les deux ne rendront le <div> que si $this->global->appDevMode vaut vrai. Les préfixes
inner- et tag- fonctionnent comme prévu.
Il arrive que la logique de votre balise doive se comporter légèrement différemment selon qu'elle est employée comme balise
paire standard ou comme n:attribut, ou selon qu'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 cette information :
$tag->isNAttribute(): bool: retournetruesi la balise est analysée comme un n:attribut$tag->prefix: ?string: retourne le préfixe utilisé avec le n:attribut, qui peut êtrenull(pas un n:attribut),Tag::PrefixNone,Tag::PrefixInnerouTag::PrefixTag
Maintenant que nous comprenons les balises simples, l'analyse des arguments, les balises paires, les providers et les
n:attributs, attaquons un scénario plus complexe : des balises imbriquées dans d'autres balises, en partant de notre balise
{debug}.
Balises intermédiaires
Certaines balises paires autorisent, voire exigent, la présence d'autres balises à l'intérieur d'elles, avant la
balise fermante finale. On les appelle balises intermédiaires. {if}...{elseif}...{else}...{/if} ou
{switch}...{case}...{default}...{/switch} en sont des exemples classiques.
Étendons notre balise {debug} pour qu'elle prenne en charge une clause {else} facultative, rendue
lorsque l'application n'est pas en mode développement.
Objectif : modifier {debug} pour qu'elle accepte une balise intermédiaire {else} facultative.
La syntaxe finale sera {debug} ... {else} ... {/debug}.
Analyser les balises intermédiaires avec yield
Nous savons déjà que yield met en pause la fonction d'analyse create() et retourne le contenu
analysé ainsi que la balise fermante. Mais yield offre plus de contrôle : vous pouvez lui fournir un tableau de
noms de balises intermédiaires. Quand le parser rencontre l'une de ces balises au même niveau d'imbrication
(c'est-à-dire comme enfant direct de la balise parente, et non dans un autre bloc ou une autre balise à l'intérieur), il
arrête également l'analyse du contenu.
Quand l'analyse s'arrête à cause d'une balise intermédiaire, elle cesse d'analyser le contenu, reprend le générateur
create() et lui renvoie le contenu partiellement analysé ainsi que la balise intermédiaire elle-même (au
lieu de la balise fermante finale). Notre fonction create() peut alors traiter cette balise intermédiaire (analyser
ses arguments s'il y en a) et faire un 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\Templating;
use Latte\Compiler\Nodes\AreaNode;
use Latte\Compiler\Nodes\NopNode;
use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\PrintContext;
use Latte\Compiler\Tag;
class DebugNode extends StatementNode
{
// Contenu de la partie {debug}
public AreaNode $thenContent;
// Contenu facultatif de la partie {else}
public ?AreaNode $elseContent = null;
public static function create(Tag $tag): \Generator
{
$node = $tag->node = new self;
// yield en attendant soit {/debug}, soit {else}
[$node->thenContent, $nextTag] = yield ['else'];
// La balise sur laquelle nous nous sommes arrêtés était-elle {else} ?
if ($nextTag?->name === 'else') {
// Nouveau yield pour analyser le contenu entre {else} et {/debug}
[$node->elseContent, $endTag] = yield;
}
return $node;
}
// ... print() et getIterator() seront mises à jour ensuite ...
}
Désormais, yield ['else'] dit à Latte de s'arrêter non seulement sur {/debug}, mais aussi sur
{else}. Si {else} est rencontrée, $nextTag contiendra l'objet Tag de
{else}. Nous faisons alors un nouveau yield sans argument, ce qui signifie que nous n'attendons plus que
la balise {/debug} finale, et nous stockons le résultat dans $node->elseContent. Si
{else} n'a pas été trouvée, $nextTag serait le Tag de {/debug} (ou
null en cas d'usage 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 doit générer une instruction PHP
if/else fondée sur le provider appDevMode.
public function print(PrintContext $context): string
{
return $context->format(
<<<'XX'
if ($this->global->appDevMode) %line {
%node // Code de la branche 'then' (contenu de {debug})
} else {
%node // Code de la branche 'else' (contenu de {else})
}
XX,
$this->position, // Numéro de ligne pour la condition 'if'
$this->thenContent, // Premier marqueur %node
$this->elseContent ?? new NopNode, // Second marqueur %node
);
}
C'est une structure PHP if/else standard. Nous utilisons %node deux fois ; format()
substitue les nœuds fournis dans l'ordre. Nous employons ?? new NopNode pour éviter les erreurs si
$this->elseContent vaut null : le NopNode n'affiche tout simplement rien.
Implémentation de getIterator() pour les deux
contenus
Nous avons maintenant potentiellement deux nœuds de contenu enfants ($thenContent et $elseContent).
Nous devons produire les deux s'ils existent :
public function &getIterator(): \Generator
{
yield $this->thenContent;
if ($this->elseContent) {
yield $this->elseContent;
}
}
Utilisation de la balise enrichie
La balise s'utilise désormais avec une clause {else} facultative :
{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 affichaient directement du contenu ou faisaient un simple test conditionnel fondé sur un provider
global. Beaucoup de balises doivent pourtant gérer une forme d'état pendant le rendu, ou évaluer des expressions
fournies par l'utilisateur qui ne doivent tourner qu'une fois, pour des raisons de performance ou d'exactitude. Il faut en outre
réfléchir à ce qui se passe quand nos balises personnalisées sont imbriquées.
Illustrons ces notions en créant une balise {repeat $count}...{/repeat}. Cette balise répétera son contenu
intérieur $count fois.
Objectif : implémenter {repeat $count}, qui répète son contenu un nombre de fois donné.
Le besoin de variables temporaires uniques
Imaginez que l'utilisateur écrive :
{repeat rand(1, 5)} Content {/repeat}
Si nous générions naïvement une boucle PHP for comme celle-ci dans notre méthode print() :
// Simplifié, INCORRECT, code généré
for ($i = 0; $i < rand(1, 5); $i++) {
// affiche le contenu
}
Ce serait faux ! L'expression rand(1, 5) serait réévaluée à chaque itération, d'où un nombre
imprévisible de répétitions. Nous devons évaluer l'expression $count une seule fois avant le début de
la boucle et en stocker le résultat.
Nous allons générer un 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'auteur du template et avec les variables
internes de Latte (comme $ʟ_...), nous adopterons la convention du préfixe $__ (double tiret
bas) pour nos variables temporaires.
Le code généré ressemblerait alors à ceci :
$__count = rand(1, 5);
for ($__i = 0; $__i < $__count; $__i++) {
// affiche le contenu
}
Considérons maintenant l'imbrication :
{repeat $countA} {* Boucle extérieure *}
{repeat $countB} {* Boucle intérieure *}
...
{/repeat}
{/repeat}
Si les balises {repeat} extérieure et intérieure généraient du code avec les mêmes noms de variables
temporaires (par ex. $__count et $__i), la boucle intérieure écraserait les variables de la boucle
extérieure et casserait la logique.
Nous devons garantir que les variables temporaires générées pour chaque instance de la balise {repeat} sont
uniques. Nous y parvenons avec PrintContext::generateId(). Cette méthode retourne un entier unique pendant la
phase de compilation. Nous pouvons ajouter cet identifiant aux noms de nos variables temporaires.
Ainsi, au lieu de $__count, nous générerons un nom avec un suffixe numérique unique, comme
$__count_0, et de même pour le compteur de boucle, par ex. $__i_0. Les nombres proviennent d'un
compteur global à la compilation, partagé par tous les nœuds : ils sont donc seulement garantis uniques, sans former une suite
propre à chaque balise.
Implémentation de RepeatNode
Créons la classe de nœud.
<?php
namespace App\Templating;
use Latte\CompileException;
use Latte\Compiler\Nodes\AreaNode;
use Latte\Compiler\Nodes\Php\ExpressionNode;
use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\PrintContext;
use Latte\Compiler\Tag;
class RepeatNode extends StatementNode
{
public ExpressionNode $count;
public AreaNode $content;
/**
* 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();
// Récupère le contenu intérieur
[$node->content] = yield;
return $node;
}
/**
* Génère la boucle PHP 'for' avec des noms de variables uniques.
*/
public function print(PrintContext $context): string
{
// Génère des noms de variables uniques
$id = $context->generateId();
$countVar = '$__count_' . $id; // nom unique, par ex. $__count_0
$iteratorVar = '$__i_' . $id; // nom unique, par ex. $__i_0
return $context->format(
<<<'XX'
// Évalue l'expression de comptage *une seule fois* et la stocke
%raw = (int) (%node);
// Boucle en utilisant le compte stocké et la variable d'itération unique
for (%raw = 0; %2.raw < %0.raw; %2.raw++) %line {
%node // Rend le contenu intérieur
}
XX,
$countVar, // %0 - Variable où stocker le compte
$this->count, // %1 - Le nœud d'expression du compte
$iteratorVar, // %2 - Nom de la variable d'itération
$this->position, // %3 - Commentaire de ligne pour la boucle elle-même
$this->content // %4 - Le nœud du contenu intérieur
);
}
/**
* Produit les nœuds enfants (l'expression de comptage et le contenu).
*/
public function &getIterator(): \Generator
{
yield $this->count;
yield $this->content;
}
}
La méthode create() analyse l'expression $count requise avec parseExpression(). Elle
appelle d'abord $tag->expectArguments(), qui garantit que l'utilisateur a bien fourni quelque chose
après {repeat}. $tag->parser->parseExpression() échouerait certes si rien n'était fourni, mais
le message d'erreur porterait sur une syntaxe inattendue. expectArguments() donne une erreur bien plus claire, qui
dit précisément que les arguments manquent pour la balise {repeat}.
La méthode print() génère le code PHP chargé d'exécuter 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 le nouveau marqueur %raw, qui insère la
chaîne brute fournie en argument correspondant. Ici, il insère le nom de variable unique stocké dans
$countVar (par ex. $__count_1). Et que dire de %0.raw et %2.raw ? C'est une
démonstration des marqueurs positionnels. Au lieu de %raw, qui prend l'argument brut suivant,
%2.raw prend explicitement l'argument d'indice 2 (soit $iteratorVar) et insère sa valeur 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 traite correctement
l'expression de comptage et évite les collisions de noms de variables même quand les balises {repeat} sont
imbriquées.
Enregistrement et utilisation
Enregistrez la balise dans votre extension :
use App\Templating\RepeatNode;
class MyLatteExtension extends Extension
{
public function getTags(): array
{
return [
'datetime' => DatetimeNode::create(...),
'debug' => DebugNode::create(...),
'repeat' => RepeatNode::create(...), // Enregistre la balise repeat
];
}
}
Utilisez-la dans un template, y compris de façon imbriquée :
{var $rows = rand(5, 7)}
{var $cols = rand(3, 5)}
{repeat $rows}
<tr>
{repeat $cols}
<td>Boucle intérieure</td>
{/repeat}
</tr>
{/repeat}
Cet exemple montre comment gérer l'état (les compteurs de boucle) et les problèmes potentiels d'imbrication à l'aide de
variables temporaires préfixées par $__ et rendues uniques par les identifiants de
PrintContext::generateId().
n:attributs purs
Si beaucoup de n:attributs comme n:if ou n:foreach servent de raccourcis commodes à
leurs balises paires équivalentes ({if}...{/if}, {foreach}...{/foreach}), Latte vous permet aussi de
définir des balises qui n'existent que sous forme de n:attribut. Elles servent souvent à modifier les attributs ou le
comportement de l'élément HTML auquel elles sont attachées.
Parmi les exemples standard intégrés à Latte, citons n:class, qui aide à construire dynamiquement l'attribut
class, et n:attr, qui peut définir plusieurs
attributs quelconques.
Créons notre propre n:attribut pur : n:confirm, qui ajoutera une boîte de dialogue de confirmation JavaScript
avant qu'une action (suivre un lien, envoyer un formulaire) ne soit effectuée.
Objectif : implémenter n:confirm="'Are you sure?'", qui ajoute un gestionnaire onclick
empêchant l'action par défaut si l'utilisateur annule la confirmation.
Implémentation de ConfirmNode
Il nous faut une classe de nœud et une fonction d'analyse.
<?php
namespace App\Templating;
use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\PrintContext;
use Latte\Compiler\Tag;
use Latte\Compiler\Nodes\Php\ExpressionNode;
use Latte\Compiler\Nodes\Php\Scalar\StringNode;
class ConfirmNode extends StatementNode
{
public ExpressionNode $message;
public static function create(Tag $tag): self
{
$tag->expectArguments();
$node = $tag->node = new self;
$node->message = $tag->parser->parseExpression();
return $node;
}
/**
* Génère le code de l'attribut 'onclick' avec l'échappement adéquat.
*/
public function print(PrintContext $context): string
{
// Il garantit un échappement correct pour JavaScript comme pour le contexte d'attribut HTML.
return $context->format(
<<<'XX'
echo ' onclick="', LR\HtmlHelpers::escapeAttr('return confirm(' . LR\Helpers::escapeJs(%node) . ')'), '"' %line;
XX,
$this->message,
$this->position,
);
}
public function &getIterator(): \Generator
{
yield $this->message;
}
}
La méthode print() génère le code PHP qui produira finalement l'attribut HTML onclick="..." lors
du rendu du template. Gérer des contextes imbriqués (du JavaScript dans un attribut HTML) demande un échappement soigneux.
L'utilitaire LR\Helpers::escapeJs(%node) est appelé à l'exécution et échappe correctement le message pour un
usage à l'intérieur de JavaScript (la sortie ressemblerait à "Sure?"). Ensuite, l'utilitaire
LR\HtmlHelpers::escapeAttr(...) échappe les caractères particuliers dans les attributs HTML, ce qui transformerait
la sortie en return confirm("Sure?"). Cet échappement en deux temps, à l'exécution, garantit que
le message est sûr pour JavaScript et que le code JavaScript obtenu est sûr à intégrer 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(...), // Enregistre 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("Voulez-vous vraiment supprimer l'élément 123 ?")">Supprimer</a>
Quand l'utilisateur clique sur le lien, le navigateur exécute le code onclick, affiche la boîte de dialogue de
confirmation et ne poursuit vers delete.php que si l'utilisateur clique sur “OK”.
Cet exemple montre comment créer un n:attribut pur qui modifie le comportement ou les attributs de son élément HTML hôte en
générant le code PHP adéquat dans sa méthode print(). N'oubliez pas le double échappement souvent nécessaire :
une fois pour le contexte cible (ici JavaScript) et une fois pour le contexte d'attribut HTML.
Deux autres membres de l'objet Tag sont bien utiles pour écrire des n:attributs purs :
$tag->htmlElement vous donne accès à l'élément HTML environnant (un ElementNode), que vous pouvez
ainsi inspecter ou ajuster, et $tag->replaceNAttribute($node) vous permet de remplacer l'attribut par un nœud que
vous construisez. De fait, le nœud retourné par le create() d'un n:attribut pur remplace automatiquement l'attribut
sur son élément.
Sujets avancés
Les sections précédentes couvrent les notions essentielles ; voici quelques sujets plus avancés que vous pourrez rencontrer en créant des balises Latte personnalisées.
Modes de sortie des balises
L'objet Tag passé à votre fonction create() possède une propriété outputMode. Elle
influe sur la façon dont Latte traite les espaces et l'indentation environnants, en particulier quand la balise est seule sur sa
ligne. Vous pouvez modifier cette propriété dans votre fonction create().
Tag::OutputNone(le mode par défaut de toute balise, celui que conservent les structures de contrôle comme{if}ou{foreach}) : les espaces autour de la balise sont traités exactement comme avecOutputRemoveIndentation: l'indentation en tête et un unique saut de ligne final sont supprimés. La vraie différence est interne : ce mode maintient le parser de templates en mode “tête” du template. Il convient aux balises de déclaration ou de configuration comme{var}ou{default}, qui ne produisent aucune sortie directe.Tag::OutputRemoveIndentation(défini explicitement par les balises de bloc{block},{embed},{include}et{sandbox}) : supprime l'indentation qui précède la balise ainsi qu'un unique saut de ligne final. Cela aide à garder un code PHP généré plus propre et évite les lignes vides superflues dans la sortie HTML causées par la balise elle-même.Tag::OutputKeepIndentation(défini explicitement par les balises d'affichage comme{=...}) : Latte essaie de préserver l'indentation qui précède la balise ; les sauts de ligne après la balise sont généralement conservés. Cela convient aux balises qui affichent du contenu en ligne : voyez l'exemple{datetime}ci-dessus, qui définit ce mode pour cette raison exacte.
Choisissez le mode qui correspond le mieux à la vocation de votre balise. Comme le mode par défaut est
OutputNone, les balises de contrôle de flux et de déclaration n'ont rien à changer ; définissez
OutputKeepIndentation pour les balises qui affichent du contenu sur leur propre ligne.
Accéder aux balises parentes ou les plus proches
Il arrive que le comportement d'une balise dépende du contexte où elle est employée, précisément des balises parentes dans
lesquelles elle se trouve. L'objet Tag passé à votre fonction create() fournit exactement pour cela la
méthode closestTag(array $classes, ?callable $condition = null): ?Tag.
Cette méthode remonte la hiérarchie des balises Latte actuellement ouvertes (la chaîne des $tag->parent ;
les éléments HTML environnants n'en font pas partie) et retourne l'objet Tag de l'ancêtre le plus proche qui
satisfait des critères donnés. Si aucun ancêtre ne correspond, elle retourne null.
Le tableau $classes précise le type d'ancêtres que vous cherchez. Il vérifie si la classe du nœud associé à
la balise ancêtre ($ancestorTag->node) est exactement l'une des classes listées ; les sous-classes ne
correspondent pas.
function create(Tag $tag)
{
// Cherche 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 de ForeachNode elle-même :
$foreachNode = $foreachTag->node;
}
}
Remarquez le $foreachTag->node : cela ne fonctionne que parce qu'il est de convention, dans le développement
de balises Latte, d'affecter immédiatement le nœud créé à $tag->node dans la méthode create(),
comme nous l'avons toujours fait.
Parfois, la seule correspondance de type de nœud ne suffit pas. Vous pouvez avoir besoin de vérifier une propriété précise
de la balise ancêtre potentielle ou de son nœud. Le second argument facultatif de closestTag() est un callable qui
reçoit l'objet Tag ancêtre potentiel et doit indiquer s'il constitue une correspondance valable.
function create(Tag $tag)
{
$dynamicBlockTag = $tag->closestTag(
[BlockNode::class],
// Condition : le bloc doit être dynamique
fn(Tag $blockTag) => $blockTag->node->block->isDynamic(),
);
}
closestTag() vous permet de créer des balises conscientes de leur contexte et d'imposer un usage correct au sein
de la structure de vos templates, d'où des templates plus robustes et plus compréhensibles.
Marqueurs de PrintContext::format()
Nous avons souvent utilisé PrintContext::format() pour générer du code PHP dans les méthodes
print() de nos nœuds. Elle accepte une chaîne de masque et des arguments qui remplacent les marqueurs du masque.
Voici un récapitulatif des marqueurs disponibles :
%node: l'argument doit être une instance deNode. Appelle la méthodeprint()du nœud et insère la chaîne de code PHP obtenue.%dump: l'argument est n'importe quelle valeur PHP. Exporte la valeur en code PHP valide. Convient aux scalaires, aux 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 produit, sans échappement ni modification. À utiliser avec prudence, surtout 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 unExpression\ArrayNode. Affiche les éléments du tableau formatés comme arguments d'un appel de fonction ou de méthode (séparés par des virgules, en gérant les arguments nommés le cas échéant).$argsNode = new ArrayNode([...]);$context->format('myFunc(%args);', $argsNode)→myFunc(1, name: 'Joe');
%line: l'argument doit être un objetPosition(ouRange), en général$this->position. Insère un commentaire PHP/* pos X:Y */indiquant la ligne et la colonne dans le source.$context->format('echo "Hi" %line;', $this->position)→echo "Hi" /* pos 42:1 */;
%escape(...): génère du code PHP qui, à l'exécution, échappera l'expression intérieure selon les règles d'échappement sensibles au contexte en vigueur.$context->format('echo %escape(%node);', $variableNode)
%modify(...): l'argument doit être unModifierNode. Génère du code PHP qui applique au contenu intérieur les filtres indiqués dans leModifierNode, échappement sensible au contexte compris s'il n'est pas désactivé par|noescape.$context->format('%modify(%node);', $modifierNode, $variableNode)
%modifyContent(...): semblable à%modify, mais destiné à modifier des blocs de contenu capturé (souvent du HTML).
Vous pouvez référencer explicitement les arguments par leur indice, à partir de zéro : %0.node,
%1.dump, %2.raw, etc. Cela permet de réutiliser plusieurs fois un argument dans le masque sans le
passer à format() autant de fois. Voyez l'exemple de la balise {repeat}, où %0.raw et
%2.raw sont utilisés.
Exemple d'analyse d'arguments complexe
parseExpression(), parseArguments() et consorts couvrent bien des cas, mais il faut parfois une
logique d'analyse plus fine, à l'aide du TokenStream de plus bas niveau disponible via
$tag->parser->stream.
Objectif : créer une balise {embedYoutube $videoID, width: 640, height: 480}. Nous voulons analyser un
identifiant de vidéo obligatoire (chaîne ou variable), suivi de paires clé-valeur facultatives pour les dimensions.
<?php
namespace App\Templating;
use Latte\CompileException;
use Latte\Compiler\Nodes\Php\ExpressionNode;
use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\Tag;
use Latte\Compiler\Token;
class YoutubeNode extends StatementNode
{
public ExpressionNode $videoId;
public ?ExpressionNode $width = null;
public ?ExpressionNode $height = null;
public static function create(Tag $tag): self
{
$tag->expectArguments();
$node = $tag->node = new self;
// Analyse l'identifiant de vidéo obligatoire
$node->videoId = $tag->parser->parseExpression();
// Analyse les paires clé-valeur facultatives
$stream = $tag->parser->stream; // Récupère le flux de tokens
while ($stream->tryConsume(',')) { // Exige une séparation par virgule
// Attend l'identifiant 'width' ou 'height'
$keyToken = $stream->consume(Token::Php_Identifier);
$key = strtolower($keyToken->text);
$stream->consume(':'); // Attend le deux-points séparateur
$value = $tag->parser->parseExpression(); // Analyse l'expression de la valeur
if ($key === 'width') {
$node->width = $value;
} elseif ($key === 'height') {
$node->height = $value;
} else {
throw new CompileException("Unknown argument '$key'. Expected 'width' or 'height'.", $keyToken->position);
}
}
return $node;
}
// ... print() et getIterator() ...
}
Ce niveau de contrôle vous permet de définir des syntaxes très précises et complexes pour vos balises personnalisées, en interagissant directement avec le flux de tokens.
Utiliser AuxiliaryNode
Latte fournit des nœuds “utilitaires” génériques pour les situations particulières lors de la génération de code ou
dans les passes de compilation : AuxiliaryNode et Php\Expression\AuxiliaryNode.
Voyez AuxiliaryNode comme un nœud conteneur souple, qui délègue ses fonctions centrales – génération de
code et exposition des nœuds enfants – aux arguments fournis à son constructeur :
- Délégation de
print(): le premier argument du constructeur est une closure PHP. Quand Latte appelle la méthodeprint()d'unAuxiliaryNode, il exécute cette closure. Elle reçoit lePrintContextet les nœuds passés dans le second argument du constructeur, ce qui vous permet de définir à la volée une logique de génération de code PHP entièrement sur mesure. - Délégation de
getIterator(): le second argument du constructeur est un tableau d'objetsNode. Quand Latte doit parcourir les enfants d'unAuxiliaryNode(pendant les passes de compilation, par exemple), sa méthodegetIterator()produit simplement les nœuds de 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 produits par getIterator() et passés à la closure ci-dessus
[$argumentNode1, $argumentNode2]
);
Latte propose deux types distincts, selon l'endroit où vous devez insérer le code généré :
Latte\Compiler\Nodes\Php\Expression\AuxiliaryNode: à utiliser quand vous devez générer un fragment de code PHP représentant une expressionLatte\Compiler\Nodes\AuxiliaryNode: à utiliser plus généralement, quand vous devez insérer un bloc de code PHP représentant une ou plusieurs instructions
La raison majeure d'employer AuxiliaryNode plutôt que des nœuds standard (comme
StaticMethodCallNode) dans votre méthode print() ou dans une passe de compilation est de contrôler
la visibilité pour les passes de compilation suivantes, en particulier celles liées à la sécurité, comme le Sandbox.
Prenons un scénario : votre passe de compilation doit envelopper une expression fournie par l'utilisateur
($userExpr) dans un appel à une fonction utilitaire précise et de confiance,
myInternalSanitize($userExpr). Si vous créez un nœud standard
new FunctionCallNode('myInternalSanitize', [$userExpr]), il sera pleinement visible pour le parcours de l'AST. Si une
passe Sandbox s'exécute ensuite et que myInternalSanitize ne figure pas sur sa liste d'autorisation, le Sandbox
pourrait bloquer ou modifier cet appel et casser la logique interne de votre balise, alors même que vous, l'auteur
de la balise, savez que cet appel précis est sûr et nécessaire. Vous pouvez donc générer l'appel directement dans la closure
de l'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 de code PHP
$userExpr,
),
// IMPORTANT : passez tout de même ici le nœud de l'expression d'origine !
[$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.
Si le code PHP généré est bien masqué aux passes, les entrées de ce code (les nœuds représentant des données ou
des expressions de l'utilisateur) doivent malgré tout rester parcourables. C'est pourquoi le second argument du
constructeur d'AuxiliaryNode est capital. Vous devez y passer un tableau contenant tous les nœuds d'origine
(comme $userExpr dans l'exemple ci-dessus) qu'utilise votre closure. Le getIterator()
d'AuxiliaryNode produira ces nœuds, ce qui permettra aux passes de compilation comme le Sandbox de les
analyser à la recherche de problèmes.
Bonnes pratiques
- Une vocation claire : assurez-vous que votre balise a une vocation claire et nécessaire. Ne créez pas de balises pour des tâches que des filtres ou des fonctions résolvent facilement.
- Implémentez correctement
getIterator(): implémentez toujoursgetIterator()et produisez des références (&) vers tous les nœuds enfants (arguments, contenu) analysés depuis le template. C'est indispensable aux passes de compilation, à la sécurité (Sandbox) et aux optimisations futures éventuelles. - Propriétés publiques pour les nœuds : rendez publiques les propriétés qui portent des nœuds enfants, afin que les passes de compilation puissent les modifier au besoin.
- Utilisez
PrintContext::format(): servez-vous de la méthodeformat()pour générer du code PHP. Elle gère les guillemets, échappe correctement les marqueurs et ajoute automatiquement les commentaires de numéro de ligne. - Variables temporaires (
$__) : quand vous générez du code PHP d'exécution qui a besoin de variables temporaires (pour stocker des résultats intermédiaires, des compteurs de boucle), adoptez la convention du préfixe$__afin d'éviter les collisions avec les variables de l'utilisateur et les variables internes$ʟ_de Latte. - Imbrication et identifiants uniques : si votre balise peut être imbriquée ou a besoin d'un état propre à chaque
instance à l'exécution, utilisez
$context->generateId()dans votre méthodeprint()pour créer des suffixes uniques pour vos variables temporaires$__. - Providers pour les données externes : utilisez les providers (enregistrés via
Extension::getProviders()) pour accéder aux données ou services d'exécution ($this->global->…) plutôt que de coder des valeurs en dur ou de vous appuyer sur un état global. Préfixez les noms de providers. - Pensez aux n:attributs : si votre balise paire opère logiquement sur un seul élément HTML, Latte fournit
probablement une prise en charge automatique du
n:attribut. Gardez-le en tête pour le confort de l'utilisateur. Si vous créez une balise qui modifie des attributs, demandez-vous si unn:attributpur n'est pas la forme la plus appropriée. - Tests : écrivez des tests pour vos balises, couvrant à la fois l'analyse des différentes syntaxes possibles et la justesse de la sortie du code PHP généré.
En suivant ces principes, vous pourrez créer des balises personnalisées puissantes, robustes et maintenables, parfaitement intégrées au moteur de templates Latte.
Étudier les classes de nœuds qui font partie de Latte reste la meilleure façon d'apprendre tous les détails du processus d'analyse.