Aller au contenu principal

Les macros Twig : des fonctions qui ne voient rien.

Maîtrisez les macros Twig : syntaxe, importations multiples, portée et quand les remplacer par des Twig Components pour optimiser vos templates Symfony.

5 min de lecture
Sommaire · 5

On croise des macros Twig dans tous les projets Symfony d'un certain âge, et on les écrit de mémoire, sans avoir relu la page de doc depuis des années. Ce billet fait le tour complet : la syntaxe, les valeurs par défaut, les trois façons d'importer. Il couvre aussi la règle de portée, celle qui coûte une heure de debug la première fois qu'on la rencontre, et le moment précis où une macro doit céder sa place à un Twig Component.

Une fonction déguisée en template

{% macro %} définit un fragment réutilisable qui se comporte comme une fonction : des arguments, des valeurs par défaut, du HTML en sortie.

Twig
{# templates/_ui.html.twig #}
{% macro badge(label, color = 'zinc', size = 'sm') %}
    <span class="badge badge-{{ color }} badge-{{ size }}">{{ label }}</span>
{% endmacro %}

Deux différences avec une fonction PHP, et elles sont structurantes. D'abord, les arguments sont toujours optionnels : appeler badge() sans rien ne lève aucune erreur, label vaut null et le span sort vide. Ensuite, les arguments surnuméraires ne cassent rien non plus : ils atterrissent dans une variable spéciale varargs, une liste que la macro peut parcourir. Twig encaisse tout sans broncher. La validation, c'est notre travail, pas le sien.

Trois façons d'importer

Une macro définie dans un autre fichier doit être importée avant usage. Deux tags s'en chargent :

Twig
{# tout le fichier sous un alias #}
{% import '_ui.html.twig' as ui %}
{{ ui.badge('PHP 8.5', 'green') }}

{# des macros précises, renommables au passage #}
{% from '_ui.html.twig' import badge, badge as pill %}
{{ pill('brouillon', 'amber') }}

import ramène toutes les macros du fichier derrière un préfixe, from va chercher à l'unité. Le préfixe a un mérite discret : dans un template de 200 lignes, ui.badge() dit d'où vient la macro, badge() ne dit rien.

Troisième cas : la macro définie dans le template courant. Pas besoin d'import, la variable spéciale _self la sert directement :

Twig
{{ _self.badge('brouillon', 'amber') }}

Et le cas que peu de gens connaissent : une macro qui appelle une macro du même fichier. Chaque macro vit dans son propre espace, le wrapper doit donc importer _self chez lui :

Twig
{% macro field(label, name, value = '') %}
    {% import _self as ui %}
    <div class="field">
        {{ ui.badge(label) }}
        <input name="{{ name }}" value="{{ value|e }}"/>
    </div>
{% endmacro %}

Le piège : une macro porte des œillères

La doc l'énonce en une phrase, et on la découvre en général devant un template qui refuse de faire ce qu'on lui demande : une macro n'a pas accès aux variables du template qui l'appelle.

Sur le même sujet

Twig
{% set current_route = 'billets' %}

{% macro nav_item(label, route) %}
    {# current_route n'existe pas ici : la condition est toujours fausse #}
    <a class="{{ current_route == route ? 'active' }}" href="{{ path(route) }}">
        {{ label }}
    </a>
{% endmacro %}

Rien ne crashe. Le template compile, la page s'affiche, et la classe active n'apparaît juste jamais. C'est le pire genre de bug : silencieux, légal, invisible au premier survol.

Nuance qui sauve du temps : les fonctions Twig (path(), dump(), les filtres…) restent disponibles dans la macro. Ce sont les variables du contexte qui sont invisibles, pas le moteur. La macro ne voit que ce qu'on lui tend : ici, current_route doit devenir un troisième argument.

La doc offre une échappatoire : passer le contexte entier via la variable spéciale _context.

Twig
{{ ui.nav_item('Billets', 'billets', _context) }}

Ça marche, et c'est presque toujours une mauvaise idée. Une macro qui reçoit tout le contexte redépend de tout : on perd la seule garantie qu'elle offrait, celle de ne dépendre que de ses arguments. Si une macro a besoin du contexte complet pour fonctionner, elle demande en réalité à être autre chose.

Macro, include ou Component ?

Autre chose, mais quoi ? Twig et Symfony offrent trois outils pour réutiliser un fragment, et le bon choix tient en trois questions.

Pour aller plus loin

  • Le fragment a besoin du contexte du template ? include : il reçoit les variables du parent par défaut, c'est sa raison d'être.
  • Le fragment est du HTML pur, paramétré par des scalaires, répété au sein d'un même domaine de templates ? Macro : le badge, la ligne de tableau, le champ de formulaire maison. Local, bête, parfait.
  • Il y a de la logique, un typage à poser, un test à écrire, ou une réutilisation qui traverse les domaines ? Twig Component : une classe PHP porte les props et la logique, le template ne fait que rendre.

La méta-description du billet Twig Components de ce blog promet d'« arrêter les macros Twig », et il faut la lire comme une accroche plus que comme une doctrine. Les macros gardent un territoire légitime. Il est juste plus petit qu'on ne le croit, et il rétrécit à chaque fois qu'une macro gagne un if de plus.

Pour la bascule

Le mot de la fin

La macro est l'outil le plus honnête de Twig : une fonction avec des œillères, qui ne connaît que ses arguments et ne promet rien d'autre. C'est une limite, et c'est exactement ce qui la rend fiable : tout ce dont elle dépend tient dans sa signature. Savoir écrire une macro, c'est surtout savoir quand ne pas en écrire une.

Ce billet s'arrête volontairement à Twig 3 : Twig 3.28 vient d'ajouter l'appel de macro par nom dynamique via l'opérateur point (macros.(name)(args)), et Twig 4 arrive derrière. Les macros y survivront très bien. La vraie question, c'est combien des nôtres méritent le voyage, et combien attendaient juste qu'on les promeuve en Component.

Une coquille, une erreur dans ce billet ? Signale-la-moi.

Vous aimerez aussi

Activez uniquement ce que vous souhaitez. Vos choix sont conservés 6 mois.

Strictement nécessaires

Indispensables au fonctionnement du site (session, sécurité, préférence d'affichage). Aucune donnée n'est partagée à des tiers et aucun consentement n'est requis.

Toujours actif

Mesure d'audience

Statistiques via Google Analytics (GA4) : pages vues, source du trafic, navigateur et interactions clés. Dépose des cookies de mesure, activés seulement avec votre accord (Consent Mode). Sans publicité ciblée, sans Google Signals, sans partage commercial.

Contenus externes

Affiche les GIF animés hébergés par Giphy (CDN aux États-Unis). À l'affichage d'un GIF, votre adresse IP et votre navigateur sont transmis à Giphy. Sans votre accord, les GIF ne s'affichent pas.