Aller au contenu principal
Retour au blog

Twig 3 en 2025 : apply, with et TwigComponent dans Symfony

Flavien Métivier28 janvier 20258 min

À la SymfonyOnline de janvier 2025, Fabien Potencier posait la question sans détour : Twig, encore pertinent en 2025 ? La réponse n'est pas nostalgique — elle est technique. Twig 3.x, co-maintenu avec Symfony 7.x et documenté sur twig.symfony.com, n'est pas un moteur figé dans les années 2010 : il a absorbé les apports de PHP 8, durci son typage, supprimé les patterns douteux hérités de Twig 1, et s'est intégré au nouveau modèle de composants de Symfony UX. Si ton projet Symfony tourne encore sur des templates écrits comme en 2018 — {% filter %}, variables de boucle qui fuient dans le scope parent, extensions sans types — cet article est pour toi.

Ce que Twig 3 a vraiment supprimé — et pourquoi ça compte

La migration de Twig 2 vers Twig 3 ne casse pas grand-chose en surface — la majorité du code compile toujours. Mais Twig 3 expurge tout ce qui était deprecated depuis Twig 2.x, et c'est là que les projets se retrouvent bloqués sans le savoir. Les balises {% filter %} et {% spaceless %} disparaissent. Les alias sameas et divisibleby écrits sans espace ne sont plus reconnus. Les classes Twig_Extension non-namespacées héritées de Twig 1 n'existent plus. En mode strict_variables, toute variable inexistante remonte une erreur au lieu de rendre silencieusement une chaîne vide. C'est inconfortable à migrer — et c'est exactement le but.

  • {% filter upper %}...{% endfilter %} → remplacé par {% apply upper %}...{% endapply %}
  • {% spaceless %}...{% endspaceless %} → {% apply spaceless %}...{% endapply %}
  • Alias sameas / divisibleby sans espace → same as / divisible by
  • Classes Twig_* non-namespacées (héritage Twig 1) supprimées
  • _self comme argument d'include pour auto-inclusion retiré
  • Accès aux variables parent dans un {% for %} sans with : comportement durci

apply : chaîner les filtres sur un bloc entier

La balise apply est la vraie nouveauté ergonomique de Twig 3 pour les templates lourds. Elle permet d'appliquer un ou plusieurs filtres à un bloc de contenu entier, sans passer par une variable intermédiaire. C'est particulièrement utile pour le formatage de zones HTML, l'application de transformations markdown_to_html sur des blocs étendus, ou pour combiner plusieurs filtres d'assainissement avant de marquer le contenu comme raw.

