Chapitre 7.1 — Twig : le moteur de templates
Partie 7 : Twig & back-office — Chapitre 1/5 Version de référence : Twig 3 / Symfony 7.4.
⏱️ TL;DR
- Twig est le moteur de templates de Symfony : des fichiers
.html.twigcompilés en PHP puis mis en cache (rapides).- Trois délimiteurs :
{{ ... }}affiche une valeur,{% ... %}exécute une instruction (if, for…),{# ... #}un commentaire.- L’échappement automatique protège du XSS : toute valeur affichée est échappée par défaut. On ne désactive (
|raw) qu’à bon escient.- Filtres (
|upper,|date,|default) transforment ; fonctions (path(),asset(),is_granted()) produisent des valeurs.- L’accès par point (
user.email) marche sur les clés de tableau, les propriétés et les getters — de façon transparente.- La variable globale
appdonne accès àapp.user,app.request,app.flashes…- Pour un dev Next.js : Twig ≈ un JSX sans logique, avec échappement par défaut (comme React) — mais rendu côté serveur, sans état client.
🎯 Objectifs
- Écrire des templates Twig : affichage, filtres, fonctions, conditions, boucles.
- Comprendre et exploiter l’échappement automatique.
- Rendre un template depuis un contrôleur en lui passant des données.
- Utiliser la variable globale
app.
1. Un template Twig, c’est quoi
Twig transforme des fichiers .html.twig (dans templates/) en HTML. Ces fichiers sont compilés en classes PHP (dans var/cache/) puis exécutés — donc rapides en prod. Un template reçoit des variables depuis le contrôleur et produit du HTML.
{# templates/booking/show.html.twig #}
<h1>Réservation n°{{ booking.id }}</h1>
<p>Client : {{ booking.customerEmail }}</p>
<p>Statut : {{ booking.status.label }}</p>Rendu depuis le contrôleur (Partie 4) :
#[Route('/admin/bookings/{id}', methods: ['GET'])]
public function show(Booking $booking): Response
{
return $this->render('booking/show.html.twig', [
'booking' => $booking, // la variable 'booking' est disponible dans le template
]);
}2. Les trois délimiteurs
| Délimiteur | Rôle | Exemple |
|---|---|---|
{{ ... }} | Afficher une expression | {{ booking.customerEmail }} |
{% ... %} | Instruction (logique) | {% if booking.isActive %} |
{# ... #} | Commentaire (non rendu) | {# note interne #} |
3. L’accès aux données : le point magique
En Twig, foo.bar essaie plusieurs choses, dans l’ordre, de façon transparente :
- la clé de tableau
foo['bar']; - la propriété publique
foo->bar; - le getter
foo->getBar()oufoo->isBar()/foo->hasBar().
{{ booking.customerEmail }} {# appelle getCustomerEmail() #}
{{ booking.status.label }} {# getStatus() puis, sur l'enum, label() #}
{{ params.page }} {# params['page'] si params est un tableau #}Vous n’avez donc pas à savoir si c’est une propriété, un getter ou une clé : Twig résout. Pour une méthode avec argument, on l’appelle explicitement : foo.bar(arg).
💡 Pour un dev Next.js : oubliez la distinction propriété/méthode que vous auriez en JS.
booking.status.labelen Twig appelle la méthodelabel()de l’enum sans les parenthèses. C’est plus « déclaratif » : le template décrit quoi afficher, pas comment l’obtenir.
4. L’échappement automatique (sécurité)
Par défaut, Twig échappe toute valeur affichée ({{ ... }}) pour le contexte HTML. Si booking.customerEmail contient <script>, il sera rendu comme du texte inoffensif, pas exécuté. C’est une protection anti-XSS intégrée — l’une des grandes forces de Twig.
{{ userInput }} {# échappé : <b> devient <b> (sûr) #}
{{ trustedHtml|raw }} {# NON échappé : le HTML est rendu tel quel (danger si non maîtrisé) #}⚠️ Piège
|raw: le filtre|rawdésactive l’échappement — à n’utiliser que sur du HTML dont vous maîtrisez totalement la source (jamais sur une saisie utilisateur). Un|rawsur des données utilisateur = faille XSS. Par défaut, ne mettez pas de|raw; laissez Twig échapper.
5. Filtres et fonctions
Un filtre (|) transforme une valeur ; une fonction en produit une.
{{ name|upper }} {# MAJUSCULES #}
{{ booking.start|date('d/m/Y H:i') }} {# formate une date #}
{{ price|number_format(2, ',', ' ') }} {# 1 234,56 #}
{{ description|default('—') }} {# valeur de repli si vide #}
{{ items|length }} {# nombre d'éléments #}
{{ text|slice(0, 100) }} {# tronquer #}
{# fonctions #}
<a href="{{ path('booking_show', { id: booking.id }) }}">Voir</a> {# génère l'URL #}
<img src="{{ asset('images/logo.png') }}" alt=""> {# chemin d'asset #}
{% if is_granted('ROLE_ADMIN') %}...{% endif %} {# sécurité #}Filtres à connaître : upper/lower, date, number_format, default, length, join, trim, nl2br, escape/e, format, map/filter (sur des collections). On peut chaîner : {{ name|trim|lower }}.
6. Conditions et boucles
{% if booking.status.value == 'confirmed' %}
<span class="badge badge-success">Confirmée</span>
{% elseif booking.status.value == 'pending' %}
<span class="badge badge-warning">En attente</span>
{% else %}
<span class="badge badge-muted">{{ booking.status.label }}</span>
{% endif %}
<ul>
{% for slot in service.slots %}
<li>{{ slot.start|date('d/m H:i') }} — {{ slot.seats }} places</li>
{% else %}
<li>Aucun créneau.</li> {# le {% else %} d'un for s'affiche si la collection est vide #}
{% endfor %}Le bloc {% else %} d’une boucle for (affiché quand la collection est vide) est une commodité très pratique. Dans une boucle, la variable loop expose loop.index (1-based), loop.index0, loop.first, loop.last, loop.length.
7. La variable globale app
Symfony injecte une variable app dans tous les templates, pont vers le contexte de la requête :
{% if app.user %}
Connecté en tant que {{ app.user.email }}
{% endif %}
{{ app.request.locale }} {# locale courante #}
{{ app.environment }} {# dev / prod #}
{# messages flash (chapitre 4.5) #}
{% for label, messages in app.flashes %}
{% for message in messages %}
<div class="flash flash-{{ label }}">{{ message }}</div>
{% endfor %}
{% endfor %}app.user (l’utilisateur connecté, Partie 10), app.request, app.session, app.flashes, app.environment couvrent l’essentiel des besoins d’un template.
8. Diagnostiquer Twig
php bin/console debug:twig # liste filtres, fonctions, tests, globals disponibles
php bin/console lint:twig templates/ # vérifie la syntaxe de tous les templateslint:twig (en CI, Partie 14) attrape les erreurs de syntaxe avant le déploiement.
⚡ Depuis Symfony 5 : Twig lui-même a peu changé (c’est stable et mûr). La nouveauté marquante côté rendu, c’est l’arrivée des composants Twig (chapitre 7.2) et d’AssetMapper (chapitre 7.3), qui modernisent l’expérience full-stack sans bundler. Vos réflexes Twig de Symfony 5 restent valides.
✏️ Exercices
Exercice 1. Affichez la date de début d’une réservation au format « 09/07/2026 14:30 », avec un repli « Date inconnue » si elle est absente.
✅ Solution
{{ booking.start ? booking.start|date('d/m/Y H:i') : 'Date inconnue' }}
{# ou, si start peut être null et qu'on veut un repli sur le format : #}
{{ booking.start|date('d/m/Y H:i')|default('Date inconnue') }}Le filtre date formate ; l’opérateur ternaire (ou |default) gère l’absence. (Ici start est non-null dans notre modèle, mais l’exercice illustre le repli.)
Exercice 2. Pourquoi {{ comment.body|raw }} sur un commentaire saisi par un utilisateur est-il dangereux, et que faire à la place ?
✅ Solution
|raw désactive l’échappement : si l’utilisateur a saisi <script>...</script>, il sera exécuté dans le navigateur des visiteurs → faille XSS. Il faut laisser Twig échapper (juste {{ comment.body }}). Si on veut autoriser un sous-ensemble de HTML (gras, liens), on nettoie d’abord côté serveur avec un sanitizer (ex. le composant HtmlSanitizer de Symfony) et on n’affiche en |raw que le résultat assaini.
Exercice 3. Écrivez une boucle qui liste les services d’un prestataire, avec un message « Aucun service » si la liste est vide, et une classe first sur le premier.
✅ Solution
<ul>
{% for service in provider.services %}
<li class="{{ loop.first ? 'first' : '' }}">{{ service.name }}</li>
{% else %}
<li>Aucun service.</li>
{% endfor %}
</ul>Le {% else %} du for gère la collection vide ; loop.first détecte le premier élément.
🧠 Quiz de révision
1. À quoi servent {{ }}, {% %} et {# #} ?
{{ }}, {% %} et {# #} ?{{ }} affiche une valeur, {% %} exécute une instruction (if/for…), {# #} est un commentaire non rendu.
2. Que fait booking.customerEmail en Twig ?
booking.customerEmail en Twig ?Twig tente, de façon transparente : la clé ['customerEmail'], la propriété, puis le getter getCustomerEmail() (ou isCustomerEmail()/hasCustomerEmail()).
3. Twig échappe-t-il les valeurs par défaut ?
Oui : l’échappement automatique protège du XSS. |raw le désactive (à réserver au HTML maîtrisé).
4. Comment génère-t-on une URL et accède-t-on à l’utilisateur connecté dans un template ?
URL : path('nom_route', { params }). Utilisateur : app.user (via la variable globale app).
5. Quelle commande vérifie la syntaxe des templates ?
php bin/console lint:twig templates/.
Prochain chapitre : 7.2 — Héritage, includes & composants Twig — factoriser l’UI et créer des composants réutilisables.