Table des matières :
- Twig PHP dans un projet Symfony (PrestaShop 9 inclus) : pipeline de rendu
- Syntaxe essentielle Twig : affichage, filtres, tests, conditions
- Héritage de templates Twig : extends, blocks, parent() et embed
- Includes et réutilisabilité : include, macro, import, use
- Sécurité des templates Twig : auto-escaping, contextes, sandbox
- Debug et perf : cache Twig, lint, profiling et garde-fous CI
Twig PHP dans un projet Symfony (PrestaShop 9 inclus) : pipeline de rendu
Twig PHP n’est pas un « framework front » : c’est un moteur de templates côté serveur. Dans Symfony (et donc dans la partie Symfony de PrestaShop 8/9), la vue est rendue par Twig à partir d’un contexte (tableau de variables) fourni par le contrôleur. Le point important pour un dev back : Twig n’exécute pas de PHP arbitraire dans le template, et c’est volontaire. Vous encodez la logique métier en PHP (contrôleurs/services), Twig ne fait que composer le HTML (ou du texte) et appliquer des transformations.
Concrètement, le pipeline ressemble à ça :
- Requête HTTP vers une route Symfony (ex. une page admin migrée).
- Contrôleur : récupère les données (DB, services, API), construit un contexte (souvent via un Presenter / ViewModel).
- Rendu Twig : Symfony appelle le moteur Twig (
Environment) avec un nom de template et un tableau de variables. - Compilation + cache : si nécessaire, Twig compile le template (et ses parents/includes) en PHP puis met en cache.
- Response : Symfony renvoie un
Response(HTML) au navigateur.
Techniquement, Twig compile les templates en classes PHP et les met en cache. La doc officielle le dit explicitement : « Twig compiles templates down to plain optimized PHP code » (documentation Twig 3.x, https://twig.symfony.com/doc/3.x/). Résultat : une fois le cache chaud, l’impact runtime est faible (le coût principal restant la génération de la page elle‑même : DB, services, assemblage de contexte, etc.). En environnement dev, le recompilage fréquent (et les outils de debug) faussent les mesures.
Deux détails pratiques qui font souvent la différence sur un projet Symfony/PrestaShop :
- Chargement des templates (loaders + namespaces) : dans PrestaShop, vous croisez des chemins “namespacés” (
@PrestaShop,@Modules/...). C’est clé pour éviter des chemins relatifs fragiles et pour rester compatible avec les conventions du core. - Variables globales vs contexte explicite : Symfony permet de déclarer des
globalsTwig, mais sur un back-office industrialisé, mieux vaut garder un contexte explicite par page (et des composants réutilisables) : c’est plus testable, moins “magique”, et ça limite les fuites de données.
Côté PrestaShop, il faut être clair : le front-office “classique” reste majoritairement en Smarty. Twig intervient surtout dans le back-office Symfony, les pages migrées du core, et vos modules qui exposent des contrôleurs Symfony. Pour le contexte versions : en 2026, PrestaShop 9 s’appuie sur Symfony 6.4 (cf. l’article interne : PrestaShop 9 : nouveautés techniques Symfony 6.4, API et performances) et sur PHP 8.1–8.5 selon release (cf. PrestaShop 9.1 : compatibilité PHP 8.1–8.5, CLI et nouveautés développeurs). Concrètement, vous êtes sur Twig 3.x via Symfony/TwigBundle.
Mini-scénario (typique back-office) : vous créez une page de configuration de module qui affiche une table de règles de prix.
- Le contrôleur charge 200 règles, calcule un statut (“actif / expiré”), et prépare une liste de lignes déjà prêtes à rendre (dates formatables, libellés traduisibles, URLs d’actions).
- Twig se contente de boucler sur
rowset d’afficher les colonnes avec des filtres. - Si vous poussez le calcul “statut” dans Twig (conditions imbriquées + accès à la date courante + logique métier), vous rendez la page difficile à maintenir et à tester.
Syntaxe essentielle Twig : affichage, filtres, tests, conditions
Le cœur de la syntaxe Twig tient en deux délimiteurs : {{ ... }} pour afficher une expression, {% ... %} pour exécuter une instruction (if, for, set, block…). Exemple minimal (fichier .html.twig) :
<h1>{{ product.name }}</h1>
{% if product.price is defined %}
<p>Prix : {{ product.price|number_format(2, ',', ' ') }} €</p>
{% endif %}
Pour mémoriser vite, ce tableau couvre 80% des cas en admin :
| Besoin | Twig |
|---|---|
| Afficher une variable | {{ variable }} |
| Condition | {% if ... %} ... {% endif %} |
| Boucle | {% for item in items %} ... {% endfor %} |
| Définir une variable | {% set x = ... %} |
| Commentaire | {# ... #} |
Le point critique côté robustesse : les variables absentes. En Twig, un accès du type product.price peut produire un rendu vide ou lever une exception selon la configuration (strict_variables). En projet industrialisé, activer strict_variables en dev vous évite de livrer des templates “silencieux” qui masquent des regressions (voir section debug/CI). Les tests is defined, is empty, is same as(...) sont votre première ligne de défense.
Deux patterns concrets utiles en back-office :
- Fallback lisible (quand une donnée est optionnelle) :
<p>Référence : {{ product.reference|default('—') }}</p>
- Boucle avec cas “vide” (évite un
ifséparé) :
{% for row in rows %}
<tr>...</tr>
{% else %}
<tr><td colspan="5">Aucune donnée.</td></tr>
{% endfor %}
Les filtres sont l’outil de base pour transformer une valeur sans polluer le contrôleur. Quelques filtres “à connaître par cœur” :
|default('...')pour une valeur fallback,|date('Y-m-d')pour formater une date,|json_encodepour sérialiser une structure,|e/|escapepour l’échappement (voir sécurité),|raw(à éviter sauf cas contrôlé).
Dans un contexte francophone / zone euro, attention à la présentation des nombres : number_format(2, ',', ' ') colle souvent aux usages FR (virgule décimale + espace milliers), mais ce n’est pas une règle universelle (Belgique, Suisse, Canada…). Si votre back-office sert plusieurs locales, envisagez une approche “locale-aware” côté Symfony (Intl) plutôt que de figer le format au niveau du template.
Sur Symfony/PrestaShop, vous croisez aussi des filtres/fonctions fournis par les bundles : path() / url() (routing), asset() (assets), trans() (i18n). La doc Symfony rappelle la position de Twig dans la stack : « Twig is the default template engine for Symfony » (Symfony Docs). Ça se traduit en pratique par une intégration étroite avec le conteneur et la traduction, mais ça veut aussi dire que si votre contexte est mal construit, Twig ne “rattrapera” pas le design : il affichera fidèlement les données qu’on lui donne.
Enfin, n’oubliez pas les “petits outils” Twig qui évitent des bricolages :
loop.index,loop.first,loop.lastdans les boucles (pratique pour une classe CSS ou un séparateur).attribute(obj, key)si vous devez accéder dynamiquement à une propriété/clé (cas rare, mais utile pour des tableaux de colonnes).- L’opérateur de concaténation
~:
<span>{{ 'Règle #' ~ row.id }}</span>
Héritage de templates Twig : extends, blocks, parent() et embed
L’héritage (extends) est le mécanisme qui rend Twig réellement maintenable sur un back-office : vous factorisez layout, head, navigation, etc., et vous ne redéfinissez que des blocs. Typiquement, dans PrestaShop admin, vos templates de module étendent un layout fourni par le core. Exemple :
{# modules/mymodule/views/templates/admin/configure.html.twig #}
{% extends '@PrestaShop/Admin/layout.html.twig' %}
{% block content %}
<div class="card">
<h3 class="card-header">{{ 'Configuration'|trans({}, 'Modules.MyModule.Admin') }}</h3>
<div class="card-body">
...
</div>
</div>
{% endblock %}
Ce que vous gagnez immédiatement :
- Une cohérence UI avec le back-office (styles, header, breadcrumbs si gérés par le layout).
- Une meilleure compatibilité upgrade : si le core modifie le layout admin, vos pages héritées suivent plus facilement.
- Des templates plus lisibles : une page = des blocs significatifs.
Là où ça dérape souvent : vous copiez/collez un layout entier “pour aller vite”. Mauvaise idée : vous perdez la compatibilité future (mise à jour du core) et vous dupliquez la dette. Préférez {{ parent() }} quand vous devez compléter un bloc au lieu de le remplacer. Exemple :
{% block javascripts %}
{{ parent() }}
<script src="{{ asset('modules/mymodule/admin.js') }}"></script>
{% endblock %}
Autre piège fréquent : “écraser” un bloc alors que vous vouliez juste injecter un élément. Dans ce cas, parent() limite les effets de bord. Sur un back-office PrestaShop, c’est particulièrement vrai pour les blocs d’assets (CSS/JS) et certains blocs de layout liés aux notifications.
embed est utile quand vous voulez inclure un template tout en redéfinissant certains de ses blocs. C’est un pattern propre pour des composants “semi-génériques” (card, modal…) quand un simple include n’est pas suffisant. Exemple de logique (sans multiplier les fichiers) :
- Un template de composant définit des blocs
header,body,footer. - La page “embed” le composant et override uniquement
bodyetfooter.
Mais n’en abusez pas : trop d’héritage/embeds imbriqués rend les traces difficiles à lire. Si vous avez déjà des problèmes de temps de rendu/TTFB, commencez par mesurer proprement (voir TTFB PrestaShop : réduire le Time To First Byte sous 200 ms et PrestaShop performance : monitoring, tests de charge et runbooks soldes) avant d’accuser Twig : la cause est souvent ailleurs (N+1, I/O, hooks trop lourds).
Astuce “maintenance” : quand vous reprenez un template, commencez par identifier :
- le template parent (layout),
- la liste des blocs redéfinis,
- les includes principaux. Ça vous donne la carte de lecture avant de modifier quoi que ce soit.
Includes et réutilisabilité : include, macro, import, use
include sert à injecter un partial. Sa force : vous pouvez contrôler le contexte explicitement. Si vous ne faites rien, l’include hérite de tout le scope (pratique, mais sale). En code sérieux, utilisez with + only pour limiter les variables visibles :
{% include '@Modules/mymodule/views/templates/admin/_table.html.twig'
with { rows: rows, columns: columns }
only %}
Pourquoi c’est important ? Parce que la dette classique des templates Twig vient des variables “fantômes” : un partial marche “par hasard” car il voit une variable globale, puis casse silencieusement lors d’un refactor (ou dans un autre écran qui réutilise le partial).
ignore missing est très utile quand vous authorisez des overrides optionnels (ex: un module qui propose un template surchargable par un autre module ou par un thème admin). Exemple :
{% include '@Modules/mymodule/views/templates/admin/overrides/_extra.html.twig'
ignore missing %}
Les macros sont l’équivalent Twig de fonctions de rendu. C’est le bon outil pour factoriser un composant purement “présentation” (badge, bouton, rendu d’erreur), sans tomber dans un include qui traîne du state implicite. Exemple :
{# _ui.html.twig #}
{% macro badge(type, label) %}
<span class="badge badge-{{ type|e('html_attr') }}">{{ label }}</span>
{% endmacro %}
{% import '@Modules/mymodule/views/templates/admin/_ui.html.twig' as ui %}
{{ ui.badge('success', 'OK') }}
Conseil : gardez vos macros pures (entrée → sortie). Si une macro dépend de 6 variables du scope, vous recréez les mêmes problèmes qu’un include implicite.
Enfin, use (traits Twig) permet de réutiliser des blocs d’un autre template. C’est une arme puissante mais plus “méta” : bonne pour mutualiser une structure de blocs entre plusieurs pages, moins bonne pour un simple composant. Si vous vous retrouvez à faire des use + overrides de blocs en cascade, c’est souvent le signal qu’il faut remonter une partie de la logique côté PHP (ViewModel/Presenter) plutôt que de jouer au Lego dans Twig.
Checklist rapide (choisir le bon outil) :
include: partial simple, HTML réutilisable, contexte passé explicitement (only).- macro/import : petit composant répétitif (badge, bouton, label), “fonction de rendu”.
extends/blocks : structure de page, layout.embed: composant avec structure + quelques blocs customisables.use: partage de blocs entre pages (cas avancé).
Sécurité des templates Twig : auto-escaping, contextes, sandbox
Le premier malentendu avec Twig PHP : croire que {{ variable }} est toujours safe “par magie”. Oui, Twig échappe par défaut en HTML dans la majorité des configurations Symfony, mais la sécurité est contextuelle. Vous devez choisir le bon contexte d’échappement : HTML, attribut HTML, JavaScript, URL… Exemple :
<a title="{{ title|e('html_attr') }}" href="/search?q={{ q|url_encode }}">
{{ label }}
</a>
<script>
const payload = {{ data|json_encode|raw }};
</script>
Notez le pattern : json_encode produit une chaîne JSON valide, mais dans un <script> on finit souvent par mettre |raw pour éviter un double échappement. C’est acceptable uniquement si data est contrôlé et que vous savez ce que vous faites. Sinon, vous fabriquez des XSS “stockées” ou “reflected” à grande vitesse. La référence opérationnelle reste l’OWASP XSS Prevention Cheat Sheet (OWASP XSS Prevention Cheat Sheet) : elle martèle l’encodage selon le contexte (HTML/JS/URL/CSS) plutôt qu’un “escape global” naïf.
Deux pratiques concrètes qui évitent des erreurs coûteuses :
- Échapper selon le contexte d’injection :
- contenu HTML : défaut (auto-escape) ou
|e - attribut HTML :
|e('html_attr') - paramètres d’URL :
|url_encode - JavaScript inline : évitez si possible ; sinon, préférez sérialiser en JSON contrôlé plutôt que concaténer des chaînes
- Éviter
rawcomme “rustine” : si vous l’ajoutez pour “corriger un affichage”, vous créez souvent une régression sécurité. Dans le doute, remontez au contrôleur et nettoyez/validez la donnée.
Dans PrestaShop admin, l’attaque la plus réaliste n’est pas “un user public” mais un acteur avec accès back-office (employé, compte compromis, module tiers) qui injecte du contenu (nom produit, champ de configuration, traduction). Le fait que Twig échappe par défaut limite les dégâts, mais pas si vous utilisez |raw pour afficher une description riche ou un champ WYSIWYG sans sanitizer. Si vous devez afficher du HTML fourni par un utilisateur, la solution propre est un sanitizer HTML (côté PHP) avec une whitelist (balises/attributs) plutôt qu’un raw permanent.
Autre outil utile : les blocs autoescape pour forcer un contexte (ou l’interdire localement). Exemple (à utiliser avec parcimonie) :
{% autoescape 'html_attr' %}
<input value="{{ value }}">
{% endautoescape %}
Dernier point rarement utilisé mais pertinent : le Sandbox Twig (Twig\Sandbox\SecurityPolicy) pour exécuter des templates non fiables (ex: système de templates éditables). Si vous envisagez de laisser des non-devs écrire du Twig (même “un peu”), sandboxez. Sinon, vous exposez des fuites de données (accès à des variables inattendues), des appels de fonctions non prévues, et des templates impossibles à auditer. Dans un module PrestaShop, c’est le genre de feature qui finit en incident si vous ne la bornez pas.
Debug et perf : cache Twig, lint, profiling et garde-fous CI
Le debug Twig “propre” ne consiste pas à mettre des var_dump() en PHP : utilisez les outils Twig/Symfony. Avec APP_ENV=dev et le debug activé, vous pouvez faire {{ dump(variable) }} dans un template (VarDumper). C’est rapide, mais ça ne doit jamais finir en prod. Pour investiguer un contexte, préférez aussi le profiler Symfony (si activé) et la commande bin/console debug:twig pour inspecter fonctions/filtres/tests disponibles.
Un réflexe utile quand un template “ne prend pas” vos modifications :
- vérifier l’environnement (
devvsprod), - vérifier si un cache a été purgé/chaud,
- vérifier que vous modifiez le bon template (namespaces
@PrestaShop/@Modules+ éventuels overrides).
Pour durcir, deux réglages font une vraie différence :
- Cache Twig en prod : vérifiez que
var/cache/prod/twig/est bien écrit et que votre déploiement ne purge pas le cache en boucle. - strict_variables en dev/test : vous voulez que le template casse dès qu’une variable attendue n’est pas fournie.
En PrestaShop, ça se joue au niveau de la config Symfony embarquée. Ne touchez pas ça en prod “pour corriger un bug vite fait” : c’est le meilleur moyen de masquer des erreurs applicatives. Si vous avez des exceptions bruyantes en prod, corrigez la source (contrôleur/presenter), et relisez vos pratiques de gestion d’erreur PHP (cf. Gestion d’erreur PHP : bonnes pratiques et configuration développement/production).
Côté industrialisation, ajoutez une étape de lint Twig en CI. Symfony fournit bin/console lint:twig (selon install) pour vérifier la syntaxe de templates sur un chemin. Combinez-le avec des outils de qualité PHP (cf. PHPStan et Rector : industrialiser la qualité du code PHP) : l’objectif est de capter les régressions avant qu’un écran admin se transforme en 500.
Exemple minimal d’étapes CI (générique) à viser :
- lint Twig (syntaxe)
- tests PHP (unitaires/integ)
- build assets (si vous versionnez du JS admin)
- éventuellement un smoke test HTTP sur une route admin clé
Sur la perf, arrêtez les débats théoriques et mesurez : rendu Twig “à froid” (cache vidé) vs “à chaud” (cache préchauffé) peut facilement faire varier votre TTFB de plusieurs dizaines de millisecondes sur des pages complexes. Un cas courant en back-office : après un déploiement qui purge var/cache, les premières requêtes prennent +50 à +150 ms le temps de recompiler plusieurs templates; ensuite vous retombez typiquement sous ~5–15 ms de coût “template engine” (ordre de grandeur) si OPcache est correctement configuré. Pour cadrer la démarche (instrumentation, tests de charge, runbooks), référez-vous à PrestaShop performance : monitoring, tests de charge et runbooks soldes.
Deux garde-fous “déploiement” qui évitent les mauvaises surprises :
- Pré-warm du cache : si votre pipeline le permet, exécutez un warmup en amont (ou juste après mise en ligne) pour éviter que le premier utilisateur admin paye le coût de compilation.
- Surveiller le volume de templates : une explosion de
include/embedpeut rendre le graphe de rendu opaque. La solution n’est pas forcément de “réécrire en PHP”, mais souvent de simplifier la composition (moins d’imbrication) et de préparer davantage le contexte côté contrôleur.
Enfin, si vous développez des modules Symfony dans PrestaShop, ne réinventez pas la structure : utilisez les namespaces Twig @PrestaShop et @Modules et restez cohérent avec les contrôleurs/services. Pour une base saine côté module (routing, services, Doctrine, templates Twig admin), l’article interne Symfony PrestaShop : développer des modules robustes avec Doctrine donne le cadre. Twig est “juste” la couche finale ; votre stabilité vient surtout de la qualité de l’architecture PHP qui l’alimente.




