PrestaShop 9 : les changements internes et comment préparer vos modules
PrestaShop 9 est là — et il casse plus que vous ne le pensez
PrestaShop 9.0 est sorti fin 2025 avec une base Symfony 6.4 LTS — passant de Symfony 4.4, sautant deux versions majeures du framework en une seule release. J’ai migré 14 modules de production vers la compatibilité PS9 jusqu’à présent. Certains se sont bien passés. D’autres ont nécessité la réécriture complète de controllers admin.

Ceci n’est pas un résumé de changelog. C’est un guide de développeur terrain sur ce qui casse réellement, avec du code avant/après pour chaque changement majeur. Si vous maintenez des modules PrestaShop — que ce soit pour le marketplace Addons, pour des clients ou pour vos propres boutiques — voici la référence de migration que j’aurais aimé avoir quand j’ai commencé.
Le saut Symfony 6.4 : pourquoi c’est important
Symfony 4.4 a atteint sa fin de vie en novembre 2023. PrestaShop 8.x fonctionnait déjà sur une version de framework non supportée. Le saut vers 6.4 LTS (supporté jusqu’en novembre 2027 pour les correctifs de sécurité) était nécessaire mais douloureux.
Les implications clés pour les développeurs de modules :
- PHP 8.1 minimum — Le code PHP 7.x ne fonctionnera pas. PHP 8.2 ou 8.3 est recommandé pour le meilleur équilibre entre fonctionnalités et stabilité.
- Les controllers comme de vrais services — Le pattern « conteneur global » est mort. L’injection de dépendances est désormais obligatoire.
- Les annotations remplacées par les attributs PHP 8 —
@Route,@AdminSecurity,@DemoRestrictedse convertissent tous en syntaxe d’attributs. - Plusieurs bibliothèques majeures supprimées — Guzzle, SwiftMailer et Tactician ont disparu du core.
- Node.js 20 minimum — Pour les modules avec des étapes de build.
Changement majeur #1 : Controllers — du conteneur global à l’injection de dépendances
C’est le changement qui piège le plus de développeurs. Dans PS8, vous pouviez appeler $this->get('service_name') dans n’importe quel controller admin pour récupérer n’importe quel service depuis le conteneur global de Symfony. Dans PS9, le conteneur passé aux controllers est un conteneur scopé qui n’inclut que les services que vous avez explicitement déclarés.
Avant (PrestaShop 8.x)
<?php
// modules/mymodule/src/Controller/Admin/MyController.php
use PrestaShopBundle\Controller\Admin\FrameworkBundleAdminController;
class MyController extends FrameworkBundleAdminController
{
public function indexAction()
{
// Grab any service from the global container
$productRepository = $this->get('prestashop.core.product.repository');
$translator = $this->get('translator');
$configService = $this->get('my_module.config_service');
return $this->render('@Modules/mymodule/views/templates/admin/index.html.twig', [
'products' => $productRepository->findAll(),
]);
}
}
Après (PrestaShop 9.x)
<?php
// modules/mymodule/src/Controller/Admin/MyController.php
use PrestaShopBundle\Controller\Admin\PrestaShopAdminController;
use MyModule\Service\ConfigService;
use Symfony\Component\HttpFoundation\Response;
class MyController extends PrestaShopAdminController
{
// Constructor injection for services you always need
public function __construct(
private readonly ConfigService $configService,
) {
}
// Method injection for request-specific services
public function indexAction(ProductRepository $productRepository): Response
{
return $this->render('@Modules/mymodule/views/templates/admin/index.html.twig', [
'products' => $productRepository->findAll(),
'config' => $this->configService->getAll(),
]);
}
// If you MUST use the service locator pattern (migration bridge)
public static function getSubscribedServices(): array
{
return parent::getSubscribedServices() + [
ConfigService::class => ConfigService::class,
];
}
}
Changements clés :
FrameworkBundleAdminControllerest déprécié — utilisezPrestaShopAdminController- L’injection par constructeur remplace les appels
$this->get() - L’injection par méthode fonctionne pour les dépendances spécifiques à une action
getSubscribedServices()est un pont de migration — utilisez-le temporairement pendant le refactoring, mais migrez vers l’injection par constructeur/méthode
Changement majeur #2 : Des annotations aux attributs PHP 8
La dépendance sensio/framework-extra-bundle a été supprimée. Toutes les annotations de routage et de sécurité doivent être converties en attributs natifs PHP 8.
Routage
// BEFORE (PS8) — Annotation syntax
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
/**
* @Route("/mymodule/settings", name="mymodule_settings")
*/
public function settingsAction()
// AFTER (PS9) — PHP 8 Attribute syntax
use Symfony\Component\Routing\Annotation\Route;
#[Route('/mymodule/settings', name: 'mymodule_settings', methods: ['GET'])]
public function settingsAction(): Response
Annotations de sécurité
// BEFORE (PS8)
use PrestaShopBundle\Security\Annotation\AdminSecurity;
use PrestaShopBundle\Security\Annotation\DemoRestricted;
/**
* @AdminSecurity("is_granted('read', request.get('_legacy_controller'))")
* @DemoRestricted(redirectRoute="mymodule_settings")
*/
public function settingsAction()
// AFTER (PS9)
use PrestaShopBundle\Security\Attribute\AdminSecurity;
use PrestaShopBundle\Security\Attribute\DemoRestricted;
#[AdminSecurity("is_granted('read', request.get('_legacy_controller'))")]
#[DemoRestricted(redirectRoute: 'mymodule_settings')]
public function settingsAction(): Response
Notez le changement de namespace : Security\Annotation\ devient Security\Attribute\. C’est un simple rechercher-remplacer, mais si vous le ratez, vos routes n’auront aucune restriction de sécurité — ce qui est pire qu’un crash.
Écouteurs d’événements
// BEFORE (PS8) — YAML service definition + interface
# config/services.yml
services:
mymodule.event_listener:
class: MyModule\EventListener\ProductListener
tags:
- { name: kernel.event_listener, event: kernel.request }
// AFTER (PS9) — PHP attribute, no YAML needed
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
#[AsEventListener(event: 'kernel.request')]
class ProductListener
{
public function __invoke(RequestEvent $event): void
{
// ...
}
}
Changement majeur #3 : Définitions de services — du YAML à la configuration PHP
Bien que les définitions de services YAML fonctionnent encore dans PS9, l’approche recommandée est la configuration basée sur PHP avec autowiring. Plus important encore, vos services doivent être correctement configurés pour l’injection de dépendances plus stricte de Symfony 6.4.
Avant (PS8 YAML)
# modules/mymodule/config/services.yml
services:
mymodule.config_service:
class: MyModule\Service\ConfigService
arguments:
- '@prestashop.adapter.legacy.configuration'
public: true
mymodule.admin_controller:
class: MyModule\Controller\Admin\MyController
arguments:
- '@mymodule.config_service'
tags:
- { name: controller.service_arguments }
Après (PS9 config PHP avec autowiring)
<?php
// modules/mymodule/config/services.php
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
return static function (ContainerConfigurator $container): void {
$services = $container->services();
$services->defaults()
->autowire()
->autoconfigure()
->public(false);
// Register all classes in src/ as services
$services->load('MyModule\\', '../src/')
->exclude('../src/{Entity}');
// Explicit service definition only when needed
$services->set(MyModule\Service\ConfigService::class)
->arg('$shopId', '%prestashop.shop_id%');
};
Avec l’autowiring, Symfony résout automatiquement les dépendances du constructeur par type-hint. Vous n’avez besoin de définitions explicites que pour :
- Les paramètres scalaires (
$shopId,$apiKey, etc.) - Les liaisons d’interfaces (quand plusieurs implémentations existent)
- Les classes tierces qui ne peuvent pas être autowirées
Changement majeur #4 : Bibliothèques supprimées
PrestaShop 9 a supprimé plusieurs bibliothèques intégrées. Si votre module les utilisait, vous devez soit les inclure vous-même, soit migrer vers le remplacement.
| Bibliothèque supprimée | Remplacement | Effort de migration |
|---|---|---|
guzzlehttp/guzzle | symfony/http-client | Moyen — L’API est différente |
swiftmailer/swiftmailer | symfony/mailer | Moyen — Nouvelle API d’objet Email |
league/tactician-bundle | symfony/messenger | Élevé — Pattern de command bus différent |
pear/archive_tar | Inclure dans le module ou utiliser ext-zip | Faible |
De Guzzle à Symfony HttpClient
// BEFORE — Guzzle
use GuzzleHttp\Client;
$client = new Client(['base_uri' => 'https://api.example.com']);
$response = $client->request('GET', '/products', [
'query' => ['status' => 'active'],
'headers' => ['Authorization' => 'Bearer ' . $token],
]);
$data = json_decode($response->getBody()->getContents(), true);
// AFTER — Symfony HttpClient
use Symfony\Contracts\HttpClient\HttpClientInterface;
// Inject via constructor
public function __construct(
private readonly HttpClientInterface $httpClient,
) {}
public function getProducts(string $token): array
{
$response = $this->httpClient->request('GET', 'https://api.example.com/products', [
'query' => ['status' => 'active'],
'auth_bearer' => $token,
]);
return $response->toArray(); // Auto-decodes JSON
}
De SwiftMailer à Symfony Mailer
// BEFORE — SwiftMailer
$message = (new \Swift_Message('Subject'))
->setFrom('shop@example.com')
->setTo('customer@example.com')
->setBody($htmlContent, 'text/html');
$this->get('mailer')->send($message);
// AFTER — Symfony Mailer
use Symfony\Component\Mime\Email;
use Symfony\Component\Mailer\MailerInterface;
public function __construct(
private readonly MailerInterface $mailer,
) {}
public function sendNotification(): void
{
$email = (new Email())
->from('shop@example.com')
->to('customer@example.com')
->subject('Subject')
->html($htmlContent);
$this->mailer->send($email);
}
Important : Le chiffrement SSL pour les emails est supprimé dans PS9. Seuls TLS ou aucun chiffrement restent. Mettez à jour votre configuration email si vous utilisiez SSL.
Changement majeur #5 : Refactorisation du Context
Le singleton legacy Context est en cours de remplacement par des services de contexte typés. Bien que Context::getContext() fonctionne encore dans PS9, il est déprécié et la nouvelle approche est plus fiable — en particulier dans les commandes CLI et les contextes asynchrones où le Context legacy n’était pas correctement initialisé.
// BEFORE — Legacy Context singleton
$employee = Context::getContext()->employee;
$shop = Context::getContext()->shop;
$language = Context::getContext()->language;
$currency = Context::getContext()->currency;
// AFTER — Typed Context services (inject via constructor)
use PrestaShop\PrestaShop\Core\Context\EmployeeContext;
use PrestaShop\PrestaShop\Core\Context\ShopContext;
use PrestaShop\PrestaShop\Core\Context\LanguageContext;
use PrestaShop\PrestaShop\Core\Context\CurrencyContext;
public function __construct(
private readonly EmployeeContext $employeeContext,
private readonly ShopContext $shopContext,
private readonly LanguageContext $languageContext,
private readonly CurrencyContext $currencyContext,
) {}
public function someMethod(): void
{
$employee = $this->employeeContext->getEmployee();
$shopId = $this->shopContext->getId();
$langId = $this->languageContext->getId();
$currency = $this->currencyContext->getCurrency();
}
Services de contexte disponibles dans PS9 : EmployeeContext, ShopContext, LanguageContext, CurrencyContext, CountryContext, ApiClientContext et LegacyControllerContext.
Changement majeur #6 : Authentification et gestion des sessions
La connexion au back office est désormais entièrement basée sur Symfony. Si votre module lit les données employé depuis Context::$cookie, c’est peu fiable dans PS9.
// BEFORE — Cookie-based auth (unreliable in PS9)
$employeeId = Context::getContext()->cookie->id_employee;
$profile = Context::getContext()->cookie->profile;
// AFTER — Session-based approach
use Symfony\Component\HttpFoundation\RequestStack;
use PrestaShop\PrestaShop\Core\Context\EmployeeContext;
public function __construct(
private readonly RequestStack $requestStack,
private readonly EmployeeContext $employeeContext,
) {}
public function getEmployeeData(): array
{
// For employee identity
$employee = $this->employeeContext->getEmployee();
// For custom session data your module stores
$session = $this->requestStack->getCurrentRequest()?->getSession();
$myData = $session?->get('my_module_custom_data');
return [
'employee_id' => $employee->getId(),
'profile_id' => $employee->getProfileId(),
'custom_data' => $myData,
];
}
Changement majeur #7 : Hooks supprimés
Plusieurs hooks liés au flux de connexion admin ont été supprimés sans remplacement direct :

