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.

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,@DemoRestrictedsi 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 — usaPrestaShopAdminController- 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 rimossa | Sostituzione | Sforzo di migrazione |
|---|---|---|
guzzlehttp/guzzle | symfony/http-client | Medio — API diversa |
swiftmailer/swiftmailer | symfony/mailer | Medio — nuova API oggetto Email |
league/tactician-bundle | symfony/messenger | Alto — pattern command bus diverso |
pear/archive_tar | Includi nel modulo o usa ext-zip | Basso |
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:

actionAdminLoginControllerBeforeactionAdminLoginControllerLoginBefore/actionAdminLoginControllerLoginAfteractionAdminLoginControllerForgotBefore/actionAdminLoginControllerForgotAfteractionAdminLoginControllerResetBefore/actionAdminLoginControllerResetAfter
Sostituzioni per la personalizzazione dei form:
actionBackOfficeLoginForm— Modifica il form builder di loginactionEmployeeRequestPasswordResetForm— 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 hook —
hookDisplayHeader(),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_compliancyfunzionano 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 Configuration —
Configuration::get()eConfiguration::updateValue()sono invariati.
Checklist di migrazione
Ecco la checklist che uso per ogni migrazione di modulo. Stampala, attaccala al monitor:
- Versione PHP — Testa su PHP 8.1+. Rimuovi qualsiasi hack di compatibilità PHP 7.x.
- Da annotation ad attributi — Cerca-e-sostituisci
Security\Annotation\conSecurity\Attribute\. Converti@Routein#[Route]. - Classe base del controller —
FrameworkBundleAdminControllerdiventaPrestaShopAdminController. - Sostituisci
$this->get()— Passa alla constructor/method injection. - Controlla le librerie rimosse — Guzzle, SwiftMailer, Tactician. Includi nel modulo o sostituisci.
- Definizioni dei servizi — Verifica che l'autowiring funzioni. Testa con
bin/console debug:container MyService --app-id=admin. - Variabili dei template — Testa gli override dei template front-office per le strutture dati modificate.
- Hook rimossi — Controlla se il tuo modulo usa hook di login/prodotto rimossi.
- File PHP nella root — Sposta qualsiasi endpoint AJAX diretto nei ModuleFrontController.
- Aggiorna la compatibilità — Imposta
ps_versions_compliancyper 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à
Commenti (8)
Lascia un commento