PrestaShop 9: Was sich unter der Haube geändert hat und wie Sie Ihre Module vorbereiten

PrestaShop 9 ist da — und es bricht mehr als Sie denken

PrestaShop 9.0 wurde Ende 2025 mit einem Symfony 6.4 LTS-Fundament ausgeliefert — ein Sprung von Symfony 4.4, der zwei große Framework-Versionen in einem Release überspringt. Ich habe bisher 14 Produktionsmodule auf PS9-Kompatibilität migriert. Einige liefen reibungslos. Andere erforderten das komplette Umschreiben ganzer Admin-Controller.

PHP-Entwickler programmiert PrestaShop-Modul-Upgrades für Version 9 Kompatibilität

Dies ist keine Changelog-Zusammenfassung. Dies ist der Leitfaden eines praktizierenden Entwicklers zu dem, was tatsächlich bricht, mit Vorher-Nachher-Code für jede größere Änderung. Wenn Sie PrestaShop-Module pflegen — ob für den Addons-Marktplatz, für Kunden oder für Ihre eigenen Shops — ist dies die Migrationsreferenz, die ich mir gewünscht hätte, als ich begann.

Der Symfony-6.4-Sprung: Warum er wichtig ist

Symfony 4.4 erreichte sein End-of-Life im November 2023. PrestaShop 8.x lief bereits auf einer nicht mehr unterstützten Framework-Version. Der Sprung auf 6.4 LTS (mit Sicherheitsfixes bis November 2027 unterstützt) war notwendig, aber schmerzhaft.

Die wichtigsten Implikationen für Modulentwickler:

  • PHP 8.1 Minimum — PHP 7.x-Code wird nicht laufen. PHP 8.2 oder 8.3 empfohlen für die beste Balance aus Funktionen und Stabilität.
  • Controller als echte Services — Das „globaler Container"-Muster ist tot. Dependency Injection ist jetzt Pflicht.
  • Annotations durch PHP-8-Attribute ersetzt@Route, @AdminSecurity, @DemoRestricted werden alle in Attribut-Syntax konvertiert.
  • Mehrere große Bibliotheken entfernt — Guzzle, SwiftMailer und Tactician sind aus dem Core verschwunden.
  • Node.js 20 Minimum — Für Module mit Build-Schritten.

Breaking Change #1: Controller — Vom globalen Container zu Dependency Injection

Dies ist die Änderung, die die meisten Entwickler stolpern lässt. In PS8 konnten Sie $this->get('service_name') in jedem Admin-Controller aufrufen, um jeden Service aus dem globalen Symfony-Container abzurufen. In PS9 ist der an Controller übergebene Container ein begrenzter Container, der nur Services enthält, die Sie explizit deklariert haben.

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

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

Wesentliche Änderungen:

  • FrameworkBundleAdminController ist veraltet — verwenden Sie PrestaShopAdminController
  • Constructor Injection ersetzt $this->get()-Aufrufe
  • Method Injection funktioniert für aktionsspezifische Abhängigkeiten
  • getSubscribedServices() ist eine Migrationsbrücke — verwenden Sie sie vorübergehend beim Refactoring, wechseln Sie aber zu Constructor-/Method-Injection

Breaking Change #2: Annotations zu PHP-8-Attributen

Die sensio/framework-extra-bundle-Abhängigkeit wurde entfernt. Alle Routing- und Sicherheits-Annotations müssen in native PHP-8-Attribute konvertiert werden.

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

Sicherheits-Annotations

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

Beachten Sie die Namespace-Änderung: Security\Annotation\ wird zu Security\Attribute\. Dies ist ein einfaches Suchen-und-Ersetzen, aber wenn Sie es übersehen, haben Ihre Routen keine Sicherheitsbeschränkungen — was schlimmer ist als ein Absturz.

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: Service-Definitionen — YAML zu PHP-Konfiguration

Obwohl YAML-Service-Definitionen in PS9 noch funktionieren, ist der empfohlene Ansatz PHP-basierte Konfiguration mit Autowiring. Wichtiger noch: Ihre Services müssen korrekt für die strengere Dependency Injection von Symfony 6.4 konfiguriert sein.

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

Nachher (PS9 PHP-Konfiguration mit 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%');
};

Mit Autowiring löst Symfony Constructor-Abhängigkeiten automatisch per Type-Hint auf. Explizite Definitionen benötigen Sie nur für:

  • Skalare Parameter ($shopId, $apiKey usw.)
  • Interface-Bindungen (wenn mehrere Implementierungen existieren)
  • Drittanbieter-Klassen, die nicht automatisch verdrahtet werden können

