Cosa si rompe davvero quando porti un modulo a PrestaShop 9

Stiamo portando il nostro catalogo di oltre 140 moduli a PrestaShop 9 dalla release 9.0 di giugno 2025, e la risposta onesta è: più di quanto suggerisca il changelog. Alcuni moduli hanno richiesto un pomeriggio. Alcuni quasi una settimana, perché gli admin controller erano stati scritti contro il service container globale e quel pattern non esiste più.

Sviluppatore PHP che aggiorna moduli PrestaShop per la compatibilità con la versione 9

Questo non è un post marketing. È il documento che abbiamo costruito internamente durante la migrazione, con ogni breaking change che abbiamo davvero incontrato, il codice prima/dopo e le parti dove non siamo ancora sicuri al 100% che la migrazione sia completa. Se mantieni moduli, per clienti, per Addons o per i tuoi shop, leggilo prima di dire a qualcuno che il modulo è "compatibile PS 9". Abbiamo imparato nel modo duro che "si installa senza fatal error" non è la stessa cosa.

Ordina il modulo per rischio prima di toccare codice

L'errore più grande nelle prime tre migrazioni è stato riscrivere prima le parti facili e scoprire quelle difficili il giorno prima del rilascio. Non farlo. Guarda che tipo di modulo è e dove la superficie di cambiamento è più grande.

Tipo moduloDove di solito si rompe in PS 9Cosa testare prima
Admin controller Symfony modernoContainer scoped, wiring servizi, attributi route, attributi securityApri ogni controller in debug mode e conferma che i servizi iniettati nel costruttore si risolvano davvero
Legacy AdminController (Bootstrap / jQuery)URL token, endpoint AJAX, bulk actions, helper listaProva filtri lista, azioni riga, modali, endpoint AJAX, export, bulk actions: tutti
Modulo front-office / temaAssunzioni template solo Classic, differenze markup HummingbirdProduct card, carrello, checkout, account cliente e ogni hook front su Classic e Hummingbird
Checkout, pagamento o spedizioneAssunzioni su carrello, corriere, sconti e flusso ordine cambiate dai sistemi 9.1Esegui ordini reali di test: guest, account, con sconti, più corrieri
Integrazione / ERP / exportWebservice legacy vs nuova AdminAPI; warning strict PHP 8.5Smoke test su 8.2, 9.0 e 9.1 prima di cambiare il contratto integrazione
Email / notificheSwiftMailer è sparito; Symfony Mailer renderizza template in modo diversoInvia ogni tipo di email transazionale e controlla header, variabili, allegati

I nostri moduli di checkout-flow hanno richiesto più tempo. Qualsiasi cosa toccasse la pagina ordine admin o la selezione corriere è stata rivista riga per riga. Il "piccolo modulo utility che aggiunge solo un banner" ha richiesto mezza mattina.

Aggiornamento maggio 2026: ora il vero target è 9.1, non 9.0

Quando abbiamo iniziato la migrazione la risposta era "compatibile con 9.0". Un anno dopo, 9.1 è ciò che i merchant installano, e 9.0 è uno stepping stone da cui si aggiornano. Se la tua matrice CI è ancora fissata a 9.0, stai testando il PrestaShop di ieri.

9.1 dovrebbe essere backward compatible con 9.0, e in gran parte lo è. Ma aggiunge supporto PHP 8.5, porta Hummingbird come tema default per installazioni nuove, introduce nuovi lifecycle hook e hook di configurazione, aggiunge comandi CLI per miniature / ricerca / traduzioni e porta sistemi multi-shipment e discount dietro feature flag. Ognuna di queste cose può rompere in silenzio un modulo che funzionava su 9.0.

La maintenance release 9.1.3 di maggio 2026 ha portato aggiornamenti Symfony e Twig senza modifiche core: il tipo di patch release che "non dovrebbe influenzare i moduli" e poi lo fa, la prima volta che qualcuno passa un non-string a un filtro Twig. Testa sul ramo 9.x mantenuto, non solo sul tag 9.0 originale.

Cosa è cambiato tra 9.0 e 9.1, e cosa ritestiamo

