PrestaShop 9: Co sie zmienilo pod maska i jak przygotowac swoje moduły
PrestaShop 9 jest tutaj — i psuje więcej, niż myślisz
PrestaShop 9.0 został wydany pod koniec 2025 roku z fundamentem Symfony 6.4 LTS — przeskakując z Symfony 4.4, pomijając dwie główne wersje frameworka w jednym wydaniu. Do tej pory zmigrowałem 14 produkcyjnych modułów do kompatybilności z PS9. Niektóre przeszły gładko. Inne wymagały przepisania całych kontrolerów administracyjnych.

To nie jest podsumowanie changelogu. To praktyczny przewodnik programisty po tym, co faktycznie się psuje, z kodem przed i po dla każdej głównej zmiany. Jeśli utrzymujesz moduły PrestaShop — czy to dla marketplace Addons, dla klientów, czy dla własnych sklepów — to jest dokumentacja migracyjna, której brakowało mi, gdy zaczynałem.
Skok na Symfony 6.4: Dlaczego ma to znaczenie
Symfony 4.4 osiągnął koniec wsparcia w listopadzie 2023. PrestaShop 8.x działał już na niewspieranej wersji frameworka. Skok na 6.4 LTS (wspierany do listopada 2027 dla poprawek bezpieczeństwa) był konieczny, ale bolesny.
Kluczowe implikacje dla twórców modułów:
- PHP 8.1 minimum — Kod PHP 7.x nie zadziała. PHP 8.2 lub 8.3 zalecany dla najlepszego balansu funkcji i stabilności.
- Kontrolery jako właściwe serwisy — Wzorzec „globalnego kontenera" jest martwy. Dependency injection jest teraz obowiązkowy.
- Adnotacje zastąpione atrybutami PHP 8 —
@Route,@AdminSecurity,@DemoRestrictedkonwertowane na składnię atrybutów. - Usunięto kilka głównych bibliotek — Guzzle, SwiftMailer i Tactician zostały usunięte z rdzenia.
- Node.js 20 minimum — Dla modułów z krokami budowania.
Zmiana łamiąca #1: Kontrolery — Od globalnego kontenera do Dependency Injection
To jest zmiana, która sprawia najwięcej problemów programistom. W PS8 mogłeś wywołać $this->get('service_name') w dowolnym kontrolerze administracyjnym, aby pobrać dowolny serwis z globalnego kontenera Symfony. W PS9 kontener przekazywany kontrolerom jest ograniczony i zawiera tylko serwisy, które jawnie zadeklarowałeś.
Przed (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(),
]);
}
}
Po (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,
];
}
}
Kluczowe zmiany:
FrameworkBundleAdminControllerjest zdeprecjonowany — użyjPrestaShopAdminController- Constructor injection zastępuje wywołania
$this->get() - Method injection działa dla zależności specyficznych dla akcji
getSubscribedServices()to most migracyjny — używaj go tymczasowo podczas refaktoryzacji, ale docelowo przejdź na constructor/method injection
Zmiana łamiąca #2: Adnotacje na atrybuty PHP 8
Zależność sensio/framework-extra-bundle została usunięta. Wszystkie adnotacje routingu i bezpieczeństwa muszą być skonwertowane na natywne atrybuty 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
Adnotacje bezpieczeństwa
// 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
Zwróć uwagę na zmianę namespace: Security\Annotation\ zmienia się na Security\Attribute\. To proste find-and-replace, ale jeśli to przeoczysz, Twoje trasy nie będą miały żadnych ograniczeń bezpieczeństwa — co jest gorsze niż crash.
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
{
// ...
}
}
Zmiana łamiąca #3: Definicje serwisów — Z YAML na konfigurację PHP
Choć definicje serwisów w YAML nadal działają w PS9, zalecanym podejściem jest konfiguracja w PHP z autowiring. Co ważniejsze, Twoje serwisy muszą być odpowiednio skonfigurowane dla bardziej rygorystycznego dependency injection w Symfony 6.4.
Przed (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 }
Po (konfiguracja PHP PS9 z 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%');
};
Dzięki autowiring Symfony automatycznie rozwiązuje zależności konstruktora na podstawie type-hintów. Jawne definicje potrzebujesz tylko dla:
- Parametrów skalarnych (
$shopId,$apiKey, itp.) - Bindowań interfejsów (gdy istnieje wiele implementacji)
- Klas zewnętrznych, których nie można autowirować
Zmiana łamiąca #4: Usunięte biblioteki
PrestaShop 9 usunął kilka dołączonych bibliotek. Jeśli Twój moduł ich używał, musisz je albo samodzielnie dołączyć, albo zmigrować na zamiennik.
| Usunięta biblioteka | Zamiennik | Nakład migracji |
|---|---|---|
guzzlehttp/guzzle | symfony/http-client | Średni — API jest inne |
swiftmailer/swiftmailer | symfony/mailer | Średni — nowe API obiektu Email |
league/tactician-bundle | symfony/messenger | Wysoki — inny wzorzec command bus |
pear/archive_tar | Dołącz do modułu lub użyj ext-zip | Niski |
Guzzle na 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 na 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);
}
Ważne: Szyfrowanie SSL dla e-mail zostało usunięte w PS9. Pozostaje tylko TLS lub brak szyfrowania. Zaktualizuj konfigurację e-mail, jeśli używałeś SSL.
Zmiana łamiąca #5: Refaktoryzacja Context
Starszy singleton Context jest zastępowany przez typowane serwisy kontekstowe. Choć Context::getContext() nadal działa w PS9, jest zdeprecjonowany, a nowe podejście jest bardziej niezawodne — szczególnie w komendach CLI i kontekstach asynchronicznych, gdzie starszy Context nie był prawidłowo inicjalizowany.
// 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();
}
Dostępne serwisy kontekstowe w PS9: EmployeeContext, ShopContext, LanguageContext, CurrencyContext, CountryContext, ApiClientContext i LegacyControllerContext.
Zmiana łamiąca #6: Uwierzytelnianie i zarządzanie sesjami
Logowanie do back office jest teraz w pełni oparte na Symfony. Jeśli Twój moduł odczytuje dane pracownika z Context::$cookie, jest to niezdywodne w 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,
];
}
Zmiana łamiąca #7: Usunięte hooki
Kilka hooków związanych z procesem logowania administracyjnego zostało usuniętych bez bezpośredniego zamiennika:

