Table des matières :
- PrestaShop 9 : ce qui change vraiment (contrôleurs = services, rendu Twig)
- Prérequis de migration : versions, cache, autoload, et “compat layer”
- Adapter un module : passer d’un contrôleur “instancié” à un contrôleur Symfony en service
- Routage, legacy controller, Tabs BO : faire cohabiter Symfony et l’héritage PrestaShop
- Twig en pratique : templates de module, héritage, et extensions (sans réinventer Smarty)
- Adapter un thème : cohabiter Smarty (FO historique) et Twig (pages Symfony) sans casser le SEO
- Debug, performance et dette Symfony : profiler, cache, OPcache, et “coûts cachés” de Twig
- Validation et déploiement : tests, CI, monitoring d’erreurs et rollback propre
- Checklist opérationnelle (modules + thèmes) pour PrestaShop 9
PrestaShop 9 : ce qui change vraiment (contrôleurs = services, rendu Twig)
PrestaShop 9 pousse plus loin la bascule entamée depuis 1.7 : le cœur n’est plus seulement « du legacy qui appelle du Symfony », mais de plus en plus « du Symfony qui doit encore composer avec du legacy ». Le point qui casse le plus de code custom, c’est la généralisation du container Symfony et la façon dont les contrôleurs sont instanciés : au lieu d’être créés manuellement (ou via des mécanismes hérités), ils deviennent des services (donc soumis à autowiring/autoconfiguration, compilation du container, cache, etc.). Résultat : tout code de module qui “new” un contrôleur, accède à des singletons, ou dépend implicitement d’un état global devient fragile.
À l’inverse, ce virage rend les dépendances explicites : Link, Translator, Logger, repositories, adaptateurs du legacy… sont injectables proprement. Cette discipline (DI) réduit les effets de bord et facilite le test unitaire, mais elle impose une structure de module plus stricte (namespaces, autoload, fichiers config/services.yml, routes, conventions de templates). Concrètement, les “implicites” qui passaient en 1.6/1.7 finissent par coûter cher : un Context::getContext() disséminé, un helper statique qui lit directement $_GET, ou un contrôleur instancié “à la main” peuvent fonctionner en dev… puis casser après compilation/warmup en prod.
Si vous n’avez pas encore mis vos modules au format Symfony-first, commencez par l’article interne Module PrestaShop 9 : structure, services et bonnes pratiques Symfony
Côté rendu, Twig est le moteur de templates natif de Symfony. Dans PrestaShop, Twig est déjà central dans les pages Symfony (notamment back-office) ; et dès que vous exposez des pages custom via contrôleurs Symfony, Twig devient l’option la plus cohérente. Comme le rappelle la documentation officielle :
“Twig is a modern template engine for PHP.” — Twig documentation
Dans la pratique, vous allez devoir faire cohabiter Twig (pages Symfony) et Smarty (front-office historique) sans perdre en SEO, en performance, ni en maintenabilité. Un bon “fil conducteur” pour décider quoi migrer en priorité : migrer d’abord les pages BO (où Symfony/Twig est déjà un standard), puis les pages custom FO qui sont déjà difficiles à maintenir en Smarty (formulaires complexes, règles métier, backends AJAX), tout en gardant les pages catalogue/checkout stables si elles sont saines.
Prérequis de migration : versions, cache, autoload, et “compat layer”
Les exemples ci-dessous visent PrestaShop 9.x avec un kernel Symfony et PHP 8.2+ (adaptez selon votre release). Avant de modifier modules et thèmes, verrouillez d’abord les prérequis d’exécution (extensions PHP, modrewrite, etc.) et la compatibilité PHP/MariaDB. Deux points reviennent systématiquement en incident post-migration : une extension manquante et un cache incohérent (container compilé sur une version, exécuté sur une autre). Référez-vous aux prérequis et compatibilités : exigences système : compatibilités PHP/MariaDB/Elasticsearch et exigences système : prérequis mod_rewrite, bcmath et GeoIP (nouveau format)
Ajoutez à ça deux “classiques” côté modules :
- Autoload PSR-4 incomplet : classes sous
src/non trouvées parce que le module n’a pas decomposer.json(ou undump-autoloadnon exécuté) ; symptôme :Class "Acme\Foo\..." not found. - Cache et environnement : des comportements différents entre staging et prod si
APP_ENV/APP_DEBUGou les droitsvar/cachene sont pas alignés (et c’est encore plus vrai en hébergement mutualisé où les permissions varient).
Ensuite, assumez qu’en PrestaShop 9 vous serez souvent en mode hybride : un module doit parfois supporter (a) une page legacy FO en Smarty, (b) une page BO Symfony/Twig, (c) des hooks legacy, (d) des services Symfony. Le bon pattern n’est pas “tout migrer d’un coup”, mais introduire une couche d’adaptation : des classes “adapter” qui isolent l’accès au Context, à la configuration, et aux objets legacy ; et des contrôleurs Symfony qui consomment ces adaptateurs.
Mini-schéma mental (utile en revue de code) :
| Besoin | Mauvais réflexe (legacy) | Cible stable en PrestaShop 9 |
|---|---|---|
| Récupérer langue/devise/shop | Context::getContext() partout |
1 adaptateur injectable qui encapsule le legacy |
| Générer des liens BO | Link::getAdminLink() à la volée |
Router Symfony en priorité + métadonnées legacy si nécessaire |
| Passer des helpers à la vue | assign Smarty / globals “maison” | Extension Twig injectée + variables explicitement passées au render() |
Enfin, traitez la migration comme une release applicative : staging, tests, rollback. La checklist globale de migration PrestaShop 9 (sécurité/SEO/perf) est un bon cadre de validation transversale : migration PrestaShop 9 : checklist complète (sécurité, SEO, performance) et, pour le plan de rollback et les tests, migration PrestaShop 9 : sécurité, tests et plan de rollback
Adapter un module : passer d’un contrôleur “instancié” à un contrôleur Symfony en service
En Symfony, un contrôleur n’est pas un script magique : c’est une fonction/méthode qui transforme une requête en réponse. La doc Symfony est explicite et (surtout) stable :
“A controller is a PHP function you create that reads information from the Request object and creates and returns a Response object.” — Symfony documentation
En PrestaShop 9, ce modèle implique que votre module déclare (1) un contrôleur PHP namespacé, (2) un service dans config/services.yml (ou équivalent), (3) une route (souvent dans config/routes.yml). Exemple minimaliste (structure indicative, à adapter à votre module).
# modules/acmefoo/config/services.yml
services:
_defaults:
autowire: true
autoconfigure: true
public: false
Acme\Foo\Controller\Admin\FooController:
public: true
tags: ['controller.service_arguments']
Acme\Foo\Twig\FooExtension:
tags: ['twig.extension']
Trois points non négociables :
- Le contrôleur doit être public (sinon le resolver ne le trouve pas).
- Les dépendances passent en constructeur, pas via
Context::getContext()partout (vous pouvez injecter un adaptateur si vous devez toucher au legacy). - Le cache Symfony est un artefact : après ajout/modification de services ou routes, vous devez invalider
var/cache/*(et éviter de déployer un cache compilé pour un autre environnement).
Deux pièges fréquents en migration “module → services” :
- Services privés par défaut : vous voyez votre classe, mais Symfony refuse de l’instancier via le resolver si le service n’est pas accessible comme contrôleur (d’où le
public: truesur le contrôleur uniquement). - Dépendances trop “grosses” : injecter tout le container (
ContainerInterface) pour “aller plus vite” revient à recréer des globals. Si vous avez besoin d’une base DI propre, la doc Symfony sur le container reste une référence (utile en équipe pour aligner les conventions) : doc Symfony sur le container
Routage, legacycontroller, Tabs BO : faire cohabiter Symfony et l’héritage PrestaShop
Dans PrestaShop, la compatibilité BO est souvent le point dur, parce que l’écosystème (tabs, permissions, liens, contrôleurs legacy) est historiquement câblé autour des noms AdminXxx. Pour éviter de casser les URLs, les redirections, et les menus, l’approche réaliste est de définir des routes Symfony qui déclarent des métadonnées legacy (_legacy_controller, _legacy_link). Exemple :
# modules/acmefoo/config/routes.yml
acmefoo_admin_foo_index:
path: /acme/foo
methods: [GET]
defaults:
_controller: 'Acme\\Foo\\Controller\\Admin\\FooController::index'
_legacy_controller: AdminAcmeFoo
_legacy_link: AdminAcmeFoo
Ce que ça vous apporte, concrètement :
- Permissions BO cohérentes : si votre tab historique s’appelle
AdminAcmeFoo, vous évitez d’introduire un nouveau nom de ressource qui “échappe” au mapping existant. - Liens et redirections plus stables : une partie de l’écosystème (modules tiers, overrides, scripts internes) continue parfois à pointer sur des identifiants legacy.
Mini-scénario réel (très fréquent) : vous migrez une page BO “Réglages du module” vers Symfony, mais le support interne a déjà des bookmarks, et des scripts d’onboarding utilisent getAdminLink('AdminAcmeFoo'). Si vous ne renseignez pas _legacy_link, vous vous retrouvez avec :
- des liens qui redirigent vers une page 404 ou vers l’ancien contrôleur,
- une tab affichée mais un contrôleur non résolu,
- ou des permissions qui ne suivent pas (accès refusé malgré un rôle admin).
Cette partie est intimement liée à la génération d’URL : certains modules continuent d’utiliser Link::getAdminLink() ou des helpers legacy, alors que d’autres basculent sur le router Symfony. Le plus propre est d’avoir un seul point de vérité (router Symfony) et un fallback legacy uniquement quand nécessaire. Pour une vue détaillée (et des pièges concrets), voir l’article interne : Génération d’URL PrestaShop : Link, routes Symfony et legacy_link
Enfin, côté hooks, attendez-vous à ce que le core ne vous “donne pas tout” dans Twig. Beaucoup d’intégrations restent basées sur des hooks dynamiques et des conventions Smarty. Si vous devez auditer précisément les hooks disponibles (et éviter d’inventer des points d’extension), utilisez la méthode de recherche/identification : Hooks PrestaShop : rechercher et identifier les hooks dynamiques
Twig en pratique : templates de module, héritage, et extensions (sans réinventer Smarty)
Le gain de Twig en module Symfony, ce n’est pas “la syntaxe est plus belle”, c’est la structuration : héritage de layouts, includes, macros, auto-escaping, et un écosystème standard (lint, cache, profiler). Si vous avez besoin d’un rappel concis (héritage/includes/variables), l’article interne est une base : Twig : syntaxe essentielle, héritage de templates et includes
Un template module BO classique se rend généralement via un namespace de type @Modules/<module>/... (selon le loader en place). Exemple de contrôleur et rendu :
// modules/acmefoo/src/Controller/Admin/FooController.php
namespace Acme\Foo\Controller\Admin;
use PrestaShopBundle\Controller\Admin\FrameworkBundleAdminController;
use Symfony\Component\HttpFoundation\Response;
final class FooController extends FrameworkBundleAdminController
{
public function index(): Response
{
// À adapter à votre système d’autorisations/tabs.
// En BO, évitez les checks “maison” : utilisez Security/Voters quand c’est possible.
$this->denyAccessUnlessGranted('ROLE_ADMIN');
return $this->render(
'@Modules/acmefoo/views/templates/admin/foo/index.html.twig',
[
'title' => 'ACME Foo',
'psVersion' => _PS_VERSION_,
]
);
}
}
Deux détails pratiques qui améliorent la maintenabilité (sans surcharger la page) :
- Passez peu de variables, mais des variables “stables” : préférez un
viewModel(tableau structuré) plutôt que 15 variables ad hoc. Ça simplifie les refactors et réduit les erreurs de clé. - Gardez la traduction explicite : dans Twig, privilégiez les mécanismes de traduction natifs (filtres/fonctions de traduction disponibles dans l’écosystème Symfony/PrestaShop) plutôt que d’inventer une couche. Ça évite de figer des chaînes en dur dans les templates.
Si votre module doit exposer des helpers dans Twig (formatage métier, accès à une config module, calculs), ne passez pas par des variables globales ad hoc. Créez une extension Twig typée, injectable, testable, et taguée twig.extension.
// modules/acmefoo/src/Twig/FooExtension.php
namespace Acme\Foo\Twig;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
final class FooExtension extends AbstractExtension
{
public function getFilters(): array
{
return [
new TwigFilter('acme_mask', fn (string $s) => preg_replace('/.(?=.{4})/u', '•', $s)),
];
}
}
Le point d’attention : le cache Twig est compilé dans var/cache/*. Si vous déployez via CI/CD, assurez-vous que (a) le cache est régénéré dans l’environnement cible, (b) l’utilisateur système a les droits, et (c) vous ne gardez pas des templates compilés pointant vers une arborescence qui n’existe pas en prod.
Astuce de debug très “terrain” : quand un template Twig ne se met pas à jour en prod, ce n’est pas forcément Twig “qui bug”, c’est souvent un cache non invalidé ou un déploiement qui ne touche pas les fichiers attendus. Ayez un réflexe simple : vérifier la date de modification des fichiers sous var/cache/prod/ et valider que le serveur cible a bien reçu la nouvelle version du template.
Adapter un thème : cohabiter Smarty (FO historique) et Twig (pages Symfony) sans casser le SEO
Sur beaucoup de shops, le front-office reste majoritairement Smarty, et c’est normal : vous n’allez pas réécrire un thème complet “juste” parce que PrestaShop 9 contient plus de Symfony. En revanche, dès que vous ajoutez des pages Symfony rendues en Twig (module ou cœur), vous devez traiter le thème comme une source de design system : mêmes tokens (couleurs, spacing), mêmes composants (boutons, alerts), mêmes conventions d’accessibilité.
La stratégie viable consiste à factoriser la CSS/JS (au niveau du thème) et à exposer des partials Twig qui réutilisent ces classes. Typiquement : vos pages Twig BO/FO doivent “parler le même CSS” que vos templates Smarty, sinon vous recréez un mini-thème parallèle ingérable. Sur ce point, le core PrestaShop n’aide pas toujours : l’outillage assets dépend de la stack du projet (legacy vs Symfony), et vous devrez parfois maintenir deux pipelines (ou un pipeline unique qui build des bundles consommés par les deux mondes).
Mini-checklist “design system” qui évite les doublons (utile quand plusieurs devs touchent au rendu) :
- une seule source de variables (Sass/CSS variables) pour les couleurs/typos/espacements ;
- un set de composants “neutres” (bouton, badge, alerte, tableau) utilisable depuis Smarty et Twig ;
- des conventions de classes stables (BEM, utility classes, ou équivalent) ;
- une règle : pas de CSS spécifique “juste pour la page Twig” sans passer par un composant réutilisable.
Côté SEO, attention au piège classique : “une page Symfony rendue en Twig” n’est pas automatiquement équivalente à “une page FO Smarty”. Il faut vérifier : balises meta, canonicals, pagination, données structurées, poids des images, et cohérence des URLs. Pour cadrer ce contrôle, utilisez le guide interne SEO produit/schema/images : SEO PrestaShop : optimiser fiches produits, schema.org et images WebP et, si vous touchez aux URLs, croisez avec la génération d’URL (router vs Link) : Génération d’URL PrestaShop
Point SEO souvent oublié en “page custom Symfony” : le maillage interne. Si vous introduisez une nouvelle route FO, assurez-vous qu’elle est liée depuis des pages existantes (ou intégrée à un menu) et qu’elle n’est pas isolée (orphan page), surtout si elle remplace une page legacy déjà indexée.
Debug, performance et dette Symfony : profiler, cache, OPcache, et “coûts cachés” de Twig
Les migrations “controllers/services/Twig” échouent rarement à cause d’une erreur de syntaxe. Elles échouent parce que le shop devient plus lent ou plus instable, et que personne n’a instrumenté. En PrestaShop 9, vous devez suivre au minimum : TTFB, nombre de requêtes SQL, temps cumulé SQL, taux de hit cache, consommation mémoire PHP-FPM, et erreurs 5xx. Pour l’outillage d’audit, appuyez-vous sur la méthode reproductible : Audit performance PrestaShop : méthode en 6 étapes reproductibles
Twig a un coût : compilation, chargement, et rendu. En prod, ce coût doit être amorti par (a) cache Twig activé, (b) OPcache correctement configuré, (c) invalidation de cache maîtrisée.
Deux signaux faibles qui révèlent un problème de cache/OPcache lors d’une migration Twig :
- TTFB qui dérive après déploiement, puis “revient à la normale” : typiquement, warmup absent → les premiers visiteurs payent la compilation.
- Erreurs intermittentes (pages blanches ponctuelles) : permissions sur
var/cache, opcache saturé, ou redéploiement qui remplace des fichiers pendant qu’ils sont compilés.
Si vous êtes sur cPanel ou un environnement où OPcache est mal configuré, corrigez d’abord ce socle : OPcache : activer et vérifier l’extension dans cPanel et, côté PHP, gardez un œil sur la chaîne de performance (PHP-FPM/OPcache/MySQL) : Performance PrestaShop : benchmarks et optimisation PHP-FPM/OPcache/MySQL
Enfin, ne sous-estimez pas la dette “Symfony dans PrestaShop” : services mal conçus, dépendances circulaires, surcharge du container, requêtes Doctrine non maîtrisées, etc. Quand vous refactorez (ou migrez) des contrôleurs en services, profilez réellement et gardez une baseline. L’article interne orienté refacto mesurable via Blackfire est directement applicable : Dette technique Symfony : profiling Blackfire et refactoring mesurable
Validation et déploiement : tests, CI, monitoring d’erreurs et rollback propre
La bascule vers des contrôleurs/services et Twig augmente votre surface de rupture : compilation du container, routage, permissions, templates, cache, et parfois compatibilité PHP. Vous devez donc valider en trois couches : (1) tests unitaires sur services/adapters, (2) tests fonctionnels sur routes (HTTP 200/302, droits, redirections), (3) tests end-to-end sur parcours critique (login BO, création commande, checkout). Même sans framework de test complet, une suite de smoke tests HTTP (curl + assertions) fait déjà gagner du temps.
Exemple minimal de “smoke test BO” (à exécuter en staging, avec un compte de test) : vérifier qu’une route répond, puis qu’elle redirige bien vers login si non authentifié, et qu’elle répond 200 une fois la session établie. L’objectif n’est pas de tester tout le BO, mais d’attraper vite : route cassée, contrôle d’accès trop strict, erreur template, erreur fatale.
En CI/CD, évitez le déploiement “à la main” : vous voulez un build reproductible, un package, et un déploiement qui sait faire un rollback. Le socle CI proposé dans l’article interne (provenance/SBOM/validation modules) s’applique très bien pour empêcher un module non conforme de partir en prod : CI PrestaShop : provenance, SBOM et validation automatique des modules (et si vous industrialisez les builds, la partie build reproductible est utile : integration continue PrestaShop : BuildKit/GitHub Actions et build reproductible).
Enfin, mettez en place une surveillance d’erreurs pragmatique : logs PHP, MySQL, JS, alertes e-mail, et corrélation avec les déploiements. Le volume d’incidents “page blanche” (erreur fatale masquée) remonte souvent pendant ce type de migration ; si vous ne voyez pas les erreurs, vous ne corrigez rien. Basez-vous sur : PrestaShop : monitoring d’erreurs, logs et alertes e-mail et gardez un plan de rollback clair : plan de rollback et tests
Checklist opérationnelle (modules + thèmes) pour PrestaShop 9
Pour finir, voici une checklist orientée exécution, à appliquer module par module (et page par page) quand vous adaptez à des contrôleurs services et à Twig. Elle ne remplace pas la checklist de migration globale, mais elle évite les oublis “Symfony/Twig” typiques.
D’abord, côté module : (a) namespaces et autoload OK (Composer si nécessaire), (b) config/services.yml présent, autowire/autoconfigure maîtrisés, (c) contrôleurs déclarés public + tags controller, (d) routes déclarées et compatibles legacy (_legacy_controller/_legacy_link) quand vous touchez au BO, (e) dépendances legacy isolées dans des adapters, (f) cache invalidé en staging/prod après changement structurel.
Ajoutez un contrôle simple avant de livrer : “est-ce que je peux supprimer var/cache/ et tout redémarre proprement ?” Si la réponse est non (permissions, warmup, config), ce n’est pas un détail : c’est un risque d’incident en prod.
Ensuite, côté Twig : (a) templates rangés, nommés et héritant d’un layout stable, (b) extensions Twig taguées twig.extension (pas de globals bricolées), (c) rendu vérifié en prod (cache Twig/OPcache), (d) i18n gérée via le traducteur Symfony/PrestaShop, (e) HTML validé (auto-escaping, XSS), (f) composants UI cohérents avec le thème (CSS/JS partagés).
Enfin, côté thème/SEO/perf : (a) URLs et redirections stables (router vs Link), (b) canonicals, schema.org, images (WebP), (c) TTFB et SQL sous contrôle (profiling), (d) monitoring d’erreurs en place, (e) scénario de rollback testé. Pour cadrer la compatibilité globale avant mise en prod, recoupez avec : Compatibilité modules PrestaShop 9 : checklist avant migration sécurisée