AreaCosa è cambiatoCosa ritestiamo
Runtime9.0 richiede PHP 8.1+ e supporta fino a 8.4; 9.1 aggiunge e raccomanda 8.5Esegui il modulo sulla versione PHP più bassa e più alta dichiarata dal composer.json
Stack SymfonySymfony 4.4 → 6.4 LTS: si applicano i breaking change di Symfony 5 e 6Ereditarietà controller, annotazioni, eventi, definizioni servizi, package framework rimossi
Hook e CLI9.1 aggiunge lifecycle hook, hook update config e comandi CLI per miniature / ricerca / export traduzioniInstall, enable, disable, upgrade, salvataggio configurazione, rigenerazione miniature, reindex ricerca, export traduzioni
HummingbirdTema default nelle nuove installazioni 9.1; gli shop aggiornati mantengono il temaOutput front-office su Classic e Hummingbird: ogni template prodotto, carrello, checkout, account
Sconti9.1 introduce UI e architettura sconti ridisegnate sopra le regole carrelloTutto ciò che legge, crea, combina, valida o mostra cart rules, voucher e promozioni
Database / ordini9.1 ha modifiche schema per multi-shipment e nuovo sistema scontiExport ordine, fattura, fulfillment, corriere e codice ERP contro fixture ordine reali
Patch security9.1.3 ha aggiornato dipendenze Symfony e TwigRegression pass rapido dopo ogni patch, soprattutto se tocchi YAML, XML, Twig o destinatari email

Il salto a Symfony 6.4: perché è cambiato così tanto

Symfony 4.4 è andato end-of-life a novembre 2023. PrestaShop 8.x girava già su una versione framework non supportata, cosa scomoda per chi leggeva i log PHP. Saltare direttamente a 6.4 LTS, supportata fino a novembre 2027, era la scelta giusta, ma ha saltato due major framework in una release, e ogni breaking change di Symfony 5 e 6 arriva insieme.

Implicazioni pratiche:

  • PHP 8.1 minimo: eventuali shim PHP 7.x nel modulo devono uscire. 8.2-8.4 sono supportati su 9.0; 9.1 aggiunge 8.5.
  • I controller sono servizi veri: il pattern "container globale, prendi quello che serve" è morto. L'injection via costruttore è obbligatoria e il container passato al controller è scoped.
  • Attributi, non annotazioni: @Route, @AdminSecurity, @DemoRestricted diventano sintassi attributi PHP 8. sensio/framework-extra-bundle è sparito.
  • Librerie bundle rimosse: Guzzle, SwiftMailer, Tactician. Se le usavi, devi includerle tu o migrare.
  • Node 20 minimo per moduli con build step. 9.1 compila con Node 20.19.5, quindi fissa il tooling invece di sperare nel Node della macchina dev.

Breaking change #1: controller, il container globale è sparito

Questo è il cambiamento che ci ha mangiato più tempo. In PS 8 potevamo prendere qualunque servizio da qualunque controller con $this->get('service.name'). In PS 9 il container consegnato al controller è scoped: contiene solo i servizi dichiarati esplicitamente, e la chiamata nuda $this->get() lancia eccezione o restituisce null secondo il servizio.

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

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

I cambiamenti principali:

  • FrameworkBundleAdminController è deprecato. Usa PrestaShopAdminController.
  • L'injection via costruttore sostituisce $this->get() per ciò che il controller usa sempre.
  • La method injection gestisce dipendenze specifiche dell'action.
  • getSubscribedServices() è la via di fuga per un controller mezzo migrato. Usala per tenere acceso il modulo mentre rifattorizzi, non come stato finale da spedire.

Una cosa che ci ha colpiti: se il servizio decora un core service, l'autowiring sulla versione decorata smette di funzionare appena un terzo modulo nella catena non autowira correttamente. È successo su un modulo che decora il cart calculator e abbiamo perso mezza giornata su un services.yml corretto prima di capire che il problema era un altro modulo installato. La service decoration in PS 9 non perdona l'ordine.

Breaking change #2: le annotazioni diventano attributi PHP 8

La dipendenza sensio/framework-extra-bundle è sparita. Routing e annotazioni security devono diventare attributi nativi. È quasi tutto meccanico, ma c'è una trappola.

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

Security

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

Il namespace passa da Security\Annotation\ a Security\Attribute\. Un find-and-replace copre quasi tutto. Se lo perdi, la route gira senza restrizioni di sicurezza: peggio di un crash, perché il modulo sembra funzionare e espone in silenzio un'azione admin senza autorizzazione. Fai grep del codebase per Security\Annotation dopo la migrazione.