{# Avant — Twig 2, supprimé en Twig 3 #}
{% filter upper|trim %}
    Contenu à transformer
{% endfilter %}

{# Twig 3 — syntaxe apply #}
{% apply upper|trim %}
    Contenu à transformer
{% endapply %}

{# Cas concret : rendre un bloc markdown en HTML #}
{% apply markdown_to_html %}
# Titre de la section

Paragraphe avec **gras** et _italique_.
Lien vers [la documentation](https://twig.symfony.com/doc/3.x).
{% endapply %}

{# Combiner avec un filtre custom d'assainissement #}
{% apply sanitize_html|raw %}
    <p>Contenu utilisateur non filtré</p>
{% endapply %}

Le chaînage fonctionne avec n'importe quel filtre enregistré, y compris tes filtres métier. Un pattern particulièrement utile : combiner apply avec un filtre d'assainissement custom puis raw pour les zones où tu construis du HTML dynamique. L'intention est explicite dans le template, pas enfouie dans un helper PHP anonyme.

with : isoler le scope sans polluer le contexte parent

La balise with crée un scope fermé. Tout ce qui est déclaré à l'intérieur — via {% set %} ou via des variables passées en paramètre — reste invisible hors du bloc. C'est la réponse de Twig 3 à un problème classique : les variables de boucle ou de macro qui fuient dans le template parent et provoquent des bugs de rendu difficiles à tracer en production.

{# Scope isolé : foo n'est pas accessible hors du bloc #}
{% with %}
    {% set foo = 'valeur locale' %}
    <p>{{ foo }}</p>
{% endwith %}
{# {{ foo }} ici → RuntimeError en mode strict_variables #}

{# Variables injectées directement dans le scope #}
{% with { label: 'Enregistré', icon: 'check' } %}
    <span class="badge badge--{{ icon }}">{{ label }}</span>
{% endwith %}

{# Combiné avec include pour un contexte propre et explicite #}
{% with { product: featured_product, highlight: true } %}
    {% include 'partials/_product_card.html.twig' only %}
{% endwith %}

Le mot-clé only sur l'include est complémentaire : il empêche le template inclus d'accéder aux variables du scope appelant. Ensemble, with + only constituent le pattern le plus propre pour les composants Twig sans état dans un projet Symfony 7.2. Chaque include devient un appel à interface explicite, pas un héritage implicite de contexte global.

Extensions typées PHP 8 : sortir de l'ère des closures anonymes

Les extensions Twig custom sont souvent le coin le plus archaïque d'un projet Symfony : closures anonymes sans type hint, méthodes sans retour déclaré, extensions monolithiques qui enregistrent vingt filtres. PHP 8.1 a introduit la syntaxe first-class callable ($this->maMethode(...)), qui s'intègre naturellement avec Twig 3 — tu passes une référence de méthode typée, et l'IDE comprend les signatures en entrée comme en sortie. PHP 8.4, stable depuis novembre 2024, renforce encore la vérification des types au runtime.

<?php
// src/Twig/Extension/PriceExtension.php
namespace App\Twig\Extension;

use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
use Twig\TwigFunction;

final class PriceExtension extends AbstractExtension
{
    public function getFilters(): array
    {
        return [
            new TwigFilter('price', $this->formatPrice(...)),
            new TwigFilter('price_vat', $this->formatPriceWithVat(...)),
        ];
    }

    public function getFunctions(): array
    {
        return [
            new TwigFunction('currency_symbol', $this->currencySymbol(...)),
        ];
    }

    private function formatPrice(float $amount, string $currency = 'EUR'): string
    {
        return number_format($amount, 2, ',', '\u{202F}') . '\u{00A0}' . $currency;
    }

    private function formatPriceWithVat(float $amount, float $vatRate = 20.0): string
    {
        return $this->formatPrice($amount * (1 + $vatRate / 100));
    }

    private function currencySymbol(string $currency): string
    {
        return match ($currency) {
            'EUR' => '€',
            'USD' => '
#x27;, 'GBP' => '£', default => $currency, }; } }

Besoin d'un expert Symfony ?

Réserver un appel

Comparé à l'ancienne approche — tableaux de closures anonymes sans types — la lisibilité est sans appel. Les types sont vérifiés par PHP 8.4 au runtime, les refactors sont guidés par l'IDE, et Twig remonte une TypeError claire si tu passes un string là où un float est attendu, au lieu de silencieusement retourner 0 ou une chaîne vide.

Symfony UX TwigComponent : le composant qui remplace les includes copier-coller

Symfony UX TwigComponent (installé via composer require symfony/ux-twig-component) pousse la logique de composant plus loin que with + include. Un composant est une classe PHP annotée #[AsTwigComponent] liée à un template Twig : il encapsule la logique de présentation, expose des propriétés typées, et s'utilise avec la syntaxe HTML-first <twig:NomDuComposant />. Pour un projet Symfony 7.2, c'est la réponse structurelle à la dette technique des templates monolithiques et des partials dupliqués à dix endroits.

<?php
// src/Twig/Components/Alert.php
namespace App\Twig\Components;

use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;

#[AsTwigComponent]
class Alert
{
    public string $type = 'info';    // 'info' | 'success' | 'warning' | 'error'
    public string $message = '';
    public bool $dismissible = false;
}
{# templates/components/Alert.html.twig #}
<div class="alert alert--{{ type }}{% if dismissible %} alert--dismissible{% endif %}"
     role="alert">
    <p>{{ message }}</p>
    {% if dismissible %}
        <button type="button" class="alert__close" aria-label="Fermer">&#x00D7;</button>
    {% endif %}
</div>

{# Utilisation dans n'importe quel template du projet #}
<twig:Alert type="success" message="Profil mis à jour avec succès." />
<twig:Alert type="warning" message="Session expire dans 5 minutes." :dismissible="true" />

La syntaxe :dismissible="true" (avec le deux-points) passe une expression Twig évaluée, pas une chaîne littérale — c'est la différence clé avec un include ordinaire. Le composant a une interface contractuelle typée : Alert::$type peut être validé, documenté dans l'IDE, et refactoré sans grep manuel sur tous les templates. Quand tu passes à un LiveComponent (Symfony UX AsLiveComponent), la même interface PHP pilote aussi la réactivité côté client — le chemin de migration est naturel.

strict_variables : rendre les bugs visibles avant la production

Un des patterns les plus dangereux dans les projets Twig hérités : accéder à une variable ou une propriété inexistante retourne silencieusement null. En production, une faute de frappe dans un nom de variable ne plante pas — elle affiche juste un champ vide. Twig 3 n'a pas changé ce comportement par défaut, mais l'option strict_variables: true devrait être activée dans tous les environnements dev et test de tes projets Symfony 7.2.

# config/packages/twig.yaml (dev + test)
twig:
    strict_variables: true

# config/packages/prod/twig.yaml
# Sur un projet bien couvert par les tests fonctionnels,
# strict_variables: true en prod est atteignable et recommandé.
# Sur un legacy : commencer par dev/test, corriger les RuntimeError, puis passer en prod.
twig:
    strict_variables: false

Avec strict_variables: true, un {{ user.profile.bio }}profile est null lève une Twig\Error\RuntimeError immédiatement. En développement, le bug est visible. Sans ce mode, le template rendait une chaîne vide et le problème vivait silencieusement pendant des mois. À combiner avec les tests fonctionnels Symfony (WebTestCase) ou Panther pour valider le rendu en mode strict avant chaque déploiement.

Migrer un projet existant : la checklist en huit étapes

  • Vérifier que twig/twig ^3.0 est tiré par Symfony 7.x dans composer.json
  • Remplacer tous les {% filter %} par {% apply %} (grep -r 'filter ' templates/ suffit)
  • Remplacer {% spaceless %} par {% apply spaceless %}
  • Corriger les alias : sameas → same as, divisibleby → divisible by
  • Activer strict_variables: true en dev et corriger toutes les RuntimeError remontées
  • Refactorer les extensions monolithiques en classes final avec first-class callables PHP 8.1+
  • Identifier les includes répétés (plus de trois occurrences identiques) comme candidats TwigComponent
  • Ajouter with ... only sur tous les includes de composants partiels existants

Sur un projet de taille moyenne — 50 à 100 templates — ces ajustements tiennent en une journée de travail. L'outillage Rector (rector/rector) ne couvre pas encore tous les patterns Twig automatiquement, mais le remplacement des balises deprecated se fait en quelques passes de recherche/remplacement ciblées dans le dossier templates/. Le vrai gain de temps vient ensuite : chaque nouveau composant TwigComponent ou bloc with ... only réduit le temps de débogage des cycles de développement suivants.

Twig 3 ne réinvente pas le templating. Il fait quelque chose de plus utile : rendre les mauvais patterns impossibles ou visibles, et donner aux projets Symfony 7.2 des outils concrets pour structurer leurs templates comme du vrai code — typé, testé, isolé. Si ton projet a accumulé une dette dans ses vues (extensions archaïques, includes copier-coller en cascade, variables silencieusement nulles en prod), un Bear Upgrade peut transformer cet inventaire en plan de migration concret, avec les refactors priorisés et un chemin clair vers les patterns modernes Symfony 7.2 + Twig 3.

Cet article vous a plu ? Partagez-le !

Besoin d'un expert Symfony ?

20 ans d'expérience sur l'écosystème PHP/Symfony.