PrestaShop 9: cosa e cambiato sotto il cofano è come preparare i moduli

PrestaShop 9 è arrivato — e rompe più di quanto pensi

PrestaShop 9.0 è stato rilasciato alla fine del 2025 con una base Symfony 6.4 LTS — saltando da Symfony 4.4, saltando due versioni major del framework in un'unica release. Finora ho migrato 14 moduli di produzione alla compatibilità con PS9. Alcuni sono andati lisci. Altri hanno richiesto la riscrittura completa dei controller admin.

Sviluppatore PHP che programma aggiornamenti dei moduli PrestaShop per la compatibilità con la versione 9

Questo non è un riepilogo del changelog. Questa è la guida di uno sviluppatore sul campo su cosa si rompe effettivamente, con codice prima-e-dopo per ogni cambiamento importante. Se mantieni moduli PrestaShop — che sia per il marketplace Addons, per i clienti o per i tuoi negozi — questo è il riferimento per la migrazione che avrei voluto esistesse quando ho iniziato.

Il salto a Symfony 6.4: perché è importante

Symfony 4.4 ha raggiunto il fine vita a novembre 2023. PrestaShop 8.x stava già funzionando su una versione del framework non più supportata. Il salto alla 6.4 LTS (supportata fino a novembre 2027 per le correzioni di sicurezza) era necessario ma doloroso.

Le implicazioni chiave per gli sviluppatori di moduli:

  • PHP 8.1 minimo — Il codice PHP 7.x non funzionerà. PHP 8.2 o 8.3 raccomandati per il miglior equilibrio tra funzionalità e stabilità.
  • Controller come servizi a tutti gli effetti — Il pattern del “container globale” è morto. La dependency injection è ora obbligatoria.
  • Annotation sostituite dagli attributi PHP 8@Route, @AdminSecurity, @DemoRestricted si convertono tutti nella sintassi degli attributi.
  • Diverse librerie importanti rimosse — Guzzle, SwiftMailer e Tactician sono stati rimossi dal core.
  • Node.js 20 minimo — Per i moduli con step di build.

Breaking Change #1: Controller — Dal container globale alla Dependency Injection

Questo è il cambiamento che fa inciampare più sviluppatori. In PS8, potevi chiamare $this->get('service_name') in qualsiasi controller admin per recuperare qualsiasi servizio dal container globale di Symfony. In PS9, il container passato ai controller è un container circoscritto che include solo i servizi che hai dichiarato esplicitamente.

Prima (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(),
        ]);
    }
}

Dopo (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,
        ];
    }
}

Cambiamenti chiave:

  • FrameworkBundleAdminController è deprecato — usa PrestaShopAdminController
  • La constructor injection sostituisce le chiamate $this->get()
  • La method injection funziona per le dipendenze specifiche dell'azione
  • getSubscribedServices() è un ponte di migrazione — usalo temporaneamente durante il refactoring, ma passa alla constructor/method injection

Breaking Change #2: Dalle Annotation agli attributi PHP 8

La dipendenza sensio/framework-extra-bundle è stata rimossa. Tutte le annotation di routing e sicurezza devono essere convertite in attributi nativi PHP 8.

Routing

// 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

Annotation di sicurezza

// 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

Nota il cambio di namespace: Security\Annotation\ diventa Security\Attribute\. È un semplice cerca-e-sostituisci, ma se lo dimentichi, le tue route non avranno restrizioni di sicurezza — il che è peggio di un crash.

Event Listener

// 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
    {
        // ...
    }
}

Breaking Change #3: Definizioni dei servizi — Da YAML alla configurazione PHP

Sebbene le definizioni dei servizi YAML funzionino ancora in PS9, l'approccio raccomandato è la configurazione basata su PHP con autowiring. Ancora più importante, i tuoi servizi devono essere correttamente configurati per la dependency injection più rigida di Symfony 6.4.

Prima (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 }

Dopo (PS9 configurazione PHP con 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%');
};