Event listeners

// 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 servizi, YAML funziona ancora ma PHP è più pulito

I servizi YAML continuano a funzionare in PS 9 e li abbiamo mantenuti dove erano già corretti. Il nuovo lavoro va in config PHP con autowiring, perché i vincoli che Symfony 6.4 mette sul service container sono più facili da soddisfare quando la config è nella stessa lingua dei servizi.

Before (PS 8 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 }

After (PS 9 PHP config with 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 autowiring attivo scrivi definizioni esplicite solo per parametri scalari, binding di interfacce quando esistono più implementazioni e classi terze non autowirable. Se hai scritto definizioni servizi a mano per dieci anni, questa parte sembra sospetta. Funziona. Fidati.

Breaking change #4: librerie che non sono più incluse

PrestaShop 9 ha smesso di bundleizzare quattro librerie viste in molti moduli vecchi. Se il tuo composer.json le richiede come dipendenza top-level sei a posto: si installano per il modulo. Se assumevi fossero sempre disponibili perché PS 8 le includeva, hai lavoro da fare.

RimossoSostitutoSforzo migrazione
guzzlehttp/guzzlesymfony/http-clientMedio: API diversa
swiftmailer/swiftmailersymfony/mailerMedio: nuova API oggetto Email
league/tactician-bundlesymfony/messengerAlto: diverso modello command bus
pear/archive_tarBundlalo nel modulo o usa ext-zipBasso

Guzzle to 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
}

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

Una cosa che la documentazione non urla abbastanza: la crittografia SSL per email è sparita in PS 9. Restano solo TLS o nessuna crittografia. Se lo shop di un cliente era configurato con SSL sul relay SMTP, le email in uscita possono smettere di funzionare dopo l'upgrade. Abbiamo visto un upgrade 9.0 passare pulito e poi i clienti non ricevere conferme ordine per due giorni. Controlla la configurazione mail su ogni shop che aggiorni.

Breaking change #5: i servizi Context tipizzati sostituiscono il singleton

Il singleton legacy Context::getContext() funziona ancora in PS 9: è deprecato, non rimosso. Il nuovo pattern sono servizi Context tipizzati iniettati dal container, e funziona in punti dove il singleton non ha mai funzionato bene (comandi CLI, worker async, ovunque il Context legacy non sia inizializzato dal front controller).

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

Il set completo dei servizi context in PS 9: EmployeeContext, ShopContext, LanguageContext, CurrencyContext, CountryContext, ApiClientContext, LegacyControllerContext. Il singleton legacy resta per ora e il codice esistente continua a girare. Noi migriamo modulo per modulo, non in un unico sweep, e va bene così.

Breaking change #6: l'auth admin ora è completamente Symfony

Il login back office è passato a Symfony Security. Se il modulo legge dati employee da Context::$cookie, cosa vista in molti moduli vecchi, quel percorso è inaffidabile in PS 9.

// 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 del flusso login admin sono spariti senza sostituto diretto.

Editor di codice con framework Symfony usato nell'architettura PrestaShop 9

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

Per personalizzare il form login, i nuovi hook sono:

  • actionBackOfficeLoginForm — modifica il form builder login
  • actionEmployeeRequestPasswordResetForm — modifica il form reset password

Anche tutti gli hook actionAdminProductsController* sono rimossi, perché la vecchia pagina prodotto legacy non esiste più. Se il modulo aggiungeva campi extra alla vecchia pagina prodotto tramite uno di questi hook, devi riscrivere quella parte contro la pagina prodotto Symfony: molto più che cambiare nome hook.

Breaking change #8: i template front-office ora usano dati Presenter

Diverse pagine front (categoria, produttore, fornitore, negozio) sono passate a classi Presenter. Le variabili Smarty esposte hanno forma diversa. Se il modulo sovrascrive o estende uno di questi template, i riferimenti variabile vanno cambiati.

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

Ogni pagina coinvolta ha un hook actionPresent* corrispondente per modificare i dati presentati invece di sovrascrivere il template. Usalo quando puoi: sopravvive ai cambi tema e alle riscritture template Hummingbird, e abbiamo perso abbastanza tempo a sistemare override dopo revisioni Hummingbird da preferire non possederli.

