Twig 3.28.0 est sorti le 3 juillet 2026, et à première vue, c'est une release mineure de plus dans la série 3.x. En creusant le changelog, on découvre autre chose : c'est la dernière étape de préparation avant Twig 4.0, une majeure qui touche deux fondations du moteur de template — la sandbox et le parseur d'expressions. Le blog Symfony a lancé en parallèle une série « New in Twig 4.0 » qui détaille ces deux chantiers article par article. Pour une équipe qui maintient des extensions Twig personnalisées, des sandbox multi-tenant ou simplement une base de templates volumineuse, ce n'est pas une lecture optionnelle : plusieurs comportements actuels deviennent des erreurs bloquantes en 4.0, et la bonne nouvelle est que Twig 3.28 permet de tester ce futur dès maintenant, sans attendre la sortie de la majeure.
Twig 3.28.0 : la release qui prépare le terrain
Sortie le 3 juillet 2026 sous la houlette de Fabien Potencier, Twig 3.28.0 embarque son lot habituel de correctifs, mais surtout une série de dépréciations qui dessinent exactement le contour de Twig 4.0. Rien de tout cela n'est encore cassant : chaque changement de comportement reste optionnel ou signalé par un warning de dépréciation, ce qui laisse le temps de qualifier une base de code avant la bascule.
- Numéros de colonne dans les erreurs —
Error::getTemplateColumn()etToken::getOffset()pointent l'endroit exact d'une erreur de syntaxe sur une ligne dense, pas seulement le numéro de ligne - Appel dynamique de macro natif — l'opérateur point ressuscite la syntaxe
{{ forms.(champ)(nom, valeur) }}, sans passer par le détourattribute()déconseillé - Sandbox affinée — flag
always_allowed_in_sandboxsur les filtres, fonctions et tests, et allow-list dédiée pour les tests dansSecurityPolicy - Performance — la sandbox ne wrap plus systématiquement les arguments avec un contrôle
__toString; seuls les types PHP réellement coercibles en chaîne sont vérifiés - Nouvelles dépréciations — utiliser
macro,extendsouuseen dehors de la racine d'un template, oublockà l'intérieur d'un nœud de capture (set), lève désormais un warning appelé à devenir une erreur en 4.0
Deux de ces chantiers — la sandbox et le parseur d'expressions — sont suffisamment structurants pour mériter chacun leur article dédié dans la série Symfony. Voyons ce qui change concrètement, et pourquoi.
La sandbox Twig comblait des angles morts silencieux
La sandbox Twig sert à exécuter des templates dont on ne maîtrise pas entièrement le contenu — un CMS avec des blocs éditables par les utilisateurs, un générateur de rapports multi-tenant, un moteur d'e-mails personnalisables par le client. Le principe : une SecurityPolicy définit une allow-list de tags, filtres, fonctions et méthodes ; tout le reste lève une exception. Le problème identifié par l'équipe Twig, documenté dans l'article « New in Twig 4.0: A Stricter Sandbox », c'est que cette allow-list avait deux angles morts en Twig 3.x. D'une part, les tests — l'opérateur is — n'étaient jamais vérifiés par la politique de sécurité, alors qu'un test comme constant(...) donne un accès en lecture à n'importe quelle constante PHP du projet. D'autre part, cinq constructions restaient silencieusement autorisées quelle que soit la politique définie : les balises extends, use, parent, block, et la fonction attribute().
{# Politique définie sans allowedTests : la sandbox Twig 3.x ne vérifie pas les tests #}
{% if 'APP_SECRET' is constant('APP_SECRET') %}
{{ dump() }}
{% endif %}
{# attribute() et parent() restent autorisés même hors politique, en Twig 3.x #}
{{ attribute(article, 'getInternalNotes') }}Sur un template soumis à la sandbox avec une politique restrictive, ces deux extraits s'exécutent sans lever la moindre erreur en Twig 3.x — alors qu'ils exposent potentiellement une variable d'environnement ou une méthode interne non prévue pour l'utilisateur final du template.
Ce que corrige Twig 4.0, disponible en avant-première dès 3.28
Twig 4.0 transforme les tests en allow-list de premier ordre, au même rang que les tags, filtres et fonctions. Le constructeur de SecurityPolicy gagne un sixième argument, allowedTests, et toute politique qui l'omet refuse désormais explicitement les tests non listés, avec une exception dédiée SecurityNotAllowedTestError.
<?php
// SecurityPolicy Twig 4.0 — les tests deviennent une allow-list de premier ordre
use Twig\Sandbox\SecurityPolicy;
$policy = new SecurityPolicy(
allowedTags: ['if', 'for', 'set'],
allowedFilters: ['escape', 'upper', 'length'],
allowedMethods: [
'Article' => ['getTitle', 'getExcerpt'],
],
allowedProperties: [],
allowedFunctions: ['range'],
allowedTests: ['divisible by'], // nouveau 6e argument en Twig 4.0
);- Tests jugés sûrs par défaut —
defined,empty,even,odd,iterable,same as,nulletdivisible byrestent utilisables sans figurer dansallowedTests - Exception volontaire —
constant(...)n'est jamais auto-autorisé : donner accès aux constantes PHP est un choix explicite du développeur, jamais un défaut
La deuxième correction ferme les cinq portes dérobées : extends, use, parent, block et attribute() obéissent désormais à la politique de sécurité comme n'importe quel autre tag ou fonction. Pour limiter le coût de cette vérification sur les constructions réellement inoffensives, Twig 4.0 introduit un flag always_allowed_in_sandbox : un filtre, une fonction ou un test marqué ainsi échappe complètement à l'enregistrement dans la politique, avec un gain de performance mesurable. Une trentaine de tags, filtres et fonctions du cœur de Twig — if, for, escape, upper, length, cycle parmi d'autres — sont d'ores et déjà marqués ainsi, tandis que include, random, json_encode ou constant restent volontairement hors de cette liste : résolution de template, non-déterminisme ou introspection PHP, chacun pour une raison de sécurité précise.
Le point le plus utile pour une équipe qui gère une sandbox en production aujourd'hui : ce comportement 4.0 est déjà activable en Twig 3.28 via $policy->setStrict(true). C'est le moyen le plus fiable de savoir, avant la montée de version, si une politique de sécurité existante laissait passer plus que prévu.
<?php
// Active dès Twig 3.28 le comportement strict de la sandbox Twig 4.0
use Twig\Sandbox\SecurityPolicy;
$policy = new SecurityPolicy(
allowedTags: ['if', 'for'],
allowedFilters: ['escape'],
allowedMethods: [],
allowedProperties: [],
allowedFunctions: [],
);
$policy->setStrict(true);
// Toute génération de template soumise à cette politique lève désormais
// une SecurityNotAllowedTestError ou SecurityNotAllowedFunctionError
// au lieu de laisser passer silencieusement `is constant(...)`, `attribute()` ou `parent()`.Les expression parsers : la fin d'un monolithe de plus de 1000 lignes
Le second chantier de Twig 4.0 touche un morceau plus intime du moteur : la manière dont les extensions déclarent leurs opérateurs personnalisés. En Twig 3.x, la méthode getOperators() retourne un tableau imbriqué décrivant chaque opérateur — précédence, classe de nœud, associativité — mais la logique de parsing elle-même reste hardcodée dans une classe unique, Twig\ExpressionParser, qui dépasse le millier de lignes. Une extension peut ajouter un opérateur comme donnée, jamais modifier la façon dont il est effectivement analysé : la grammaire de Twig reste fermée.
Besoin d'un expert Symfony ?
Réserver un appel →<?php
// Twig 3.x — un opérateur personnalisé se déclare comme donnée brute
declare(strict_types=1);
namespace App\Twig;
use App\Twig\Node\RepeatBinary;
use Twig\Extension\AbstractExtension;
use Twig\ExpressionParser;
final class StringToolsExtension extends AbstractExtension
{
public function getOperators(): array
{
return [
[], // opérateurs unaires
[
'repeat' => [
'precedence' => 30,
'class' => RepeatBinary::class,
'associativity' => ExpressionParser::OPERATOR_LEFT,
],
],
];
}
}Twig 4.0 renverse l'approche avec getExpressionParsers(), qui retourne une liste d'objets. Chacun encapsule tout ce qui concerne son opérateur : nom, précédence, associativité, et la logique de parsing elle-même. L'algorithme sous-jacent est du Pratt parsing — Top Down Operator Precedence, formalisé par Vaughan Pratt en 1973 — qui distingue deux familles de parseurs : les prefix parsers, qui démarrent une expression (un littéral, l'opérateur unaire not, une parenthèse), et les infix parsers, qui l'étendent (+, le filtre |, l'accès attribut ., un appel de fonction). Concrètement, ajouter un opérateur binaire repeat qui répète une chaîne se résume à deux petites classes.
<?php
// Twig 4.0 — le parser possède sa propre logique de parsing
declare(strict_types=1);
namespace App\Twig;
use App\Twig\Node\RepeatBinary;
use Twig\Extension\AbstractExtension;
use Twig\ExpressionParser\Infix\BinaryOperatorExpressionParser;
final class StringToolsExtension extends AbstractExtension
{
public function getExpressionParsers(): array
{
return [
new BinaryOperatorExpressionParser(
RepeatBinary::class,
'repeat',
30,
),
];
}
}<?php
// Le nœud d'expression compile l'opérateur en PHP natif
declare(strict_types=1);
namespace App\Twig\Node;
use Twig\Compiler;
use Twig\Node\Expression\Binary\AbstractBinary;
final class RepeatBinary extends AbstractBinary
{
public function compile(Compiler $compiler): void
{
$compiler
->raw('str_repeat(')
->subcompile($this->getNode('left'))
->raw(', ')
->subcompile($this->getNode('right'))
->raw(')');
}
}{{ '=' repeat 10 }}
{# affiche : ========== #}
{{ 'ab' repeat 2 ~ '!' }}
{# affiche : abab! #}Ce découpage a un effet qu'on ne voit pas immédiatement en écrivant une extension : les filtres, l'accès attribut, les appels de fonction et le ternaire deviennent eux aussi des expression parsers, traités par le même registre et la même table de précédence que les opérateurs personnalisés. Pour éviter qu'un nom d'opérateur entre en collision avec un filtre du même nom, chaque parseur déclare via getOperatorTokens() les tokens qu'il possède ; un parseur comme celui du littéral n'en possède aucun, ce qui laisse le nom entièrement libre pour un filtre. Une fois tous les constructs convertis à ce modèle, la classe Twig\ExpressionParser n'a plus de raison d'exister : elle est purement et simplement supprimée en Twig 4.0.
Bonne nouvelle pour qui maintient une extension avec des opérateurs personnalisés : getExpressionParsers() fonctionne déjà depuis Twig 3.21. Migrer une extension dès aujourd'hui la rend compatible à la fois avec la 3.x courante et avec la 4.0 à venir, sans code de compatibilité conditionnel. getOperators() reste utilisable pour l'instant, mais avec un message de dépréciation qui ne laisse aucun doute sur l'échéance.
Ce qui casse concrètement sur une base de templates existante
Sur un projet Symfony avec des centaines de templates accumulés depuis plusieurs années, trois familles de changements méritent un audit avant la bascule vers Twig 4.0.
- Macros,
extendsetusehors racine — un template qui déclare une macro ou hérite d'un layout à l'intérieur d'un bloc conditionnel ou d'une boucle fonctionnait par tolérance ; c'est déprécié depuis 3.28 et deviendra une erreur en 4.0 blockà l'intérieur d'unset— capturer un bloc nommé dans une variable via un nœud de capture est également marqué obsolète par le nouveauCorrectnessNodeVisitor- Extensions avec
getOperators()— tout opérateur personnalisé déclaré à l'ancienne cessera de fonctionner en 4.0 une foisTwig\ExpressionParsersupprimé - Sandbox sans
allowedTests— une politique de sécurité qui ne liste pas explicitement les tests autorisés doit être révisée, sous peine de blocage inattendu au premierisnon couvert - Appels de macro dynamiques via
attribute()— le contournement historique reste fonctionnel, mais l'opérateur point natif introduit en 3.28 le rend inutile
{# Twig 3.x, avant 3.28 — contournement via attribute() #}
{% import 'forms.html.twig' as forms %}
{% set widget = 'textField' %}
{{ attribute(forms, widget)(name, value) }}
{# Twig 3.28+ — appel dynamique natif via l'opérateur point #}
{% import 'forms.html.twig' as forms %}
{% set widget = 'textField' %}
{{ forms.(widget)(name, value) }}Ce dernier point rejoint un sujet déjà traité sur ce blog : la composition de macros et de TwigComponent profite directement de cette syntaxe plus lisible, notamment quand un composant délègue son rendu à une macro choisie dynamiquement selon une prop. D'autres changements plus discrets méritent aussi un passage en revue : la classe Twig\Markup est marquée @final en 3.28 et le deviendra réellement en 4.0, include() retourne désormais un objet Markup plutôt qu'une chaîne brute — ce qui évite un ré-échappement accidentel — et un Markup vide n'est plus considéré comme truthy dans un test if. Aucun de ces points ne casse un projet du jour au lendemain, mais chacun peut expliquer un comportement différent après montée de version si le test correspondant n'existe pas.
Une feuille de route de migration réaliste
La bonne nouvelle de cette majeure, c'est qu'elle ne surprend personne : chaque comportement de Twig 4.0 est déjà accessible en 3.28, en option ou sous forme de warning. La feuille de route tient en quatre étapes.
- Épingler Twig 3.28 et lancer la suite de tests avec les dépréciations promues en échec, pour repérer les macros/extends/use mal placés et les blocs capturés dans un
set - Ajouter
allowedTestsà chaqueSecurityPolicyexistante, puis activersetStrict(true)en environnement de test pour vérifier qu'aucun template ne dépend des angles morts historiques - Migrer toute extension personnalisée de
getOperators()versgetExpressionParsers()— le code obtenu tourne identiquement sur Twig 3.21+ et sur Twig 4.0 - Remplacer les contournements
attribute()utilisés pour des appels de macro dynamiques par la syntaxe point native
# Épingle Twig 3.28 le temps de qualifier la migration
composer require "twig/twig:^3.28"
# Fait tourner la suite de tests avec les dépréciations promues en échec
SYMFONY_DEPRECATIONS_HELPER=max[total]=0 \
php bin/phpunit --testsuite=functionalCette itération de Twig s'inscrit dans un fil plus large de l'écosystème Symfony ces derniers mois : d'un côté des workers Messenger HTTP-less en Symfony 8.1 qui rendent des templates Twig sans jamais voir passer de requête, de l'autre un frontend qui continue d'évoluer avec Symfony UX 3.3, et maintenant un moteur de template qui referme ses failles de sécurité sans imposer de réécriture brutale. Qualifier une sandbox Twig existante, migrer une extension personnalisée vers les nouveaux expression parsers, ou simplement auditer une base de templates héritée avant la bascule vers Twig 4.0 : c'est exactement le type de chantier qui gagne à être mené avec un regard extérieur habitué à ce genre de montée de version majeure, pendant que les warnings de dépréciation d'aujourd'hui sont encore faciles à corriger.
Besoin d'un expert Symfony ?
20 ans d'expérience sur l'écosystème PHP/Symfony.