Con l'autowiring, Symfony risolve automaticamente le dipendenze del costruttore tramite type-hint. Hai bisogno di definizioni esplicite solo per:

  • Parametri scalari ($shopId, $apiKey, ecc.)
  • Binding di interfacce (quando esistono implementazioni multiple)
  • Classi di terze parti che non possono essere autowired

Breaking Change #4: Librerie rimosse

PrestaShop 9 ha rimosso diverse librerie incluse. Se il tuo modulo le usava, devi includerle nel modulo stesso o migrare alla sostituzione.

Libreria rimossaSostituzioneSforzo di migrazione
guzzlehttp/guzzlesymfony/http-clientMedio — API diversa
swiftmailer/swiftmailersymfony/mailerMedio — nuova API oggetto Email
league/tactician-bundlesymfony/messengerAlto — pattern command bus diverso
pear/archive_tarIncludi nel modulo o usa ext-zipBasso

Da Guzzle a 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
}

Da SwiftMailer a 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);
}

Importante: La crittografia SSL per le email è stata rimossa in PS9. Rimangono solo TLS o nessuna crittografia. Aggiorna la tua configurazione email se stavi usando SSL.

Breaking Change #5: Refactorizzazione del Context

Il singleton legacy Context viene sostituito da servizi di contesto tipizzati. Sebbene Context::getContext() funzioni ancora in PS9, è deprecato e il nuovo approccio è più affidabile — specialmente nei comandi CLI e nei contesti asincroni dove il Context legacy non era correttamente inizializzato.

// 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();
}

Servizi di contesto disponibili in PS9: EmployeeContext, ShopContext, LanguageContext, CurrencyContext, CountryContext, ApiClientContext e LegacyControllerContext.

Breaking Change #6: Autenticazione e gestione delle sessioni

Il login del back office è ora completamente basato su Symfony. Se il tuo modulo legge i dati degli impiegati da Context::$cookie, questo non è più affidabile in 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,
    ];
}

Breaking Change #7: Hook rimossi

Diversi hook relativi al flusso di login admin sono stati rimossi senza sostituzione diretta:

Editor di codice che mostra il framework Symfony utilizzato nell'architettura di PrestaShop 9

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

Sostituzioni per la personalizzazione dei form:

  • actionBackOfficeLoginForm — Modifica il form builder di login
  • actionEmployeeRequestPasswordResetForm — Modifica il form di reset password

Tutti gli hook legacy della pagina prodotto (actionAdminProductsController*) sono stati rimossi poiché la pagina prodotto legacy non esiste più.

Breaking Change #8: Modifiche ai dati dei template Front Office

Diverse pagine front-end ora usano classi Presenter, cambiando la struttura dati disponibile nei template Smarty. Se il tuo modulo sovrascrive o estende questi template, dovrai aggiornare i riferimenti alle variabili.