Breaking Change #4: Entfernte Bibliotheken

PrestaShop 9 hat mehrere mitgelieferte Bibliotheken entfernt. Wenn Ihr Modul diese verwendet hat, müssen Sie sie entweder selbst bündeln oder zur Ersatzlösung migrieren.

Entfernte BibliothekErsatzMigrationsaufwand
guzzlehttp/guzzlesymfony/http-clientMittel — API ist anders
swiftmailer/swiftmailersymfony/mailerMittel — neues Email-Objekt-API
league/tactician-bundlesymfony/messengerHoch — anderes Command-Bus-Muster
pear/archive_tarIm Modul bündeln oder ext-zip verwendenGering

Guzzle zu 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 zu 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);
}

Wichtig: SSL-Verschlüsselung für E-Mails wurde in PS9 entfernt. Es bleiben nur TLS oder keine Verschlüsselung. Aktualisieren Sie Ihre E-Mail-Konfiguration, falls Sie SSL verwendet haben.

Breaking Change #5: Context-Refaktorisierung

Der Legacy-Context-Singleton wird durch typisierte Context-Services ersetzt. Obwohl Context::getContext() in PS9 noch funktioniert, ist es veraltet, und der neue Ansatz ist zuverlässiger — insbesondere in CLI-Befehlen und asynchronen Kontexten, in denen der Legacy-Context nicht korrekt initialisiert war.

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

Verfügbare Context-Services in PS9: EmployeeContext, ShopContext, LanguageContext, CurrencyContext, CountryContext, ApiClientContext und LegacyControllerContext.

Breaking Change #6: Authentifizierung und Session-Management

Das Back-Office-Login ist jetzt vollständig Symfony-basiert. Wenn Ihr Modul Mitarbeiterdaten aus Context::$cookie liest, ist das in PS9 unzuverlässig.

// 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: Entfernte Hooks

Mehrere Hooks im Zusammenhang mit dem Admin-Login-Ablauf wurden ohne direkten Ersatz entfernt:

Code-Editor mit dem Symfony-Framework der PrestaShop 9 Architektur

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

Ersatz für Formularanpassungen:

  • actionBackOfficeLoginForm — Den Login-Form-Builder modifizieren
  • actionEmployeeRequestPasswordResetForm — Das Passwort-Zurücksetzen-Formular modifizieren

Alle Legacy-Produktseiten-Hooks (actionAdminProductsController*) wurden ebenfalls entfernt, da die Legacy-Produktseite nicht mehr existiert.

Breaking Change #8: Front-Office-Templatedaten-Änderungen

Mehrere Front-End-Seiten verwenden jetzt Presenter-Klassen, die die in Smarty-Templates verfügbare Datenstruktur ändern. Wenn Ihr Modul diese Templates überschreibt oder erweitert, müssen Sie Variablenreferenzen aktualisieren.

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

Betroffene Seiten: Kategorie, Hersteller, Lieferant und Geschäft. Jede hat einen entsprechenden actionPresent*-Hook, der es Modulen ermöglicht, die präsentierten Daten zu modifizieren.

Breaking Change #9: Modulsicherheit — Keine Root-PHP-Dateien

Module können keine direkt ausführbaren PHP-Dateien mehr auf der Root-Ebene enthalten. Dies betrifft Module, die AJAX-Endpunkte als eigenständige PHP-Dateien verwenden.

// 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: Konsolenbefehle und Multi-Kernel

PS9 führt separate Anwendungskernels für Admin, Admin-API und Front ein. Konsolenbefehle akzeptieren jetzt einen --app-id-Parameter, um den richtigen Kernel anzusprechen:

# 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

Dies ist wichtig für Modul-Konsolenbefehle — stellen Sie sicher, dass Ihr Befehl im richtigen Kernel-Kontext registriert ist.

Was gleich bleibt (Die gute Nachricht)

Nicht alles bricht. Diese Muster funktionieren in PS9 weiterhin:

  • Das Hook-SystemhookDisplayHeader(), hookActionProductUpdate() usw. funktionieren genau wie bisher.
  • Smarty-Templates — Front-Office-Smarty-Templates werden weiterhin unterstützt. Der Twig-Übergang betrifft nur den Admin-Bereich.
  • Modul-Deskriptor$this->name, $this->version, $this->ps_versions_compliancy funktionieren auf die gleiche Weise.
  • Db::getInstance() — Direkte Datenbankabfragen für modulspezifische Tabellen bleiben voll funktionsfähig.
  • ObjectModel für Modultabellen — Ihre benutzerdefinierten ObjectModel-Unterklassen für modulspezifische Tabellen funktionieren weiterhin. Die Deprecation betrifft nur Core-Entity-ObjectModels.
  • Configuration APIConfiguration::get() und Configuration::updateValue() sind unverändert.

