Création d'une extension
Une extension est une classe réutilisable qui permet de définir des balises, des filtres, des fonctions, des fournisseurs, etc. personnalisés.
Nous créons des extensions lorsque nous voulons réutiliser nos personnalisations Latte dans différents projets ou les partager avec d'autres. Il est également utile de créer une extension pour chaque projet Web qui contiendra toutes les balises et tous les filtres spécifiques que vous souhaitez utiliser dans les modèles de projet.
Classe d'extension
Extension est une classe héritant de Latte\Extension. Elle
est enregistrée dans Latte à l'aide de addExtension()
(ou via le fichier de configuration) :
$latte = new Latte\Engine;
$latte->addExtension(new MyLatteExtension);
Si vous enregistrez plusieurs extensions et qu'elles définissent des balises, des filtres ou des fonctions de même nom, la dernière extension ajoutée l'emporte. Cela implique également que vos extensions peuvent remplacer les tags/filtres/fonctions natifs.
Chaque fois que vous apportez une modification à une classe et que l'actualisation automatique n'est pas désactivée, Latte recompile automatiquement vos modèles.
Une classe peut mettre en œuvre l'une des méthodes suivantes :
abstract class Extension
{
/**
* Initializes before template is compiler.
*/
public function beforeCompile(Engine $engine): void;
/**
* Returns a list of parsers for Latte tags.
* @return array<string, callable>
*/
public function getTags(): array;
/**
* Returns a list of compiler passes.
* @return array<string, callable>
*/
public function getPasses(): array;
/**
* Returns a list of |filters.
* @return array<string, callable>
*/
public function getFilters(): array;
/**
* Returns a list of functions used in templates.
* @return array<string, callable>
*/
public function getFunctions(): array;
/**
* Returns a list of providers.
* @return array<mixed>
*/
public function getProviders(): array;
/**
* Returns a value to distinguish multiple versions of the template.
*/
public function getCacheKey(Engine $engine): mixed;
/**
* Initializes before template is rendered.
*/
public function beforeRender(Template $template): void;
}
Pour avoir une idée de ce à quoi ressemble l'extension, jetez un coup d'œil à la CoreExtension intégrée.
beforeCompile(Latte\Engine $engine): void
Appelé avant que le modèle ne soit compilé. Cette méthode peut être utilisée pour les initialisations liées à la compilation, par exemple.
getTags(): array
Appelé lorsque le modèle est compilé. Retourne un tableau associatif nom de la balise ⇒ appelable, qui sont des fonctions d'analyse de la balise.
public function getTags(): array
{
return [
'foo' => [FooNode::class, 'create'],
'bar' => [BarNode::class, 'create'],
'n:baz' => [NBazNode::class, 'create'],
// ...
];
}
La balise n:baz
représente un pur n:attribut, c'est-à-dire une balise qui ne peut être écrite que comme un
attribut.
Dans le cas des balises foo
et bar
, Latte reconnaîtra automatiquement s'il s'agit de paires, et si
c'est le cas, elles peuvent être écrites automatiquement en utilisant des n:attributs, y compris les variantes avec les
préfixes n:inner-foo
et n:tag-foo
.
L'ordre d'exécution de ces n:attributes est déterminé par leur ordre dans le tableau renvoyé par getTags()
.
Ainsi, n:foo
est toujours exécuté avant n:bar
, même si les attributs sont listés dans l'ordre
inverse dans la balise HTML comme <div n:bar="..." n:foo="...">
.
Si vous devez déterminer l'ordre des n:attributs sur plusieurs extensions, utilisez la méthode d'aide order()
,
où le paramètre before
xor after
détermine quelles balises sont ordonnées avant ou après la
balise .
public function getTags(): array
{
return [
'foo' => self::order([FooNode::class, 'create'], before: 'bar')]
'bar' => self::order([BarNode::class, 'create'], after: ['block', 'snippet'])]
];
}
getPasses(): array
Elle est appelée lorsque le modèle est compilé. Elle renvoie un tableau associatif name pass ⇒ callable, qui sont des fonctions représentant ce qu'on appelle des passes de compilation qui traversent et modifient l'AST.
Là encore, la méthode d'aide order()
peut être utilisée. La valeur des paramètres before
ou
after
peut être *
avec la signification avant/après tout.
public function getPasses(): array
{
return [
'optimize' => [Passes::class, 'optimizePass'],
'sandbox' => self::order([$this, 'sandboxPass'], before: '*'),
// ...
];
}
beforeRender(Latte\Engine $engine): void
Elle est appelée avant chaque rendu de modèle. La méthode peut être utilisée, par exemple, pour initialiser les variables utilisées pendant le rendu.
getFilters(): array
Il est appelé avant que le modèle soit rendu. Retourne les filtres sous la forme d'un tableau associatif nom du filtre ⇒ appelable.
public function getFilters(): array
{
return [
'batch' => [$this, 'batchFilter'],
'trim' => [$this, 'trimFilter'],
// ...
];
}
getFunctions(): array
Il est appelé avant que le modèle ne soit rendu. Retourne les fonctions sous la forme d'un tableau associatif nom de la fonction ⇒ appelable.
public function getFunctions(): array
{
return [
'clamp' => [$this, 'clampFunction'],
'divisibleBy' => [$this, 'divisibleByFunction'],
// ...
];
}
getProviders(): array
Elle est appelée avant le rendu du modèle. Renvoie un tableau de fournisseurs, qui sont généralement des objets qui
utilisent des balises au moment de l'exécution. Ils sont accessibles via $this->global->...
.
public function getProviders(): array
{
return [
'myFoo' => $this->foo,
'myBar' => $this->bar,
// ...
];
}
getCacheKey(Latte\Engine $engine): mixed
Elle est appelée avant le rendu du modèle. La valeur de retour fait partie de la clé dont le hachage est contenu dans le nom du fichier de modèle compilé. Ainsi, pour différentes valeurs de retour, Latte générera différents fichiers de cache.
Comment fonctionne Latte ?
Pour comprendre comment définir des balises personnalisées ou des passages de compilateur, il est essentiel de comprendre comment Latte fonctionne sous le capot.
La compilation de modèles dans Latte fonctionne de manière simpliste comme suit :
- Tout d'abord, le lexer segmente le code source du modèle en petits morceaux (tokens) pour faciliter le traitement.
- Ensuite, le parser convertit le flux de tokens en un arbre de nœuds significatif (l'arbre de syntaxe abstraite, AST).
- Enfin, le compilateur génère une classe PHP à partir de l'AST qui rend le modèle et le met en cache.
En fait, la compilation est un peu plus compliquée. Latte a deux lexers et parsers : un pour le modèle HTML et un pour le code PHP à l'intérieur des balises. De plus, l'analyse syntaxique ne s'exécute pas après la tokénisation, mais le lexer et l'analyse syntaxique s'exécutent en parallèle dans deux „threads“ et se coordonnent. C'est de la science-fiction :-)
En outre, toutes les balises ont leurs propres routines d'analyse. Lorsque l'analyseur rencontre une balise, il appelle sa fonction d'analyse (il renvoie Extension::getTags()). Son travail consiste à analyser les arguments de la balise et, dans le cas de balises appariées, le contenu interne. Elle renvoie un node qui devient une partie de l'AST. Voir Fonction d'analyse syntaxique des balises pour plus de détails.
Lorsque l'analyseur syntaxique termine son travail, nous avons un AST complet représentant le modèle. Le nœud racine est
Latte\Compiler\Nodes\TemplateNode
. Les nœuds individuels à l'intérieur de l'arbre représentent non seulement les
balises, mais aussi les éléments HTML, leurs attributs, les expressions utilisées à l'intérieur des balises, etc.
Ensuite, les passes du compilateur entrent en jeu. Il s'agit de fonctions (renvoyées par Extension::getPasses()) qui modifient l'AST.
L'ensemble du processus, depuis le chargement du contenu du modèle, en passant par l'analyse syntaxique, jusqu'à la génération du fichier résultant, peut être séquencé à l'aide de ce code, avec lequel vous pouvez expérimenter et vidanger les résultats intermédiaires :
$latte = new Latte\Engine;
$source = $latte->getLoader()->getContent($file);
$ast = $latte->parse($source);
$latte->applyPasses($ast);
$code = $latte->generate($ast, $file);
Exemple d'AST
Pour avoir une meilleure idée de l'AST, nous ajoutons un exemple. Il s'agit du modèle source :
{foreach $category->getItems() as $item}
<li>{$item->name|upper}</li>
{else}
no items found
{/foreach}
Et voici sa représentation sous forme d'AST :
Latte\Compiler\Nodes\TemplateNode( Latte\Compiler\Nodes\FragmentNode( - Latte\Essential\Nodes\ForeachNode( expression: Latte\Compiler\Nodes\Php\Expression\MethodCallNode( object: Latte\Compiler\Nodes\Php\Expression\VariableNode('$category') name: Latte\Compiler\Nodes\Php\IdentifierNode('getItems') ) value: Latte\Compiler\Nodes\Php\Expression\VariableNode('$item') content: Latte\Compiler\Nodes\FragmentNode( - Latte\Compiler\Nodes\TextNode(' ') - Latte\Compiler\Nodes\Html\ElementNode('li')( content: Latte\Essential\Nodes\PrintNode( expression: Latte\Compiler\Nodes\Php\Expression\PropertyFetchNode( object: Latte\Compiler\Nodes\Php\Expression\VariableNode('$item') name: Latte\Compiler\Nodes\Php\IdentifierNode('name') ) modifier: Latte\Compiler\Nodes\Php\ModifierNode( filters: - Latte\Compiler\Nodes\Php\FilterNode('upper') ) ) ) ) else: Latte\Compiler\Nodes\FragmentNode( - Latte\Compiler\Nodes\TextNode('no items found') ) ) ) )
Balises personnalisées
Trois étapes sont nécessaires pour définir une nouvelle balise :
- définition de la fonction d'analyse de la balise (responsable de l'analyse de la balise en un nœud)
- création d'une classe de nœud (responsable de la génération du code PHP et de la traversée de l'AST)
- enregistrement de la balise à l'aide de Extension::getTags()
Fonction d'analyse syntaxique des balises
L'analyse des balises est gérée par leur fonction d'analyse (celle renvoyée par Extension::getTags()). Son travail consiste à analyser et à vérifier les arguments contenus dans la
balise (elle utilise TagParser pour ce faire). De plus, si la balise est une paire, elle demandera à TemplateParser d'analyser et
de retourner le contenu interne. La fonction crée et renvoie un nœud, qui est généralement un enfant de
Latte\Compiler\Nodes\StatementNode
, et qui devient une partie de l'AST.
Nous créons une classe pour chaque nœud, ce que nous allons faire maintenant, et nous y plaçons élégamment la fonction
d'analyse en tant que fabrique statique. À titre d'exemple, essayons de créer la balise familière {foreach}
:
use Latte\Compiler\Nodes\StatementNode;
class ForeachNode extends StatementNode
{
// une fonction d'analyse syntaxique qui crée simplement un nœud pour l'instant
public static function create(Latte\Compiler\Tag $tag): self
{
$node = $tag->node = new self;
return $node;
}
public function print(Latte\Compiler\PrintContext $context): string
{
// le code sera ajouté plus tard
}
public function &getIterator(): \Generator
{
// le code sera ajouté plus tard
}
}
La fonction d'analyse syntaxique create()
reçoit un objet Latte\Compiler\Tag, qui contient des informations de base sur
la balise (s'il s'agit d'une balise classique ou d'un n:attribut, sur quelle ligne elle se trouve, etc.) et accède principalement
à Latte\Compiler\TagParser dans
$tag->parser
.
Si la balise doit avoir des arguments, vérifiez leur existence en appelant $tag->expectArguments()
. Les
méthodes de l'objet $tag->parser
sont disponibles pour les analyser :
parseExpression(): ExpressionNode
pour une expression de type PHP (par exemple10 + 3
)parseUnquotedStringOrExpression(): ExpressionNode
pour une expression ou une chaîne de caractères non citéeparseArguments(): ArrayNode
pour le contenu d'un tableau (par exemple,10, true, foo => bar
)parseModifier(): ModifierNode
pour un modificateur (par ex.|upper|truncate:10
)parseType(): expressionNode
pour un indice de type (par exemple,int|string
ouFoo\Bar[]
)
et un bas niveau Latte\Compiler\TokenStream opérant directement avec les jetons :
$tag->parser->stream->consume(...): Token
$tag->parser->stream->tryConsume(...): ?Token
Latte étend la syntaxe de PHP par petites touches, par exemple en ajoutant des modificateurs, des opérateurs ternaires
raccourcis, ou en permettant d'écrire des chaînes alphanumériques simples sans guillemets. C'est pourquoi nous utilisons le
terme PHP-like au lieu de PHP. Ainsi, la méthode parseExpression()
analyse foo
comme
'foo'
, par exemple. En outre, unquoted-string est un cas particulier de chaîne de caractères qui n'a pas
besoin d'être citée, mais qui n'a pas besoin d'être alphanumérique. Par exemple, il s'agit du chemin d'accès à un fichier
dans la balise {include ../file.latte}
. La méthode parseUnquotedStringOrExpression()
est utilisée pour
l'analyser.
L'étude des classes de nœuds qui font partie de Latte est la meilleure façon d'apprendre tous les détails minutieux du processus d'analyse.
Revenons à la balise {foreach}
. Dans cette balise, nous attendons des arguments de la forme
expression + 'as' + second expression
, que nous analysons comme suit :
use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\Nodes\Php\ExpressionNode;
use Latte\Compiler\Nodes\AreaNode;
class ForeachNode extends StatementNode
{
public ExpressionNode $expression;
public ExpressionNode $value;
public static function create(Latte\Compiler\Tag $tag): self
{
$tag->expectArguments();
$node = $tag->node = new self;
$node->expression = $tag->parser->parseExpression();
$tag->parser->stream->consume('as');
$node->value = $parser->parseExpression();
return $node;
}
}
Les expressions que nous avons écrites dans les variables $expression
et $value
représentent des
sous-nœuds.
Définissez les variables avec les sous-nœuds comme publics afin qu'ils puissent être modifiés dans les étapes de traitement ultérieures si nécessaire. Il est également nécessaire de les rendre disponibles pour la navigation.
Pour les balises appariées, comme la nôtre, la méthode doit également laisser TemplateParser analyser le contenu interne de
la balise. Ceci est géré par yield
, qui renvoie une paire [contenu interne, balise finale]. Nous stockons le
contenu interne dans la variable $node->content
.
public AreaNode $content;
public static function create(Latte\Compiler\Tag $tag): \Generator
{
// ...
[$node->content, $endTag] = yield;
return $node;
}
Le mot-clé yield
entraîne la fin de la méthode create()
, qui redonne le contrôle au
TemplateParser, qui continue à analyser le contenu jusqu'à ce qu'il atteigne la balise de fin. Il transmet ensuite le contrôle
à create()
, qui reprend là où il s'est arrêté. L'utilisation de la méthode yield
, renvoie
automatiquement Generator
.
Vous pouvez également transmettre à yield
un tableau de noms de balises pour lesquelles vous souhaitez arrêter
l'analyse s'ils apparaissent avant la balise de fin. Cela nous aide à mettre en œuvre la construction
{foreach}...{else}...{/foreach}
construction. Si {else}
apparaît, nous analysons le contenu qui le suit
dans $node->elseContent
:
public AreaNode $content;
public ?AreaNode $elseContent = null;
public static function create(Latte\Compiler\Tag $tag): \Generator
{
// ...
[$node->content, $nextTag] = yield ['else'];
if ($nextTag?->name === 'else') {
[$node->elseContent] = yield;
}
return $node;
}
Le nœud de retour termine l'analyse des balises.
Génération du code PHP
Chaque nœud doit implémenter la méthode print()
. Renvoie le code PHP qui rend la partie donnée du modèle
(code d'exécution). On lui passe un objet Latte\Compiler\PrintContext en paramètre, qui
possède une méthode utile format()
qui simplifie l'assemblage du code résultant.
La méthode format(string $mask, ...$args)
accepte les caractères de remplacement suivants dans le masque :
%node
imprime Node%dump
exporte la valeur vers PHP%raw
insère le texte directement sans aucune transformation%args
imprime ArrayNode comme arguments à l'appel de fonction%line
imprime un commentaire avec un numéro de ligne%escape(...)
échappe le contenu%modify(...)
applique un modificateur%modifyContent(...)
applique un modificateur aux blocs
Notre fonction print()
pourrait ressembler à ceci (nous négligeons la branche else
pour
simplifier) :
public function print(Latte\Compiler\PrintContext $context): string
{
return $context->format(
<<<'XX'
foreach (%node as %node) %line {
%node
}
XX,
$this->expression,
$this->value,
$this->position,
$this->content,
);
}
La variable $this->position
est déjà définie par la classe Latte\Compiler\Node et est définie par l'analyseur
syntaxique. Elle contient un objet Latte\Compiler\Position avec la position de la balise dans
le code source sous la forme d'un numéro de ligne et de colonne.
Le code d'exécution peut utiliser des variables auxiliaires. Pour éviter toute collision avec les variables utilisées par le
modèle lui-même, il est de convention de les préfixer avec les caractères $ʟ__
.
Il peut également utiliser des valeurs arbitraires au moment de l'exécution, qui sont transmises au modèle sous la forme de
fournisseurs à l'aide de la méthode Extension::getProviders(). Il y accède en utilisant
$this->global->...
.
Traversée de l'AST
Afin de traverser l'arbre AST en profondeur, il est nécessaire d'implémenter la méthode getIterator()
. Cela
permettra d'accéder aux sous-nœuds :
public function &getIterator(): \Generator
{
yield $this->expression;
yield $this->value;
yield $this->content;
if ($this->elseContent) {
yield $this->elseContent;
}
}
Notez que getIterator()
renvoie une référence. C'est ce qui permet aux visiteurs des nœuds de remplacer les
nœuds individuels par d'autres nœuds.
Si un nœud a des sous-nœuds, il est nécessaire d'implémenter cette méthode et de rendre tous les sous-nœuds disponibles. Sinon, une faille de sécurité pourrait être créée. Par exemple, le mode sandbox ne serait pas en mesure de contrôler les sous-nœuds et de garantir que les constructions non autorisées ne sont pas appelées dans ces derniers.
Étant donné que le mot-clé yield
doit être présent dans le corps de la méthode, même si celle-ci n'a pas de
nœuds enfants, écrivez-la comme suit :
public function &getIterator(): \Generator
{
if (false) {
yield;
}
}
AuxiliaryNode (nœud auxiliaire)
Si vous créez une nouvelle balise pour Latte, il est conseillé de créer une classe de nœuds dédiée, qui la représentera
dans l'arbre AST (voir la classe ForeachNode
dans l'exemple ci-dessus). Dans certains cas, la classe de nœuds
auxiliaire triviale AuxiliaryNode peut s'avérer
utile, car elle permet de passer le corps de la méthode print()
et la liste des nœuds rendus accessibles par la
méthode getIterator()
en tant que paramètres du constructeur :
// Latte\Compiler\Nodes\Php\Expression\AuxiliaryNode
// or Latte\Compiler\Nodes\AuxiliaryNode
$node = new AuxiliaryNode(
// body of the print() method:
fn(PrintContext $context, $argNode) => $context->format('myFunc(%node)', $argNode),
// nodes accessed via getIterator() and also passed into the print() method:
[$argNode],
);
Le compilateur passe
Les Compiler Passes sont des fonctions qui modifient les AST ou collectent des informations dans ces derniers. Elles sont renvoyées par la méthode Extension::getPasses().
Traverseur de nœuds
La façon la plus courante de travailler avec l'AST est d'utiliser un Latte\Compiler\NodeTraverser:
use Latte\Compiler\Node;
use Latte\Compiler\NodeTraverser;
$ast = (new NodeTraverser)->traverse(
$ast,
enter: fn(Node $node) => ...,
leave: fn(Node $node) => ...,
);
La fonction enter (c'est-à-dire le visiteur) est appelée lorsqu'un nœud est rencontré pour la première fois, avant que ses sous-nœuds ne soient traités. La fonction leave est appelée après que tous les sous-nœuds aient été visités. Un modèle commun est que la fonction enter est utilisée pour collecter certaines informations, puis la fonction leave effectue des modifications sur cette base. Au moment où leave est appelée, tout le code à l'intérieur du nœud aura déjà été visité et les informations nécessaires auront été collectées.
Comment modifier l'AST ? La manière la plus simple est de changer simplement les propriétés des nœuds. La deuxième façon
est de remplacer entièrement le nœud en retournant un nouveau nœud. Exemple : le code suivant changera tous les entiers de
l'AST en chaînes de caractères (par exemple, 42 sera changé en '42'
).
use Latte\Compiler\Nodes\Php;
$ast = (new NodeTraverser)->traverse(
$ast,
leave: function (Node $node) {
if ($node instanceof Php\Scalar\IntegerNode) {
return new Php\Scalar\StringNode((string) $node->value);
}
},
);
Un AST peut facilement contenir des milliers de nœuds, et les parcourir tous peut être lent. Dans certains cas, il est possible d'éviter une traversée complète.
Si vous cherchez tous les Html\ElementNode
dans un arbre, vous savez qu'une fois que vous avez vu
Php\ExpressionNode
, il est inutile de vérifier également tous ses nœuds enfants, car le HTML ne peut pas être à
l'intérieur des expressions. Dans ce cas, vous pouvez demander au traverseur de ne pas faire de récursion dans le nœud de
classe :
$ast = (new NodeTraverser)->traverse(
$ast,
enter: function (Node $node) {
if ($node instanceof Php\ExpressionNode) {
return NodeTraverser::DontTraverseChildren;
}
// ...
},
);
Si vous ne recherchez qu'un seul nœud spécifique, il est également possible d'interrompre entièrement la traversée après l'avoir trouvé.
$ast = (new NodeTraverser)->traverse(
$ast,
enter: function (Node $node) {
if ($node instanceof Nodes\ParametersNode) {
return NodeTraverser::StopTraversal;
}
// ...
},
);
Aides pour les nœuds
La classe Latte\Compiler\NodeHelpers fournit quelques méthodes qui peuvent trouver des noeuds AST qui satisfont un certain callback etc. Quelques exemples sont montrés :
use Latte\Compiler\NodeHelpers;
// trouve tous les nœuds d'éléments HTML
$elements = NodeHelpers::find($ast, fn(Node $node) => $node instanceof Nodes\Html\ElementNode);
// trouve le premier noeud de texte
$text = NodeHelpers::findFirst($ast, fn(Node $node) => $node instanceof Nodes\TextNode);
// convertit le noeud de valeur PHP en valeur réelle
$value = NodeHelpers::toValue($node);
// convertit un noeud textuel statique en chaîne de caractères
$text = NodeHelpers::toText($node);