Aller au contenu principal

Twig Components : la puissance du Backend dans le Frontend

Arrêtez les macros Twig et les includes. Passez aux Composants Twig : Classes PHP typées, logique encapsulée et réutilisabilité maximale.
Catégorie

Symfony

Architecture, composants et patterns avancés du framework Symfony.

Lecture
4 min
Niveau
Intermédiaire
oct 14 2025
Partager

C'est l'histoire d'une frustration. D'un côté, nous avons Symfony et PHP : robustes, typés, orientés objet. De l'autre, nos templates Twig : des fichiers .html.twig qui finissent souvent en plat de spaghettis, avec des include imbriqués et des variables passées de manière implicite (le fameux "Ah zut, j'ai oublié de passer currentUser au header").

Pendant ce temps, nos amis du Frontend utilisent React/Vue. Ils ont des Composants. Des briques autonomes, isolées, qui portent leur propre logique.

Symfony a riposté avec Twig Components (symfony/ux-twig-component). Et spoiler : ça change tout.

Le problème des include et des macro

Avant, pour faire une "Card Produit" réutilisable, on avait deux choix médiocres :

  1. Le include : On dépend du contexte global. Si le template parent n'a pas la variable product, ça plante ou c'est vide. C'est fragile.
  2. La macro : C'est une fonction d'affichage pure. Mais si vous avez besoin de calculer un prix TTC ou de formater une date complexe, vous devez le faire avant d'appeler la macro. La logique fuit partout.

Le Composant = une classe au service du template

Un Twig Component, c'est l'union sacrée d'une Classe PHP et d'un Template.

La classe PHP

C'est ici que ça devient génial. C'est une classe PHP standard. Vous avez accès au Service Container ! Regardez mon composant Seo (src/Twig/Component/Utils/Seo.php). Il ne se contente pas d'afficher des balises, il construit la stratégie SEO.

PHP
#[AsTwigComponent(
    name: 'Utils:Seo',
    template: 'components/Utils/Seo.html.twig',
)]
final class Seo
{
    // On peut passer un Post ou une Page entière au composant
    public ?AbstractContent $content = null;

    private ?array $jsonLdSchema = null;
    private ?SeoDto $seoDto = null;

    public function __construct(
        private readonly StringHelper $stringHelper,
        private readonly JsonLdBuilder $jsonLdBuilder,
    ) {}

    #[ExposeInTemplate('seo')]
    public function getSeo(): SeoDto
    {
        if (!$this->seoDto instanceof SeoDto) {
            if ($this->content instanceof AbstractContent) {
                $baseSeo = $this->content->seo;
                // ... (Hydratation du DTO depuis le contenu)
                $this->seoDto = new SeoDto(/* ... */);
            } else {
                $this->seoDto = new SeoDtoFactory()->createEmpty();
            }
        }
        return $this->seoDto;
    }

    // Méthode calculée exposée au template
    #[ExposeInTemplate('jsonLdSchema')]
    public function getJsonLdSchema(): array
    {
        // On cache le résultat pour éviter de le recalculer
        if ($this->jsonLdSchema === null) {
            $this->jsonLdSchema = $this->jsonLdBuilder->build($this->content, $this->getDescription());
        }

        return $this->jsonLdSchema;
    }

    #[ExposeInTemplate('title')]
    public function getTitle(): string
    {
        // Logique de fallback intelligente pour le titre
        return $this->stringHelper->coalesce(
            $this->getSeo()->title,       // Titre SEO spécifique
            $this->content?->title,       // Titre du contenu
            'Le code est dans le <pre>' // Défaut
        );
    }
    
    // ... autres méthodes (getDescription, getOgTitle, etc.)
}

Le template Twig

Le template est d'une propreté absolue. Il ne fait pas de calculs. Il se contente d'afficher ce que l'Ingénieur lui a préparé.

Twig
{# components/Utils/Seo.html.twig #}
<title>{{ title }}</title>
<meta name="description" content="{{ description|striptags|trim }}">
<meta name="robots" content="{{ robots }}">

{# Open Graph #}
<meta property="og:title" content="{{ ogTitle }}">
<meta property="og:description" content="{{ ogDescription }}">
<meta property="og:type" content="{{ ogType }}">

{# Injection propre du JSON-LD calculé par le PHP #}
<script type="application/ld+json">
    {{ jsonLdSchema|json_encode(constant('JSON_PRETTY_PRINT') b-or constant('JSON_UNESCAPED_SLASHES'))|raw }}
</script>

L'appel via la syntaxe HTML-like

Pour utiliser ce super-composant SEO sur n'importe quelle page, c'est une ligne :

Twig
<twig:Utils:Seo :content="post" />

Pourquoi c'est une révolution architecturale

Ce n'est pas juste de la poudre aux yeux, c'est un changement de paradigme.

  1. Encapsulation : Le composant est responsable de ses données. Si j'ai besoin de changer la façon dont le JSON-LD est généré, je touche la classe PHP, pas les 50 templates qui l'utilisent.
  2. Typage Fort : public ?AbstractContent $content. Si j'essaie de passer une chaîne de caractères à la place d'un objet Content, PHP explose immédiatement. Fini les bugs silencieux de types en Twig.
  3. Testabilité : Je peux tester unitaire ma classe Seo sans jamais rendre de HTML. Je vérifie que getTitle() renvoie bien le bon fallback. C'est impossible avec un include ou une macro.

Le mot de la fin

Twig Components n'essaie pas de transformer Symfony en React. Il essaie de ramener les bonnes pratiques du Backend (encapsulation, injection de dépendance, typage) dans la couche de vue. C'est la fin du code spaghetti dans les templates. Et il était temps.

Poursuivre la lecture

Sélectionné avec soin pour vous.

DX

Les délimiteurs Twig : ce problème d'espace blanc que vous ignorez

Gaps inline-block, diffs bruyants, layouts instables : comprenez l'impact des délimiteurs Twig sur l'espace blanc et adoptez les bonnes pratiques avec {%- et {{-.

8 min de lecture
DX

Symfony UX Toolkit : quand le frontend devient (enfin) un plaisir

Découvrez comment Symfony UX Toolkit et le kit Shadcn révolutionnent le frontend.

4 min de lecture
Symfony

Anatomie d’un projet Symfony

Explorez les mystères de Symfony avec humour acide : dossiers, outils et secrets révélés pour les développeurs curieux.

5 min de lecture