actionAdminLoginControllerBeforeactionAdminLoginControllerLoginBefore/actionAdminLoginControllerLoginAfteractionAdminLoginControllerForgotBefore/actionAdminLoginControllerForgotAfteractionAdminLoginControllerResetBefore/actionAdminLoginControllerResetAfter
Remplacements pour la personnalisation des formulaires :
actionBackOfficeLoginForm— Modifier le form builder de connexionactionEmployeeRequestPasswordResetForm— Modifier le formulaire de réinitialisation de mot de passe
Tous les hooks legacy de la page produit (actionAdminProductsController*) sont également supprimés puisque la page produit legacy n’existe plus.
Changement majeur #8 : Modifications des données de templates front office
Plusieurs pages front-end utilisent désormais des classes Presenter, modifiant la structure de données disponible dans les templates Smarty. Si votre module surcharge ou étend ces templates, vous devrez mettre à jour les références de variables.
{* BEFORE (PS8) — Manufacturer page *}
<img src="{$manufacturer.image}" alt="{$manufacturer.name}">
{$manufacturer.nb_products} products
{* AFTER (PS9) — Presenter-based data structure *}
<img src="{$manufacturer.image.medium.url}" alt="{$manufacturer.name}">
{l s='%number% product' sprintf=['%number%' => $manufacturer.nb_products] d='Shop.Theme.Catalog'}
Pages concernées : Catégorie, Fabricant, Fournisseur et Magasin. Chacune dispose d’un hook actionPresent* correspondant qui permet aux modules de modifier les données présentées.
Changement majeur #9 : Sécurité des modules — plus de fichiers PHP à la racine
Les modules ne peuvent plus contenir de fichiers PHP directement exécutables au niveau racine. Cela affecte les modules qui utilisent des endpoints AJAX comme fichiers PHP autonomes.
// BEFORE — Direct PHP file (modules/mymodule/ajax.php)
require_once dirname(__FILE__) . '/../../config/config.inc.php';
// Process AJAX request...
// AFTER — ModuleFrontController
// modules/mymodule/controllers/front/ajax.php
class MyModuleAjaxModuleFrontController extends ModuleFrontController
{
public function displayAjaxProcess()
{
$result = $this->module->processAjaxRequest();
$this->ajaxRender(json_encode($result));
}
}
// URL: index.php?fc=module&module=mymodule&controller=ajax&action=process
Changement majeur #10 : Commandes console et Multi-Kernel
PS9 introduit des kernels d’application séparés pour admin, admin-api et front. Les commandes console acceptent désormais un paramètre --app-id pour cibler le bon kernel :
# Clear admin cache only
php bin/console cache:clear --env=prod --app-id=admin
# Debug front-office event listeners
php bin/console debug:event-dispatcher kernel.request --app-id=front
# List admin-api routes
php bin/console debug:router --app-id=admin-api
C’est important pour les commandes console des modules — assurez-vous que votre commande est enregistrée dans le bon contexte de kernel.
Ce qui reste inchangé (la bonne nouvelle)
Tout ne casse pas. Ces patterns continuent de fonctionner dans PS9 :
- Le système de hooks —
hookDisplayHeader(),hookActionProductUpdate(), etc. fonctionnent toujours exactement comme avant. - Les templates Smarty — Les templates Smarty du front office restent supportés. La transition vers Twig concerne uniquement l’admin.
- Le descripteur de module —
$this->name,$this->version,$this->ps_versions_compliancyfonctionnent de la même manière. Db::getInstance()— Les requêtes directes à la base de données pour les tables spécifiques aux modules restent pleinement fonctionnelles.- ObjectModel pour les tables de modules — Vos sous-classes ObjectModel personnalisées pour les tables spécifiques aux modules fonctionnent encore. La dépréciation ne concerne que les ObjectModels des entités core.
- L’API Configuration —
Configuration::get()etConfiguration::updateValue()sont inchangés.
Checklist de migration
Voici la checklist que j’utilise pour chaque migration de module. Imprimez-la, collez-la sur votre écran :
- Version PHP — Testez sur PHP 8.1+. Supprimez tout hack de compatibilité PHP 7.x.
- Annotations vers attributs — Rechercher-remplacer
Security\Annotation\parSecurity\Attribute\. Convertir@Routeen#[Route]. - Classe de base du controller —
FrameworkBundleAdminControllerversPrestaShopAdminController. - Remplacer
$this->get()— Passer à l’injection par constructeur/méthode. - Vérifier les bibliothèques supprimées — Guzzle, SwiftMailer, Tactician. Inclure ou remplacer.
- Définitions de services — Vérifiez que l’autowiring fonctionne. Testez avec
bin/console debug:container MyService --app-id=admin. - Variables de templates — Testez les surcharges de templates front-office pour les structures de données modifiées.
- Hooks supprimés — Vérifiez si votre module utilise des hooks de connexion/produit supprimés.
- Fichiers PHP à la racine — Déplacez tout endpoint AJAX direct vers ModuleFrontController.
- Mettre à jour la compatibilité — Définissez
ps_versions_compliancypour inclure 9.x :
$this->ps_versions_compliancy = [
'min' => '8.0.0',
'max' => '9.99.99',
];
Double compatibilité : supporter PS8 et PS9
La plupart des développeurs de modules doivent supporter les deux versions pendant la transition. Voici le pattern que j’utilise :
// Check PrestaShop version at runtime
if (version_compare(_PS_VERSION_, '9.0.0', '>=')) {
// PS9 path — use new Context services
} else {
// PS8 path — use legacy Context singleton
}
// For controller base class, use a compatibility layer:
if (class_exists('PrestaShopBundle\Controller\Admin\PrestaShopAdminController')) {
class MyModuleAdminControllerBase extends PrestaShopAdminController {}
} else {
class MyModuleAdminControllerBase extends FrameworkBundleAdminController {}
}
class MyActualController extends MyModuleAdminControllerBase
{
// Your controller code
}
Pour les définitions de services, vous pouvez maintenir à la fois services.yml (PS8) et services.php (PS9) — PrestaShop charge la configuration PHP si elle est présente, en se rabattant sur le YAML.
Le bilan
PrestaShop 9 est une meilleure plateforme avec une base de framework moderne. Le coût de migration est réel — j’estime 2 à 8 heures par module selon la complexité — mais le résultat est un code plus maintenable avec une injection de dépendances propre, une sécurité de typage et un accès à l’écosystème complet de Symfony 6.4.
Commencez par la documentation officielle des changements PS9 pour la liste complète des dépréciations, et testez minutieusement sur un environnement de staging avant de déployer en production.
Tous nos modules sur mypresta.rocks ont été mis à jour pour la compatibilité PS9. Si vous migrez vos propres modules et rencontrez un problème non couvert ici, contactez-nous — il y a de fortes chances que nous l’ayons déjà résolu.
David Miller construit des modules PrestaShop depuis l’ère PS 1.6. Il maintient plus de 30 modules en production sur mypresta.rocks.
Articles Connexes
- Ce Que PrestaShop 9 a Changé et Pourquoi la Compatibilité des Modules Compte
- PrestaShop 1.7 vs 8 vs 9 : Quelle Version Devriez-Vous Utiliser ?
- Construire des Modules pour Six Versions de PrestaShop : Le Défi de la Compatibilité
Commentaires (8)
Laisser un commentaire