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.

Programista PHP kodujący aktualizacje modułów PrestaShop dla kompatybilności z wersją 9

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

  • FrameworkBundleAdminController jest zdeprecjonowany — użyj PrestaShopAdminController
  • 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 bibliotekaZamiennikNakład migracji
guzzlehttp/guzzlesymfony/http-clientŚredni — API jest inne
swiftmailer/swiftmailersymfony/mailerŚredni — nowe API obiektu Email
league/tactician-bundlesymfony/messengerWysoki — inny wzorzec command bus
pear/archive_tarDołącz do modułu lub użyj ext-zipNiski

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:

Edytor kodu pokazujący framework Symfony używany w architekturze PrestaShop 9

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

Zamienniki do personalizacji formularzy:

  • actionBackOfficeLoginForm — Modyfikacja buildera formularza logowania
  • actionEmployeeRequestPasswordResetForm — 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ówhookDisplayHeader(), 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_compliancy dział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 ConfigurationConfiguration::get() i Configuration::updateValue() pozostają bez zmian.

Checklista migracji

Oto checklista, której używam przy każdej migracji modułu. Wydrukuj ją, przyklej do monitora:

  1. Wersja PHP — Testuj na PHP 8.1+. Usuń wszelkie hacki kompatybilności z PHP 7.x.
  2. Adnotacje na atrybuty — Find-replace Security\Annotation\ na Security\Attribute\. Konwertuj @Route na #[Route].
  3. Klasa bazowa kontroleraFrameworkBundleAdminController na PrestaShopAdminController.
  4. Zastąp $this->get() — Przejdź na constructor/method injection.
  5. Sprawdź usunięte biblioteki — Guzzle, SwiftMailer, Tactician. Dołącz lub zamień.
  6. Definicje serwisów — Zweryfikuj, czy autowiring działa. Testuj z bin/console debug:container MyService --app-id=admin.
  7. Zmienne szablonów — Testuj nadpisania szablonów front-office pod kątem zmienionych struktur danych.
  8. Usunięte hooki — Sprawdź, czy Twój moduł używa jakichkolwiek usuniętych hooków logowania/produktu.
  9. Pliki PHP w katalogu głównym — Przenieś wszelkie bezpośrednie endpointy AJAX do ModuleFrontController.
  10. Zaktualizuj compliancy — Ustaw ps_versions_compliancy tak, 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
Udostępnij ten wpis:
David Miller

David Miller

Ponad dekada praktycznego doświadczenia z PrestaShop. David tworzy wydajne moduły e-commerce skupione na SEO, optymalizacji zamówień i zarządzaniu sklepem. Pasjonat czystego kodu i mierzalnych...

Spodobał Ci się ten artykuł?

Otrzymuj nasze najnowsze porady, przewodniki i aktualizacje modułów prosto na swoją skrzynkę.

Komentarze (8)

T
Thomas Mueller 2026-02-14
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.
Odpowiedz
E
Elena Rodriguez 2026-02-14
Does anyone know if the hook system changes affect actionProductUpdate? We rely heavily on that hook for our inventory sync module.
Odpowiedz
D
David Miller 2026-02-14
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 2026-02-14
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.
Odpowiedz
S
Sophie Laurent 2026-02-14
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 2026-02-14
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.
Odpowiedz
M
Marcus Weber 2026-02-14
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?
Odpowiedz
D
David Miller 2026-02-14
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.

Zostaw komentarz

Ładowanie...
Do góry