{* 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'}

Pagine interessate: Categoria, Produttore, Fornitore e Negozio. Ciascuna ha un hook actionPresent* corrispondente che permette ai moduli di modificare i dati presentati.

Breaking Change #9: Sicurezza dei moduli — Niente file PHP nella root

I moduli non possono più contenere file PHP direttamente eseguibili a livello root. Questo riguarda i moduli che usano endpoint AJAX come file PHP standalone.

// 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

Breaking Change #10: Comandi Console e Multi-Kernel

PS9 introduce kernel applicazione separati per admin, admin-api e front. I comandi console ora accettano un parametro --app-id per puntare al kernel corretto:

# 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

Questo è importante per i comandi console dei moduli — assicurati che il tuo comando sia registrato nel contesto del kernel corretto.

Cosa resta uguale (la buona notizia)

Non tutto si rompe. Questi pattern continuano a funzionare in PS9:

  • Il sistema degli hookhookDisplayHeader(), hookActionProductUpdate(), ecc. funzionano esattamente come prima.
  • Template Smarty — I template Smarty del front office rimangono supportati. La transizione a Twig è solo per l'admin.
  • Descrittore del modulo$this->name, $this->version, $this->ps_versions_compliancy funzionano allo stesso modo.
  • Db::getInstance() — Le query dirette al database per le tabelle specifiche del modulo rimangono pienamente funzionali.
  • ObjectModel per le tabelle del modulo — Le tue sottoclassi personalizzate di ObjectModel per le tabelle specifiche del modulo funzionano ancora. La deprecazione riguarda solo gli ObjectModel delle entità core.
  • API ConfigurationConfiguration::get() e Configuration::updateValue() sono invariati.

Checklist di migrazione

Ecco la checklist che uso per ogni migrazione di modulo. Stampala, attaccala al monitor:

  1. Versione PHP — Testa su PHP 8.1+. Rimuovi qualsiasi hack di compatibilità PHP 7.x.
  2. Da annotation ad attributi — Cerca-e-sostituisci Security\Annotation\ con Security\Attribute\. Converti @Route in #[Route].
  3. Classe base del controllerFrameworkBundleAdminController diventa PrestaShopAdminController.
  4. Sostituisci $this->get() — Passa alla constructor/method injection.
  5. Controlla le librerie rimosse — Guzzle, SwiftMailer, Tactician. Includi nel modulo o sostituisci.
  6. Definizioni dei servizi — Verifica che l'autowiring funzioni. Testa con bin/console debug:container MyService --app-id=admin.
  7. Variabili dei template — Testa gli override dei template front-office per le strutture dati modificate.
  8. Hook rimossi — Controlla se il tuo modulo usa hook di login/prodotto rimossi.
  9. File PHP nella root — Sposta qualsiasi endpoint AJAX diretto nei ModuleFrontController.
  10. Aggiorna la compatibilità — Imposta ps_versions_compliancy per includere 9.x:
$this->ps_versions_compliancy = [
    'min' => '8.0.0',
    'max' => '9.99.99',
];

Doppia compatibilità: supportare PS8 e PS9

La maggior parte degli sviluppatori di moduli deve supportare entrambe le versioni durante la transizione. Ecco il pattern che uso:

// 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
}

Per le definizioni dei servizi, puoi mantenere sia services.yml (PS8) che services.php (PS9) — PrestaShop carica la configurazione PHP se presente, con fallback su YAML.

Conclusione

PrestaShop 9 è una piattaforma migliore con una base framework moderna. Il costo della migrazione è reale — stimerei 2-8 ore per modulo a seconda della complessità — ma il risultato è un codice più manutenibile con dependency injection corretta, type safety e accesso all'intero ecosistema di Symfony 6.4.

Inizia dalla documentazione ufficiale delle modifiche PS9 per l'elenco completo delle deprecazioni, e testa accuratamente su un ambiente di staging prima di fare il deploy in produzione.

Tutti i nostri moduli su mypresta.rocks sono stati aggiornati per la compatibilità con PS9. Se stai migrando i tuoi moduli e incontri qualcosa non trattato qui, contattaci — probabilmente l'abbiamo già risolto.

David Miller sviluppa moduli PrestaShop dall'era PS 1.6. Mantiene oltre 30 moduli di produzione su mypresta.rocks.

Articoli Correlati

  • Cosa Ha Cambiato PrestaShop 9 e Perché la Compatibilità dei Moduli Conta
  • PrestaShop 1.7 vs 8 vs 9: Quale Versione Dovreste Usare?
  • Costruire Moduli per Sei Versioni di PrestaShop: La Sfida della Compatibilità
Condividi questo articolo:
David Miller

David Miller

Oltre un decennio di esperienza pratica con PrestaShop. David sviluppa moduli e-commerce ad alte prestazioni focalizzati su SEO, ottimizzazione del checkout e gestione del negozio. Appassionato di...

Ti è piaciuto questo articolo?

Ricevi i nostri ultimi consigli, guide e aggiornamenti dei moduli nella tua casella di posta.

Commenti (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.
Rispondi
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.
Rispondi
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.
Rispondi
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.
Rispondi
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?
Rispondi
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.

Lascia un commento

Loading...
Back to top