Générer du HTML de façon élégante
Variables • Filtres • Structures • Héritage • Fonctions
Formation DWWM - 2026
Le moteur de templates de Symfony
Twig est un moteur de templates moderne, rapide et sécurisé pour PHP.
Imagine que tu veux peindre 100 fois le même logo :
Twig = le pochoir pour générer du HTML !
<!DOCTYPE html>
<html>
<head>
<title><?php echo $title; ?></title>
</head>
<body>
<h1><?php echo htmlspecialchars($title); ?></h1>
<?php if (!empty($users)): ?>
<ul>
<?php foreach ($users as $user): ?>
<li>
<?php echo htmlspecialchars($user['name']); ?>
</li>
<?php endforeach; ?>
</ul>
<?php else: ?>
<p>Aucun utilisateur</p>
<?php endif; ?>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<title>{{ title }}</title>
</head>
<body>
<h1>{{ title }}</h1>
{% if users is not empty %}
<ul>
{% for user in users %}
<li>{{ user.name }}</li>
{% endfor %}
</ul>
{% else %}
<p>Aucun utilisateur</p>
{% endif %}
</body>
</html>
✅ Avantages de Twig :
💡 Performance : La compilation se fait UNE SEULE fois. Les requêtes suivantes utilisent le cache PHP, ce qui rend Twig très rapide !
Les 3 délimiteurs Twig
Twig utilise 3 types de délimiteurs pour différentes actions :
| Délimiteur | Usage | Exemple |
|---|---|---|
{{ }} |
Afficher une variable | {{ nom }} |
{% %} |
Exécuter une instruction (if, for...) | {% if age > 18 %} |
{# #} |
Commenter (non affiché) | {# TODO: améliorer #} |
{# Ceci est un commentaire, invisible dans le HTML final #}
{% set message = "Bonjour" %} {# Instruction : définir variable #}
<h1>{{ message }}</h1> {# Affichage : "Bonjour" #}
{% if user.isAdmin %} {# Instruction : condition #}
<p>Panneau d'administration</p>
{% endif %}
{{ }} = Dialogue parlé
Ce que le public entend, ce qui s'affiche à l'écran.
<p>{{ personnage.nom }} dit : "{{ personnage.replique }}"</p>
{% %} = Didascalie (indication scénique)
Instructions pour les acteurs, non visibles du public.
{% if personnage.estEnColere %}
<span style="color: red;">{{ personnage.replique }}</span>
{% endif %}
{# #} = Note du metteur en scène
Commentaires personnels, jamais affichés.
{# TODO: ajouter effet sonore ici #}
Manipuler et afficher des données
Utilisez {{ }} pour afficher n'importe quelle variable :
{# Dans le contrôleur #}
return $this->render('page.html.twig', [
'nom' => 'Marie',
'age' => 25,
'actif' => true,
'note' => 15.5
]);
{# Dans le template #}
<p>Nom : {{ nom }}</p> {# Marie #}
<p>Âge : {{ age }}</p> {# 25 #}
<p>Actif : {{ actif }}</p> {# 1 (true) #}
<p>Note : {{ note }}</p> {# 15.5 #}
💡 Échappement automatique : Twig échappe automatiquement les caractères HTML dangereux pour éviter les failles XSS.
{{ "<script>alert('XSS')</script>" }}
{# Affiche : <script>alert('XSS')</script> #}
{# Le code JavaScript n'est PAS exécuté ! #}
{# Dans le contrôleur #}
return $this->render('page.html.twig', [
'user' => [
'nom' => 'Dupont',
'prenom' => 'Marie',
'age' => 25
]
]);
{# Dans le template - 2 syntaxes possibles #}
{{ user.nom }} {# Dupont #}
{{ user['nom'] }} {# Dupont #}
{{ user.prenom }} {# Marie #}
{{ user['prenom'] }} {# Marie #}
{{ user.nom }}
{{ article.titre }}
{{ produit.prix }}
✅ Plus lisible
{{ user['nom'] }}
{{ article['titre'] }}
{{ produit['prix'] }}
✅ Utile pour clés dynamiques
Twig peut appeler automatiquement les getters !
class User
{
private string $nom;
private string $email;
public function getNom(): string
{
return $this->nom;
}
public function getEmail(): string
{
return $this->email;
}
public function isAdmin(): bool
{
return $this->admin;
}
}
{{ user.nom }} {# Appelle automatiquement getNom() #}
{{ user.email }} {# Appelle automatiquement getEmail() #}
{{ user.admin }} {# Appelle automatiquement isAdmin() #}
✅ Magie de Twig : Quand vous faites {{ user.nom }}, Twig essaie dans cet ordre :
$user->nom$user->getNom()$user->isNom() ou $user->hasNom()Utilisez {% set %} pour créer ou modifier des variables :
{% set nom = "Marie" %}
{% set age = 25 %}
{% set actif = true %}
<p>{{ nom }} a {{ age }} ans</p>
{% set prenom = "Marie" %}
{% set nom = "Dupont" %}
{% set nomComplet = prenom ~ " " ~ nom %}
<p>{{ nomComplet }}</p> {# Marie Dupont #}
{% set prix = 100 %}
{% set tva = prix * 0.20 %}
{% set total = prix + tva %}
<p>Total TTC : {{ total }} €</p> {# 120 € #}
{% set contenu %}
<p>Ceci est un long</p>
<p>bloc de HTML</p>
{% endset %}
{{ contenu }} {# Affiche tout le bloc #}
Modifier l'affichage des données
Un filtre transforme une variable avant de l'afficher.
Quand tu prends une photo :
La photo originale ne change pas, seul l'affichage est modifié !
{{ variable|filtre }}
{{ variable|filtre1|filtre2|filtre3 }} {# Chaînage #}
{{ variable|filtre(argument1, argument2) }} {# Avec arguments #}
| Filtre | Description | Exemple | Résultat |
|---|---|---|---|
upper |
Tout en MAJUSCULES | {{ "bonjour"|upper }} |
BONJOUR |
lower |
Tout en minuscules | {{ "BONJOUR"|lower }} |
bonjour |
capitalize |
Première lettre en majuscule | {{ "bonjour"|capitalize }} |
Bonjour |
title |
Chaque mot avec majuscule | {{ "bonjour monde"|title }} |
Bonjour Monde |
trim |
Supprimer espaces début/fin | {{ " test "|trim }} |
test |
length |
Nombre de caractères | {{ "Bonjour"|length }} |
7 |
reverse |
Inverser le texte | {{ "Bonjour"|reverse }} |
ruojnoB |
{% set texte = "Bonjour le monde" %}
{{ texte|slice(0, 7) }} {# Bonjour #}
{{ texte|slice(8) }} {# le monde #}
{{ texte|slice(-5) }} {# monde (5 derniers) #}
{% set phrase = "Bonjour Marie" %}
{{ phrase|replace({'Marie': 'Paul'}) }}
{# Bonjour Paul #}
{{ phrase|replace({'Bonjour': 'Salut', 'Marie': 'tout le monde'}) }}
{# Salut tout le monde #}
{% set article = "Ceci est un très long article avec beaucoup de contenu..." %}
<p>{{ article|slice(0, 100) }}...</p>
{# Affiche les 100 premiers caractères + "..." #}
{% set maintenant = "now" %}
{{ maintenant|date("d/m/Y") }} {# 28/01/2025 #}
{{ maintenant|date("d F Y") }} {# 28 janvier 2025 #}
{{ maintenant|date("H:i:s") }} {# 14:30:45 #}
{{ maintenant|date("d/m/Y à H:i") }} {# 28/01/2025 à 14:30 #}
{{ "Bonjour %s !"|format("Marie") }}
{# Bonjour Marie ! #}
{{ "Il y a %d utilisateurs"|format(42) }}
{# Il y a 42 utilisateurs #}
{{ "%s a %d ans"|format("Marie", 25) }}
{# Marie a 25 ans #}
{{ 1234.5678|number_format }}
{# 1,235 (arrondi par défaut) #}
{{ 1234.5678|number_format(2) }}
{# 1,234.57 (2 décimales) #}
{{ 1234.5678|number_format(2, ',', ' ') }}
{# 1 234,57 (séparateurs français) #}
{% set nombres = [5, 2, 8, 1, 9] %}
{{ nombres|sort|join(', ') }}
{# 1, 2, 5, 8, 9 #}
{% set noms = ["Alice", "Bob", "Charlie"] %}
{{ noms|reverse|join(', ') }}
{# Charlie, Bob, Alice #}
{% set fruits = ["pomme", "poire", "banane"] %}
{{ fruits|join(', ') }}
{# pomme, poire, banane #}
{{ fruits|join(' - ') }}
{# pomme - poire - banane #}
{{ fruits|join(' et ') }}
{# pomme et poire et banane #}
{% set noms = ["Alice", "Bob", "Charlie"] %}
{{ noms|first }} {# Alice #}
{{ noms|last }} {# Charlie #}
{% set code = "<script>alert('XSS')</script>" %}
{{ code }} {# Auto-échappé par défaut #}
{{ code|e }} {# Échappé manuellement #}
{{ code|escape }} {# Même chose #}
{# Affiche : <script>alert('XSS')</script> #}
{% set html = "<strong>Important</strong>" %}
{{ html }} {# <strong>Important</strong> #}
{{ html|raw }} {# <strong>Important</strong> (HTML rendu) #}
⚠️ DANGER : N'utilisez |raw que sur du contenu de confiance ! Jamais sur des données utilisateur, sinon faille XSS garantie !
{# ❌ DANGEREUX - Ne JAMAIS faire #}
{{ commentaire_utilisateur|raw }}
{# ✅ SÛR - Échappement par défaut #}
{{ commentaire_utilisateur }}
{{ nom|default('Anonyme') }}
{# Si nom est vide, affiche "Anonyme" #}
{{ article.titre|default('Sans titre') }}
{{ user.email|default('email@example.com') }}
{{ 3.14159|round }} {# 3 #}
{{ 3.14159|round(2) }} {# 3.14 #}
{{ 3.14159|round(3) }} {# 3.142 #}
{# Modes d'arrondi #}
{{ 3.5|round(0, 'floor') }} {# 3 (arrondi inférieur) #}
{{ 3.5|round(0, 'ceil') }} {# 4 (arrondi supérieur) #}
{{ -42|abs }} {# 42 #}
{{ 42|abs }} {# 42 #}
On peut appliquer plusieurs filtres à la suite avec le pipe | :
{{ " bonjour le monde "|trim|upper }}
{# BONJOUR LE MONDE #}
{# Étapes :
1. " bonjour le monde " (original)
2. "bonjour le monde" (après trim)
3. "BONJOUR LE MONDE" (après upper)
#}
{% set texte = " ceci est un LONG texte avec DES majuscules " %}
{{ texte|trim|lower|capitalize|slice(0, 20) }}...
{# Ceci est un long tex... #}
{# Étapes :
1. " ceci est un LONG texte avec DES majuscules "
2. "ceci est un LONG texte avec DES majuscules" (trim)
3. "ceci est un long texte avec des majuscules" (lower)
4. "Ceci est un long texte avec des majuscules" (capitalize)
5. "Ceci est un long tex" (slice 0-20)
6. "Ceci est un long tex..." (ajout ...)
#}
💡 Ordre important : Les filtres s'appliquent de gauche à droite. L'ordre peut changer complètement le résultat !
Conditions et boucles
{% if condition %}
{# Code si vrai #}
{% endif %}
{% if user.isAdmin %}
<p>Vous êtes administrateur</p>
{% else %}
<p>Vous êtes utilisateur normal</p>
{% endif %}
{% if age < 18 %}
<p>Mineur</p>
{% elseif age < 65 %}
<p>Adulte</p>
{% else %}
<p>Senior</p>
{% endif %}
{% if user.isAdmin and user.isActive %}
<p>Admin actif</p>
{% endif %}
{% if age > 18 or hasPermission %}
<p>Accès autorisé</p>
{% endif %}
{% if not user.isBanned %}
<p>Bienvenue</p>
{% endif %}
| Opérateur | Description | Exemple |
|---|---|---|
== |
Égal à | {% if age == 18 %} |
!= |
Différent de | {% if nom != 'Admin' %} |
< |
Inférieur à | {% if age < 18 %} |
> |
Supérieur à | {% if prix > 100 %} |
<= |
Inférieur ou égal | {% if note <= 10 %} |
>= |
Supérieur ou égal | {% if stock >= 5 %} |
and |
ET logique | {% if age > 18 and hasLicense %} |
or |
OU logique | {% if admin or moderator %} |
not |
NON logique | {% if not banned %} |
Les tests permettent de vérifier le type ou l'état d'une variable :
| Test | Description | Exemple |
|---|---|---|
is defined |
Variable existe | {% if user is defined %} |
is null |
Variable est null | {% if user is null %} |
is empty |
Variable vide | {% if tableau is empty %} |
is even |
Nombre pair | {% if nombre is even %} |
is odd |
Nombre impair | {% if nombre is odd %} |
is iterable |
Peut être parcouru | {% if data is iterable %} |
{% if users is not empty %}
<ul>
{% for user in users %}
<li>{{ user.name }}</li>
{% endfor %}
</ul>
{% endif %}
{% if numero is even %}
<p>{{ numero }} est pair</p>
{% else %}
<p>{{ numero }} est impair</p>
{% endif %}
{% for item in collection %}
{# Code répété pour chaque item #}
{% endfor %}
{% set fruits = ['pomme', 'poire', 'banane'] %}
<ul>
{% for fruit in fruits %}
<li>{{ fruit }}</li>
{% endfor %}
</ul>
{# Résultat :
<li>pomme</li>
<li>poire</li>
<li>banane</li>
#}
{% set user = {
'nom': 'Dupont',
'prenom': 'Marie',
'age': 25
} %}
<dl>
{% for cle, valeur in user %}
<dt>{{ cle }}</dt>
<dd>{{ valeur }}</dd>
{% endfor %}
</dl>
Le bloc else s'exécute si le tableau est vide :
<h2>Liste des utilisateurs</h2>
<ul>
{% for user in users %}
<li>{{ user.name }}</li>
{% else %}
<li>Aucun utilisateur trouvé</li>
{% endfor %}
</ul>
<ul>
<li>Alice</li>
<li>Bob</li>
</ul>
<ul>
<li>Aucun utilisateur trouvé</li>
</ul>
Dans une boucle for, la variable loop donne des infos sur l'itération :
| Variable | Description |
|---|---|
loop.index |
Numéro actuel (commence à 1) |
loop.index0 |
Numéro actuel (commence à 0) |
loop.first |
true si premier élément |
loop.last |
true si dernier élément |
loop.length |
Nombre total d'éléments |
loop.parent |
Contexte parent (boucles imbriquées) |
<ul>
{% for user in users %}
<li class="{% if loop.first %}first{% endif %} {% if loop.last %}last{% endif %}">
{{ loop.index }}. {{ user.name }}
</li>
{% endfor %}
</ul>
{# Résultat :
<li class="first">1. Alice</li>
<li>2. Bob</li>
<li class="last">3. Charlie</li>
#}
La fonctionnalité la plus puissante de Twig
Imagine un moule à gaufres :
Le moule reste le même (header, footer...), seul le contenu des trous change !
Le template de base contient la structure commune de toutes les pages.
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>{% block title %}Mon Site{% endblock %}</title>
{% block stylesheets %}
<link rel="stylesheet" href="/css/style.css">
{% endblock %}
</head>
<body>
<header>
<nav>
<a href="/">Accueil</a>
<a href="/blog">Blog</a>
<a href="/contact">Contact</a>
</nav>
</header>
<main>
{% block body %}
{# Contenu par défaut si aucun bloc body défini #}
{% endblock %}
</main>
<footer>
<p>© {{ 'now'|date('Y') }} Mon Site</p>
</footer>
{% block javascripts %}
<script src="/js/app.js"></script>
{% endblock %}
</body>
</html>
💡 Points clés :
{% block nom %} crée un "trou" remplaçableLe template enfant étend le parent et remplit ses blocs.
{% extends 'base.html.twig' %}
{% block title %}Mon Blog - {{ parent() }}{% endblock %}
{% block body %}
<h1>Liste des articles</h1>
<div class="articles">
{% for article in articles %}
<article>
<h2>{{ article.titre }}</h2>
<p>{{ article.extrait }}</p>
<a href="{{ path('blog_show', {id: article.id}) }}">
Lire la suite
</a>
</article>
{% endfor %}
</div>
{% endblock %}
{% block javascripts %}
{{ parent() }} {# Garde le JS de base.html.twig #}
<script src="/js/blog.js"></script> {# Ajoute du JS spécifique #}
{% endblock %}
✅ Résultat : Le HTML final aura :
{% extends 'base.html.twig' %}
{# DOIT être la PREMIÈRE ligne du fichier #}
{# Dans base.html.twig #}
{% block javascripts %}
<script src="/js/app.js"></script>
{% endblock %}
{# Dans enfant.html.twig #}
{% block javascripts %}
{{ parent() }} {# ← Garde <script src="/js/app.js"> #}
<script src="/js/custom.js"></script> {# ← Ajoute ce script #}
{% endblock %}
{# Résultat final :
<script src="/js/app.js"></script>
<script src="/js/custom.js"></script>
#}
{% block javascripts %}
<script src="/js/custom.js"></script>
{% endblock %}
❌ Remplace TOUT le contenu du parent
{% block javascripts %}
{{ parent() }}
<script src="/js/custom.js"></script>
{% endblock %}
✅ Conserve + ajoute
On peut créer une hiérarchie de templates :
<html>
<body>
<header>Header commun</header>
{% block content %}{% endblock %}
<footer>Footer commun</footer>
</body>
</html>
{% extends 'base.html.twig' %}
{% block content %}
<div class="admin-layout">
<aside>Menu admin</aside>
<main>{% block admin_content %}{% endblock %}</main>
</div>
{% endblock %}
{% extends 'admin_base.html.twig' %}
{% block admin_content %}
<h1>Gestion des utilisateurs</h1>
{# ... #}
{% endblock %}
Découper pour mieux régner
include() insère le contenu d'un autre template.
{{ include('chemin/template.html.twig') }}
{# Avec variables #}
{{ include('partials/card.html.twig', {
titre: 'Mon titre',
contenu: 'Mon contenu'
}) }}
templates/partials/header.html.twig
<header>
<h1>{{ siteName }}</h1>
<nav>
<a href="/">Accueil</a>
<a href="/blog">Blog</a>
</nav>
</header>
templates/page.html.twig
{{ include('partials/header.html.twig', {
siteName: 'Mon Super Site'
}) }}
<main>
{# Contenu de la page #}
</main>
| Critère | extends | include |
|---|---|---|
| Usage | Hériter d'une structure | Insérer un fragment |
| Nombre | UN SEUL extends par fichier | Autant d'includes que voulu |
| Position | DOIT être en première ligne | N'importe où |
| Cas d'usage | Layout global (header/footer) | Composants réutilisables (card, button...) |
{% extends 'base.html.twig' %}
{% block body %}
<h1>Ma page</h1>
{% endblock %}
Pour hériter du layout complet
<div class="products">
{% for product in products %}
{{ include('partials/product_card.html.twig', {
product: product
}) }}
{% endfor %}
</div>
Pour insérer des composants
Outils intégrés pour vos templates
{{ path('home') }}
{# / #}
{{ path('blog_show', {id: 123}) }}
{# /blog/123 #}
<a href="{{ path('contact') }}">Contact</a>
{{ url('home') }}
{# http://localhost:8000/ #}
{{ url('blog_show', {id: 123}) }}
{# http://localhost:8000/blog/123 #}
{# Pour emails, flux RSS... #}
<meta property="og:url" content="{{ url('article_show', {id: article.id}) }}">
<link rel="stylesheet" href="{{ asset('css/style.css') }}">
{# /css/style.css #}
<script src="{{ asset('js/app.js') }}"></script>
{# /js/app.js #}
<img src="{{ asset('images/logo.png') }}" alt="Logo">
{# /images/logo.png #}
💡 Pourquoi utiliser asset() ?
{{ dump(user) }}
{# Affiche toute la structure de user #}
{{ dump(user, article, comments) }}
{# Affiche plusieurs variables #}
{{ random(100) }}
{# Nombre aléatoire entre 0 et 100 #}
{{ random(['rouge', 'vert', 'bleu']) }}
{# Un élément aléatoire du tableau #}
{{ random() }}
{# Nombre aléatoire entre 0 et 2^31 - 1 #}
{% for i in range(1, 10) %}
{{ i }} {# 1 2 3 4 5 6 7 8 9 10 #}
{% endfor %}
{% for i in range(0, 10, 2) %}
{{ i }} {# 0 2 4 6 8 10 #}
{% endfor %}
{{ max(1, 3, 2) }} {# 3 #}
{{ min(1, 3, 2) }} {# 1 #}
{{ max([10, 5, 20]) }} {# 20 #}
Créer ses propres fonctions
Une macro est comme une fonction réutilisable en Twig.
Imagine un tampon avec ton logo :
Tu crées le tampon une fois, puis tu l'utilises partout !
{% macro nom(parametre1, parametre2) %}
{# Code HTML à générer #}
{% endmacro %}
templates/macros/forms.html.twig
{% macro button(text, type, class) %}
<button type="{{ type|default('button') }}"
class="btn {{ class|default('btn-primary') }}">
{{ text }}
</button>
{% endmacro %}
{% macro input(name, label, type) %}
<div class="form-group">
<label for="{{ name }}">{{ label }}</label>
<input type="{{ type|default('text') }}"
id="{{ name }}"
name="{{ name }}"
class="form-control">
</div>
{% endmacro %}
{% import 'macros/forms.html.twig' as forms %}
{% import 'macros/forms.html.twig' as forms %}
<form>
{{ forms.input('email', 'Adresse email', 'email') }}
{{ forms.input('password', 'Mot de passe', 'password') }}
{{ forms.button('Se connecter', 'submit', 'btn-success') }}
{{ forms.button('Annuler', 'button', 'btn-secondary') }}
</form>
<form>
<div class="form-group">
<label for="email">Adresse email</label>
<input type="email" id="email" name="email" class="form-control">
</div>
<div class="form-group">
<label for="password">Mot de passe</label>
<input type="password" id="password" name="password" class="form-control">
</div>
<button type="submit" class="btn btn-success">Se connecter</button>
<button type="button" class="btn btn-secondary">Annuler</button>
</form>
| Élément | Syntaxe | Exemple |
|---|---|---|
| Afficher | {{ }} |
{{ user.name }} |
| Instruction | {% %} |
{% if age > 18 %} |
| Commentaire | {# #} |
{# TODO #} |
| Filtre | |filtre |
{{ nom|upper }} |
| Concaténation | ~ |
{{ prenom ~ " " ~ nom }} |
| Test | is |
{% if user is null %} |
| Extends | {% extends %} |
{% extends 'base.html.twig' %} |
| Block | {% block %} |
{% block title %}...{% endblock %} |
| Include | {{ include() }} |
{{ include('header.html.twig') }} |
| Macro | {% macro %} |
{% macro button(text) %}...{% endmacro %} |
| Filtre | Usage |
|---|---|
|upper |
MAJUSCULES |
|lower |
minuscules |
|capitalize |
Première lettre en majuscule |
|title |
Chaque Mot En Majuscule |
|trim |
Supprimer espaces |
|length |
Nombre d'éléments/caractères |
|date |
Formater une date |
|slice |
Extraire une portion |
|join |
Concaténer avec séparateur |
|default |
Valeur par défaut si vide |
|replace |
Remplacer du texte |
|round |
Arrondir un nombre |
|sort |
Trier un tableau |
|raw |
Ne pas échapper (DANGER) |
|escape (e) |
Échapper le HTML |
{% if condition %}
...
{% elseif autre_condition %}
...
{% else %}
...
{% endif %}
{% for item in collection %}
{{ item }}
{% else %}
Tableau vide
{% endfor %}
loop.index {# 1, 2, 3... #}
loop.index0 {# 0, 1, 2... #}
loop.first {# true si premier #}
loop.last {# true si dernier #}
loop.length {# nombre total #}
{% if variable is defined %}
{% if variable is null %}
{% if tableau is empty %}
{% if nombre is even %}
{% if nombre is odd %}
| Commande | Usage |
|---|---|
{% extends 'base.html.twig' %} |
Hériter d'un template (1ère ligne) |
{% block nom %}...{% endblock %} |
Définir un bloc remplaçable |
{{ parent() }} |
Conserver le contenu du parent |
{{ include('partial.html.twig') }} |
Inclure un fragment |
{% block content %} plutôt que {% block b1 %}{# Explication de la logique #}{# ❌ MAUVAIS #}
{# Commentaire #}
{% extends 'base.html.twig' %}
{# ✅ BON #}
{% extends 'base.html.twig' %}
{# Commentaire #}
{# ❌ MAUVAIS #}
{% block content %}
<h1>Titre</h1>
{# Oublié {% endblock %} #}
{# ✅ BON #}
{% block content %}
<h1>Titre</h1>
{% endblock %}
{# ❌ DANGEREUX - Faille XSS #}
{{ user_comment|raw }}
{# ✅ SÛR - Échappement automatique #}
{{ user_comment }}
{# ❌ MAUVAIS - Logique complexe dans Twig #}
{% set total = 0 %}
{% for item in cart %}
{% set total = total + (item.price * item.quantity * (1 + item.tax)) %}
{% endfor %}
{# ✅ BON - Faire le calcul dans le contrôleur #}
{{ cart.total }}
templates/
├── base.html.twig # Layout principal
├── bundles/ # Surcharges de bundles
├── macros/ # Macros réutilisables
│ ├── forms.html.twig
│ ├── ui.html.twig
│ └── alerts.html.twig
├── partials/ # Fragments réutilisables
│ ├── header.html.twig
│ ├── footer.html.twig
│ ├── sidebar.html.twig
│ └── pagination.html.twig
├── blog/ # Templates du blog
│ ├── index.html.twig
│ ├── show.html.twig
│ └── _article_card.html.twig # Partial (préfixe _)
├── user/ # Templates utilisateur
│ ├── login.html.twig
│ ├── register.html.twig
│ └── profile.html.twig
└── email/ # Templates d'emails
├── welcome.html.twig
└── reset_password.html.twig
💡 Convention : Préfixer les partials par _ pour les distinguer des pages complètes (ex: _card.html.twig)
{{ dump(user) }}
{{ dump(user, article, comments) }}
{{ dump() }} {# Toutes les variables disponibles #}
En mode dev, la toolbar Symfony affiche :
# Lister toutes les fonctions Twig
php bin/console debug:twig
# Lister tous les filtres Twig
php bin/console debug:twig --filter=
# Voir les détails d'un filtre
php bin/console debug:twig --filter=date
php bin/console cache:clear
🌐 https://twig.symfony.com
Documentation complète de Twig
🌐 https://symfony.com/doc/current/templates.html
Intégration de Twig dans Symfony
🌐 https://github.com/twigphp/Twig-extensions
Extensions supplémentaires pour Twig
🌐 https://devhints.io/twig
Aide-mémoire rapide
💡 Prochaine étape : Doctrine ORM pour gérer la base de données !
Vous savez maintenant :
✅ Créer des templates propres et maintenables
✅ Réutiliser du code avec l'héritage
✅ Transformer les données avec les filtres
✅ Sécuriser vos templates contre XSS
💡 Conseil final :
Twig est puissant mais simple. Gardez vos templates lisibles : si c'est trop complexe, c'est que ça devrait être dans le contrôleur !
Prochaine étape : Doctrine ORM ! 🗄️
Formation DWWM - 2026