Breaking change #9: niente più endpoint PHP standalone a root modulo

Serve ancora il file PHP principale del modulo. Il pattern problematico è un ajax.php o callback.php direttamente chiamabile nella root del modulo che include a mano config/config.inc.php. Questi endpoint bypassano routing, token, rilevamento SSL, context cliente e il normale lifecycle request, e gli strumenti security di PS 9 li considerano un odore.

// 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 split multi-kernel

PS 9 divide l'applicazione in tre kernel Symfony: admin, admin-api e front. I comandi console accettano --app-id per targetizzare un kernel specifico.

# 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

Se il modulo include un comando console, assicurati che sia registrato nel kernel giusto. Avevamo un comando CLI per reindicizzare la ricerca registrato senza specificare kernel: funzionava su PS 8, ma su PS 9 non veniva scoperto finché non abbiamo aggiunto il tag corretto.

Cosa non è cambiato (le parti che non abbiamo riscritto)

Non tutto si rompe. Questi pattern sopravvivono in PS 9 senza modifiche.

  • Il sistema hookhookDisplayHeader(), hookActionProductUpdate() e tutti gli altri funzionano come prima. Vedi il nostro riferimento hook per i pattern sopravvissuti al salto versione.
  • Template Smarty front — la migrazione Twig è solo admin. Il front-office resta Smarty.
  • Descriptor modulo$this->name, $this->version, $this->ps_versions_compliancy non cambiano.
  • Db::getInstance() — query DB dirette sulle tabelle del tuo modulo continuano a funzionare.
  • ObjectModel per tabelle modulo — le tue sottoclassi ObjectModel per tabelle custom funzionano ancora. La deprecazione colpisce gli ObjectModel delle entity core, non i tuoi.
  • Configuration APIConfiguration::get() / Configuration::updateValue() sono invariati.

La matrice di test che eseguiamo davvero

Una installazione verde su PS 9 non equivale a compatibilità. Lo abbiamo imparato su un modulo che superava lo smoke test, è arrivato su due shop cliente e poi crashava quando il merchant provava il pulsante bulk-action. Questa è la matrice che usiamo su ogni modulo commerciale prima di marcarlo "9.x compatible".

TargetPerché contaCosa significa "pass"
PrestaShop 8.2 su PHP 8.1 / 8.2Molti shop production resteranno su PS 8 mentre testano 9La build stabile esistente si installa, aggiorna e gira senza rumore deprecation che blocca il lavoro normale
PrestaShop 9.0 su PHP 8.1–8.4Prova la migrazione major: Symfony 6.4, PHP 8.1 minimo, librerie rimosseOgni controller, hook, cron job, endpoint AJAX e percorso email gira su una 9.0 pulita
Ramo PrestaShop 9.1Il target pratico corrente, ciò che i merchant installano davveroIl modulo sopravvive a Hummingbird default, cambi sconti/ordini e ultime patch release
Temi Classic e HummingbirdLe nuove 9.1 usano Hummingbird, gli shop aggiornati mantengono temi derivati da ClassicGli hook front renderizzano senza layout rotto, selector mancanti, contenuti duplicati o errori JS
Fresh install e upgrade da vecchia versione moduloSQL upgrade e default config falliscono in modi diversi rispetto a installazioni frescheEntrambi i percorsi producono stesse tabelle, configurazioni, tab, hook e permessi finali
Shop con debug mode attivoWarning nascosti in prod diventano ticket supporto dopoNessun undefined index, warning dynamic property, deprecation o container exception nell'uso normale

La riga "fresh install e upgrade" non è negoziabile. Molti dei peggiori bug spediti negli anni erano nel percorso upgrade: fresh install funzionava sempre perché testavamo fresh install. Lo shop che si rompeva era di un cliente esistente, con database pieno di valori che il nostro SQL upgrade non considerava.

La nostra checklist di migrazione

