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.

Développeur PHP codant les mises à jour des modules PrestaShop pour la compatibilité version 9

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, @DemoRestricted se 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 :

  • FrameworkBundleAdminController est déprécié — utilisez PrestaShopAdminController
  • 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éeRemplacementEffort de migration
guzzlehttp/guzzlesymfony/http-clientMoyen — L’API est différente
swiftmailer/swiftmailersymfony/mailerMoyen — Nouvelle API d’objet Email
league/tactician-bundlesymfony/messengerÉlevé — Pattern de command bus différent
pear/archive_tarInclure dans le module ou utiliser ext-zipFaible

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 :

Éditeur de code montrant le framework Symfony utilisé dans l'architecture de PrestaShop 9

  • actionAdminLoginControllerBefore
  • actionAdminLoginControllerLoginBefore / actionAdminLoginControllerLoginAfter
  • actionAdminLoginControllerForgotBefore / actionAdminLoginControllerForgotAfter
  • actionAdminLoginControllerResetBefore / actionAdminLoginControllerResetAfter

Remplacements pour la personnalisation des formulaires :

  • actionBackOfficeLoginForm — Modifier le form builder de connexion
  • actionEmployeeRequestPasswordResetForm — 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 hookshookDisplayHeader(), 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_compliancy fonctionnent 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 ConfigurationConfiguration::get() et Configuration::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 :

  1. Version PHP — Testez sur PHP 8.1+. Supprimez tout hack de compatibilité PHP 7.x.
  2. Annotations vers attributs — Rechercher-remplacer Security\Annotation\ par Security\Attribute\. Convertir @Route en #[Route].
  3. Classe de base du controllerFrameworkBundleAdminController vers PrestaShopAdminController.
  4. Remplacer $this->get() — Passer à l’injection par constructeur/méthode.
  5. Vérifier les bibliothèques supprimées — Guzzle, SwiftMailer, Tactician. Inclure ou remplacer.
  6. Définitions de services — Vérifiez que l’autowiring fonctionne. Testez avec bin/console debug:container MyService --app-id=admin.
  7. Variables de templates — Testez les surcharges de templates front-office pour les structures de données modifiées.
  8. Hooks supprimés — Vérifiez si votre module utilise des hooks de connexion/produit supprimés.
  9. Fichiers PHP à la racine — Déplacez tout endpoint AJAX direct vers ModuleFrontController.
  10. Mettre à jour la compatibilité — Définissez ps_versions_compliancy pour 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é
Partager cet article:
David Miller

David Miller

Plus d'une décennie d'expertise pratique PrestaShop. David développe des modules e-commerce haute performance axés sur le SEO, l'optimisation du passage en caisse et la gestion de boutique....

Cet article vous a plu ?

Recevez nos derniers conseils, guides et mises à jour de modules dans votre boîte mail.

Commentaires (8)

T
Thomas Mueller 14/02/2026
We just finished migrating our checkout module to PS9. The process was smoother than expected - most hooks work identically. The main pain point was updating our admin controllers from annotations to attributes.
Répondre
E
Elena Rodriguez 14/02/2026
Does anyone know if the hook system changes affect actionProductUpdate? We rely heavily on that hook for our inventory sync module.
Répondre
D
David Miller 14/02/2026
Elena, actionProductUpdate works the same way in PS9. The hook system is fully backwards-compatible. Your inventory sync module should be fine without changes.
J
Jakub Kowalski 14/02/2026
The migration checklist is super helpful. One thing I would add: test your cron jobs and CLI commands too. We found that some of our module console commands broke because of the Symfony 6.4 DI container changes.
Répondre
S
Sophie Laurent 14/02/2026
Great point about CLI commands Jakub! We had a similar issue with our import cron. The fix was updating the service injection in our console command class.
S
Sophie Laurent 14/02/2026
Finally someone explains the API Platform integration clearly. We have been struggling with the old webservice for years. The OAuth2 scoped tokens alone make the upgrade worth it.
Répondre
M
Marcus Weber 14/02/2026
Great overview of the PS9 changes! The Doctrine ORM migration is definitely the biggest challenge for us. We have about 15 modules that use ObjectModel extensively. Any tips on a gradual migration path?
Répondre
D
David Miller 14/02/2026
Thanks Marcus! For a gradual migration, I recommend starting with read-only Doctrine queries alongside your existing ObjectModel code. That way you can test without risk. Replace ObjectModel writes only after thorough testing.

Laisser un commentaire

Loading...
Back to top