Migrations-Checkliste

Hier ist die Checkliste, die ich für jede Modulmigration verwende. Drucken Sie sie aus und kleben Sie sie an Ihren Monitor:

  1. PHP-Version — Testen Sie auf PHP 8.1+. Entfernen Sie alle PHP-7.x-Kompatibilitäts-Workarounds.
  2. Annotations zu Attributen — Suchen-Ersetzen von Security\Annotation\ zu Security\Attribute\. @Route zu #[Route] konvertieren.
  3. Controller-BasisklasseFrameworkBundleAdminController zu PrestaShopAdminController.
  4. $this->get() ersetzen — Zu Constructor-/Method-Injection wechseln.
  5. Entfernte Bibliotheken prüfen — Guzzle, SwiftMailer, Tactician. Bündeln oder ersetzen.
  6. Service-Definitionen — Überprüfen, ob Autowiring funktioniert. Testen mit bin/console debug:container MyService --app-id=admin.
  7. Template-Variablen — Front-Office-Template-Overrides auf geänderte Datenstrukturen testen.
  8. Entfernte Hooks — Prüfen, ob Ihr Modul entfernte Login-/Produkt-Hooks verwendet.
  9. Root-PHP-Dateien — Alle direkten AJAX-Endpunkte zu ModuleFrontController verschieben.
  10. Kompatibilität aktualisierenps_versions_compliancy auf 9.x setzen:
$this->ps_versions_compliancy = [
    'min' => '8.0.0',
    'max' => '9.99.99',
];

Duale Kompatibilität: PS8 und PS9 gleichzeitig unterstützen

Die meisten Modulentwickler müssen während der Übergangszeit beide Versionen unterstützen. Hier ist das Muster, das ich verwende:

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

Für Service-Definitionen können Sie sowohl services.yml (PS8) als auch services.php (PS9) pflegen — PrestaShop lädt die PHP-Konfiguration, wenn vorhanden, und fällt auf YAML zurück.

Das Fazit

PrestaShop 9 ist eine bessere Plattform mit einem modernen Framework-Fundament. Die Migrationskosten sind real — ich schätze 2–8 Stunden pro Modul, je nach Komplexität — aber das Ergebnis ist wartbarerer Code mit korrekter Dependency Injection, Typsicherheit und Zugang zum vollständigen Ökosystem von Symfony 6.4.

Beginnen Sie mit der offiziellen PS9-Änderungsdokumentation für die vollständige Deprecation-Liste und testen Sie gründlich in einer Staging-Umgebung, bevor Sie in die Produktion deployen.

Alle unsere Module bei mypresta.rocks wurden für PS9-Kompatibilität aktualisiert. Wenn Sie Ihre eigenen Module migrieren und auf etwas stoßen, das hier nicht behandelt wird, schreiben Sie uns — die Chancen stehen gut, dass wir es bereits gelöst haben.

David Miller entwickelt PrestaShop-Module seit der PS-1.6-Ära. Er pflegt über 30 Produktionsmodule bei mypresta.rocks.

Verwandte Artikel

  • Was PrestaShop 9 geändert hat und warum Modul-Kompatibilität wichtig ist
  • PrestaShop 1.7 vs 8 vs 9: Welche Version sollten Sie verwenden?
  • Module für sechs PrestaShop-Versionen bauen: Die Herausforderung der Kompatibilität
Diesen Beitrag teilen:
David Miller

David Miller

Über ein Jahrzehnt praktische PrestaShop-Expertise. David entwickelt leistungsstarke E-Commerce-Module mit Fokus auf SEO, Checkout-Optimierung und Shop-Management. Leidenschaft für sauberen Code...

Hat Ihnen dieser Artikel gefallen?

Erhalten Sie unsere neuesten Tipps, Anleitungen und Modul-Updates direkt in Ihr Postfach.

Kommentare (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.
Antworten
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.
Antworten
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.
Antworten
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.
Antworten
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?
Antworten
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.

Einen Kommentar hinterlassen

Lade ...
Nach oben