La teniamo in un file markdown in ogni repo modulo. Stampala, incollala, fanne ciò che vuoi.

  1. Versione PHP — testa su 8.1+. Rimuovi shim PHP 7.x.
  2. Annotazioni ad attributi — find-replace Security\Annotation\ con Security\Attribute\. Converti @Route in #[Route]. Grep per superstiti.
  3. Classe base controllerFrameworkBundleAdminController a PrestaShopAdminController.
  4. Sostituisci $this->get() — passa a constructor o method injection.
  5. Librerie rimosse — Guzzle, SwiftMailer, Tactician, pear/archive_tar. Bundla o sostituisci.
  6. Definizioni servizi — conferma che autowiring funzioni. bin/console debug:container MyService --app-id=admin.
  7. Variabili template — testa override front contro la nuova forma Presenter.
  8. Hook rimossi — grep per hook login e product deprecati.
  9. Endpoint PHP standalone — sposta file AJAX o callback diretti a ModuleFrontController.
  10. Aggiorna compliancy — imposta ps_versions_compliancy per includere 9.x:
$this->ps_versions_compliancy = [
    'min' => '8.0.0',
    'max' => '9.99.99',
];

Una nota onesta sull'ultimo punto: impostiamo l'upper bound a 9.99.99 solo sui moduli dove abbiamo eseguito la matrice completa contro l'ultima 9.1. Metterlo su un modulo solo smoke-tested è la bugia che rompe gli shop dei clienti.

Supportare PS 8 e PS 9 dalla stessa codebase

Per moduli commerciali non possiamo spedire due versioni e aspettarci che i clienti scelgano quella giusta. Il pattern che ha funzionato è un version check runtime con due code path e una compatibility base class.

// 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 i servizi puoi tenere sia services.yml (PS 8) sia services.php (PS 9) nello stesso modulo. PrestaShop carica il file PHP se esiste e ripiega su YAML.

Questo pattern dual-codebase vive nel nostro package condiviso prestashop-compat e lo estendiamo man mano che incontriamo differenze. Ogni metodo deprecato o rimosso in una versione futura viene wrappato, e il wrapper rileva la versione a runtime. Tools::displayPrice() è l'esempio canonico: sparito in 9.0, sostituito dal formatter locale-aware, wrappato in \MyPrestaRocks\Compat\PriceFormatter::format() così lo correggiamo in un solo posto.

Vincoli Composer che spediamo davvero

Risolvi le dipendenze contro la versione PHP più bassa che dichiari di supportare, poi testa separatamente contro la più alta. Altrimenti Composer sceglie felicemente librerie che richiedono 8.4 e la tua dichiarazione "8.1+" è finzione.

{
  "require": {
    "php": ">=8.1 <8.6",
    "symfony/http-client": "^6.4"
  },
  "config": {
    "platform": {
      "php": "8.1.0"
    }
  }
}
matrix:
  include:
    - prestashop: "8.2"
      php: "8.1"
    - prestashop: "9.0"
      php: "8.4"
    - prestashop: "9.1"
      php: "8.5"
      theme: "hummingbird"

Cosa diremmo a uno sviluppatore alla prima migrazione PS 9

PrestaShop 9 è una piattaforma migliore di 8. Symfony 6.4 è attuale, i servizi context tipizzati sono più gradevoli del singleton, l'autowiring rimuove una classe di bug che avevamo spesso, e l'admin-api accanto al Webservice legacy dà agli integratori una strada avanti. Il costo è reale: da mezza giornata per un piccolo modulo utility a una settimana per qualsiasi cosa tocchi pagina prodotto admin o checkout flow.

Due cose su cui insisteremmo dopo averlo fatto tante volte. Non dichiarare compatibilità 9.x sulla speranza. Esegui la matrice completa o marca il modulo come "9.0 tested, 9.1 in progress" finché non lo hai davvero provato su 9.1 con Hummingbird. Abbiamo visto moduli del tipo "dovrebbe andare, non tocca l'admin" chiamare un hook che non esiste più. E non provare a fare tutto insieme. Scegli un modulo, fai la migrazione end-to-end, scrivi cosa hai imparato, e il secondo richiederà metà tempo. Il quinto diventa routine.

Parti dalla documentazione ufficiale sulle modifiche 9.0 e dalle note developer 9.1 per la lista canonica dell'impatto sui moduli. Testa in staging prima di avvicinarti a uno shop cliente. E se usi uno dei nostri moduli e incontri qualcosa non coperto qui, scrivici: probabilmente lo abbiamo già risolto su uno degli altri 147.

Letture correlate

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 codice pulito e risultati misurabili.

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.
Caricamento...
Torna su