actionAdminLoginControllerBeforeactionAdminLoginControllerLoginBefore/actionAdminLoginControllerLoginAfteractionAdminLoginControllerForgotBefore/actionAdminLoginControllerForgotAfteractionAdminLoginControllerResetBefore/actionAdminLoginControllerResetAfter
Zamienniki do personalizacji formularzy:
actionBackOfficeLoginForm— Modyfikacja buildera formularza logowaniaactionEmployeeRequestPasswordResetForm— Modyfikacja formularza resetowania hasła
Wszystkie starsze hooki strony produktu (actionAdminProductsController*) zostały również usunięte, ponieważ starsza strona produktu już nie istnieje.
Zmiana łamiąca #8: Zmiany danych szablonów front office
Kilka stron front-endowych używa teraz klas Presenter, zmieniając strukturę danych dostępną w szablonach Smarty. Jeśli Twój moduł nadpisuje lub rozszerza te szablony, musisz zaktualizować odwołania do zmiennych.
{* 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'}
Dotknięte strony: Category, Manufacturer, Supplier i Store. Każda z nich ma odpowiadający hook actionPresent*, który pozwala modułom modyfikować prezentowane dane.
Zmiana łamiąca #9: Bezpieczeństwo modułów — Koniec z plikami PHP w katalogu głównym
Moduły nie mogą już zawierać bezpośrednio wykonywalnych plików PHP na poziomie katalogu głównego. Dotyczy to modułów, które używają endpointów AJAX jako samodzielnych plików PHP.
// 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
Zmiana łamiąca #10: Komendy konsolowe i Multi-Kernel
PS9 wprowadza oddzielne kernele aplikacji dla admin, admin-api i front. Komendy konsolowe akceptują teraz parametr --app-id do targetowania odpowiedniego kernela:
# 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
Ma to znaczenie dla konsolowych komend modułu — upewnij się, że Twoja komenda jest zarejestrowana w odpowiednim kontekście kernela.
Co pozostaje bez zmian (Dobre wieści)
Nie wszystko się psuje. Te wzorce nadal działają w PS9:
- System hooków —
hookDisplayHeader(),hookActionProductUpdate()itp. działają dokładnie tak jak wcześniej. - Szablony Smarty — Szablony Smarty front office pozostają wspierane. Przejście na Twig dotyczy tylko panelu administracyjnego.
- Deskryptor modułu —
$this->name,$this->version,$this->ps_versions_compliancydziałają tak samo. Db::getInstance()— Bezpośrednie zapytania bazodanowe dla tabel specyficznych dla modułu pozostają w pełni funkcjonalne.- ObjectModel dla tabel modułów — Twoje niestandardowe podklasy ObjectModel dla tabel specyficznych dla modułu nadal działają. Deprecjacja dotyczy tylko ObjectModeli rdzeniowych encji.
- API Configuration —
Configuration::get()iConfiguration::updateValue()pozostają bez zmian.
Checklista migracji
Oto checklista, której używam przy każdej migracji modułu. Wydrukuj ją, przyklej do monitora:
- Wersja PHP — Testuj na PHP 8.1+. Usuń wszelkie hacki kompatybilności z PHP 7.x.
- Adnotacje na atrybuty — Find-replace
Security\Annotation\naSecurity\Attribute\. Konwertuj@Routena#[Route]. - Klasa bazowa kontrolera —
FrameworkBundleAdminControllernaPrestaShopAdminController. - Zastąp
$this->get()— Przejdź na constructor/method injection. - Sprawdź usunięte biblioteki — Guzzle, SwiftMailer, Tactician. Dołącz lub zamień.
- Definicje serwisów — Zweryfikuj, czy autowiring działa. Testuj z
bin/console debug:container MyService --app-id=admin. - Zmienne szablonów — Testuj nadpisania szablonów front-office pod kątem zmienionych struktur danych.
- Usunięte hooki — Sprawdź, czy Twój moduł używa jakichkolwiek usuniętych hooków logowania/produktu.
- Pliki PHP w katalogu głównym — Przenieś wszelkie bezpośrednie endpointy AJAX do ModuleFrontController.
- Zaktualizuj compliancy — Ustaw
ps_versions_compliancytak, aby obejmował 9.x:
$this->ps_versions_compliancy = [
'min' => '8.0.0',
'max' => '9.99.99',
];
Podwójna kompatybilność: Wsparcie PS8 i PS9
Większość twórców modułów musi wspierać obie wersje podczas przejścia. Oto wzorzec, którego używam:
// 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
}
Dla definicji serwisów możesz utrzymywać zarówno services.yml (PS8), jak i services.php (PS9) — PrestaShop ładuje konfigurację PHP, jeśli jest dostępna, z fallbackiem na YAML.
Podsumowanie
PrestaShop 9 to lepsza platforma z nowoczesnym fundamentem frameworka. Koszt migracji jest realny — szacuję 2-8 godzin na moduł w zależności od złożoności — ale rezultatem jest bardziej utrzymywalny kod z prawidłowym dependency injection, type safety i dostępem do pełnego ekosystemu Symfony 6.4.
Zacznij od oficjalnej dokumentacji zmian PS9 dla pełnej listy deprecjacji i testuj dokładnie na środowisku staging przed wdrożeniem na produkcję.
Wszystkie nasze moduły na mypresta.rocks zostały zaktualizowane do kompatybilności z PS9. Jeśli migrujesz własne moduły i napotkasz coś, czego tu nie omówiono, napisz do nas — jest duża szansa, że już to rozwiązaliśmy.
David Miller buduje moduły PrestaShop od ery PS 1.6. Utrzymuje ponad 30 produkcyjnych modułów na mypresta.rocks.
Powiązane Artykuły
- Co zmienił PrestaShop 9 i dlaczego kompatybilność modułów ma znaczenie
- PrestaShop 1.7 vs 8 vs 9: którą wersję powinieneś używać?
- Tworzenie modułów dla sześciu wersji PrestaShop: wyzwanie kompatybilności
Komentarze (8)
Zostaw komentarz