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.

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,@DemoRestrictedwerden 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:
FrameworkBundleAdminControllerist veraltet — verwenden SiePrestaShopAdminController- 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,$apiKeyusw.) - 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 Bibliothek | Ersatz | Migrationsaufwand |
|---|---|---|
guzzlehttp/guzzle | symfony/http-client | Mittel — API ist anders |
swiftmailer/swiftmailer | symfony/mailer | Mittel — neues Email-Objekt-API |
league/tactician-bundle | symfony/messenger | Hoch — anderes Command-Bus-Muster |
pear/archive_tar | Im Modul bündeln oder ext-zip verwenden | Gering |
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:

actionAdminLoginControllerBeforeactionAdminLoginControllerLoginBefore/actionAdminLoginControllerLoginAfteractionAdminLoginControllerForgotBefore/actionAdminLoginControllerForgotAfteractionAdminLoginControllerResetBefore/actionAdminLoginControllerResetAfter
Ersatz für Formularanpassungen:
actionBackOfficeLoginForm— Den Login-Form-Builder modifizierenactionEmployeeRequestPasswordResetForm— 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-System —
hookDisplayHeader(),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_compliancyfunktionieren 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 API —
Configuration::get()undConfiguration::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:
- PHP-Version — Testen Sie auf PHP 8.1+. Entfernen Sie alle PHP-7.x-Kompatibilitäts-Workarounds.
- Annotations zu Attributen — Suchen-Ersetzen von
Security\Annotation\zuSecurity\Attribute\.@Routezu#[Route]konvertieren. - Controller-Basisklasse —
FrameworkBundleAdminControllerzuPrestaShopAdminController. $this->get()ersetzen — Zu Constructor-/Method-Injection wechseln.- Entfernte Bibliotheken prüfen — Guzzle, SwiftMailer, Tactician. Bündeln oder ersetzen.
- Service-Definitionen — Überprüfen, ob Autowiring funktioniert. Testen mit
bin/console debug:container MyService --app-id=admin. - Template-Variablen — Front-Office-Template-Overrides auf geänderte Datenstrukturen testen.
- Entfernte Hooks — Prüfen, ob Ihr Modul entfernte Login-/Produkt-Hooks verwendet.
- Root-PHP-Dateien — Alle direkten AJAX-Endpunkte zu ModuleFrontController verschieben.
- Kompatibilität aktualisieren —
ps_versions_compliancyauf 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
Kommentare (8)
Einen Kommentar hinterlassen