Introduction
Twig 4.0 apporte un changement structurel important pour tous les développeurs qui maintiennent des extensions définissant des opérateurs personnalisés : la méthode getOperators() disparaît au profit de getExpressionParsers(). Derrière ce renommage se cache une refonte complète du moteur de parsing des expressions, basée sur un algorithme vieux de plus de 50 ans mais redoutablement efficace.
Cet article vous explique ce qui change, pourquoi ce changement améliore l'architecture interne de Twig, et comment adapter vos extensions existantes.
Le problème de l'ancienne architecture : un tableau magique sans comportement
Dans Twig 3, déclarer un opérateur personnalisé dans une extension passait par la méthode getOperators(), qui retournait deux tableaux imbriqués avec une structure très spécifique :
public function getOperators(): array
{
return [
// Opérateurs unaires
[
'not' => [
'precedence' => 70,
'class' => NotUnary::class
],
],
// Opérateurs binaires
[
'~' => [
'precedence' => 27,
'class' => ConcatBinary::class,
'associativity' => ExpressionParser::OPERATOR_LEFT,
],
],
];
}
Ce tableau décrit les opérateurs, mais la logique de parsing était ailleurs, enfouie dans le cœur de Twig. Cette séparation entre la déclaration et le comportement rendait le code difficile à maintenir, difficile à étendre, et surtout difficile à comprendre pour quiconque souhaitait ajouter des opérateurs non conventionnels.
Le vrai problème : le système était un monolithe. Toute la logique de précédence et d'associativité était câblée dans ExpressionParser, une classe centrale qui devait tout connaître de tous les opérateurs. Ajouter un opérateur exotique (ternaire, avec un comportement custom) relevait du bricolage.
L'algorithme de Vaughan Pratt : simple, élégant, puissant
Pour comprendre la solution apportée par Twig 4.0, il faut remonter à 1973. Vaughan Pratt publie alors "Top Down Operator Precedence", un article décrivant une technique de parsing si simple qu'elle en paraît presque triviale — et pourtant remarquablement expressive.
L'idée centrale du Pratt Parser (ou Top Down Operator Precedence) : chaque token sait lui-même comment se parser. Plutôt que d'avoir un parseur central qui connaît tous les opérateurs et leurs règles, chaque opérateur embarque sa propre logique sous deux formes :
nud(null denotation) : comment l'opérateur se comporte quand il apparaît en position préfixe (début d'une expression, sans token à gauche). C'est le cas des opérateurs unaires commenotou-.led(left denotation) : comment l'opérateur se comporte quand il apparaît en position infixe (avec un token à sa gauche). C'est le cas des opérateurs binaires comme+,~,and, etc.
Chaque opérateur porte également sa précédence (ou binding power), qui détermine avec quelle force il "attire" les tokens autour de lui. Le parseur principal devient alors un simple algorithme récursif très court, qui délègue tout le comportement aux opérateurs eux-mêmes.
Cet algorithme est notamment utilisé dans V8 (le moteur JavaScript de Chrome) et dans de nombreux compilateurs modernes. Sa force : il est extensible par nature. Ajouter un nouvel opérateur ne touche pas au parseur central.
Ce qui change concrètement : getExpressionParsers()
Dans Twig 4.0, getOperators() est remplacée par getExpressionParsers(), qui retourne un tableau d'objets implémentant une interface dédiée. Chaque objet encapsule à la fois la déclaration et le comportement de l'opérateur.
Voici à quoi ressemble la nouvelle API :
public function getExpressionParsers(): array
{
return [
new NotUnaryExpressionParser(),
new ConcatBinaryExpressionParser(),
];
}
Chaque ExpressionParser implémente les méthodes correspondant au modèle Pratt :
getTag(): le token déclencheur (not,~,and...)getPrecedence(): la précédence de l'opérateurparse()ouparseInfix(): la logique de parsing, directement dans l'objet
Ce changement apporte plusieurs avantages concrets :
Cohésion accrue : la déclaration et le comportement sont co-localisés dans la même classe. Plus besoin de jongler entre un tableau de config et une classe AST séparée.
Extensibilité native : ajouter un opérateur ternaire custom, un opérateur avec une syntaxe non-standard, ou un opérateur dont la précédence dépend du contexte devient possible sans modifier le cœur de Twig.
Lisibilité : le code d'un ExpressionParser est auto-documenté. On voit immédiatement ce que fait l'opérateur, comment il se parse, et quelle précédence il a.
Impact sur vos extensions : ce que vous devez faire
Si vous maintenez une extension Twig qui définit des opérateurs via getOperators(), vous devrez migrer vers getExpressionParsers() pour Twig 4.0.
Étapes de migration :
- Identifier toutes vos extensions implémentant
getOperators(). - Créer une classe par opérateur, implémentant l'interface
ExpressionParserInterface(ou étendant la classe de base appropriée selon qu'il s'agit d'un opérateur unaire ou binaire). - Déplacer la logique de parsing dans ces classes (précédence, associativité, construction du nœud AST).
- Remplacer
getOperators()pargetExpressionParsers()dans votre extension, en retournant les instances de vos nouvelles classes.
Pour les opérateurs simples (unaires ou binaires classiques), Twig fournit des classes de base à étendre, ce qui limite la quantité de code à écrire. La migration est plus une réorganisation qu'une réécriture.
⚠️ Breaking change :
getOperators()est supprimée dans Twig 4.0. Si vous utilisez des extensions tierces qui définissent des opérateurs, vérifiez leur compatibilité avant de migrer.
Conclusion
Le passage au Pratt Parser dans Twig 4.0 est un bel exemple de refactoring motivé par des principes d'ingénierie solides : cohésion, extensibilité, et séparation des responsabilités. Ce n'est pas un simple renommage de méthode — c'est une refonte architecturale qui rend le système de parsing réellement ouvert à l'extension.
Pour la majorité des projets Symfony utilisant Twig sans extensions custom définissant des opérateurs, ce changement est transparent. Pour les mainteneurs d'extensions, la migration est nécessaire mais reste accessible.
L'article original de Fabien Potencier sur le blog Symfony détaille les PR associés (#4543, #4550, #4578, #4775) et donne plus de détails sur l'implémentation interne. Si vous êtes curieux de l'algorithme de Pratt lui-même, c'est une excellente lecture.