Plugin / Interceptor Pattern in Magento 2: Before, After und Around erklärt
· Lesezeit: ca. 15 Minuten · Teil der Serie: Design Patterns in Magento 2
Plugin / Interceptor
Pattern in Magento 2
Before, After, Around – Magento-Methoden erweitern ohne Core-Override. Plugin-Chains, sortOrder, Einschränkungen und die wichtigsten Fallstricke vollständig erklärt.
Das Plugin Pattern: Magentos einzigartiger Erweiterungsmechanismus
Das Plugin Pattern – in Magento 2 auch als Interceptor Pattern bezeichnet – ist wohl das Magento-spezifischste aller Design Patterns. Es löst ein Problem, das in großen Systemen mit vielen gleichzeitig installierten Modulen unvermeidlich entsteht: Wie können mehrere Module dasselbe Verhalten einer Klasse erweitern, ohne sich gegenseitig zu überschreiben?
Die klassische Lösung – eine Klasse durch eine eigene Subklasse ersetzen (Preference / Override) – versagt hier, weil nur eine Klasse gleichzeitig aktiv sein kann. Magento 2 löst dieses Problem mit einem automatisch generierten Proxy-Mechanismus: Für jede Klasse mit Plugins generiert Magento eine Interceptor-Klasse, die alle registrierten Plugins in einer Chain aufruft.
- 1. Wie Plugins intern funktionieren
- 2. Before Plugin: Argumente modifizieren
- 3. After Plugin: Rückgabewert modifizieren
- 4. Around Plugin: Methode umschließen
- 5. Plugin Registrierung in di.xml
- 6. sortOrder: Plugin-Reihenfolge steuern
- 7. Plugin-Chain: mehrere Plugins auf einer Methode
- 8. Einschränkungen: wann Plugins nicht funktionieren
- 9. Praxisbeispiele aus dem Magento-Alltag
- 10. Plugin vs. Preference vs. Observer
- 11. Zusammenfassung
- 12. FAQ
1. Wie Plugins intern funktionieren
Wenn bin/magento setup:di:compile ausgeführt wird, generiert Magento für jede Klasse mit Plugins eine Interceptor-Klasse. Diese erbt von der originalen Klasse und überschreibt alle betroffenen Methoden. Die generierte Klasse liegt unter generated/code/.
Statt der originalen Klasse wird immer die Interceptor-Klasse instanziiert. Die Interceptor-Klasse ruft alle registrierten Before-Plugins, dann die ursprüngliche Methode, dann alle After-Plugins auf – in der konfigurierten Reihenfolge (sortOrder). Around-Plugins umschließen diesen gesamten Aufruf.
<?php
// Simplified generated Interceptor — what Magento generates automatically:
// generated/code/Magento/Catalog/Api/ProductRepositoryInterface/Interceptor.php
class Interceptor extends ProductRepository
{
public function getById(int $productId, bool $editMode = false): ProductInterface
{
// 1. Run all Before-Plugins
$pluginInfo = $this->pluginList->getNext($this->subjectType, 'getById');
if (!$pluginInfo) {
return parent::getById($productId, $editMode);
}
// 2. Call plugin chain (before → original → after)
return $this->___callPlugins('getById', func_get_args(), $pluginInfo);
}
}
2. Before Plugin: Argumente modifizieren
Ein Before Plugin wird vor der originalen Methode ausgeführt. Die Plugin-Methode heißt before + MethodenName (PascalCase). Sie empfängt das Subject (das Original-Objekt) und die Argumente der Original-Methode. Der Rückgabewert ist ein Array mit den (möglicherweise modifizierten) Argumenten.
<?php
declare(strict_types=1);
namespace Mironsoft\Catalog\Plugin;
use Magento\Catalog\Api\ProductRepositoryInterface;
/**
* Before Plugin: Normalizes product SKU to uppercase before any repository lookup.
*/
class NormalizeSkuPlugin
{
/**
* Intercepts ProductRepository::get() before it executes.
*
* @return array modified arguments [string $sku]
*/
public function beforeGet(
ProductRepositoryInterface $subject,
string $sku,
bool $editMode = false,
?int $storeId = null,
bool $forceReload = false
): array {
// Return array of modified arguments — order must match original method signature
return [strtoupper(trim($sku)), $editMode, $storeId, $forceReload];
}
/**
* Intercepts ProductRepository::save() before it executes.
* Example: enforce SKU format before saving.
*
* @return array|null return null to NOT modify arguments
*/
public function beforeSave(
ProductRepositoryInterface $subject,
\Magento\Catalog\Api\Data\ProductInterface $product
): ?array {
if (!$product->getSku()) {
// Auto-generate SKU if missing
$product->setSku('AUTO-' . uniqid());
}
return [$product]; // return modified argument
}
}
Wichtig: Before Plugins müssen ein Array zurückgeben (mit den möglicherweise modifizierten Argumenten) oder null, wenn keine Änderung vorgenommen wird. Die Reihenfolge im Array muss der Original-Methoden-Signatur entsprechen.
3. After Plugin: Rückgabewert modifizieren
Ein After Plugin wird nach der originalen Methode ausgeführt. Die Plugin-Methode heißt after + MethodenName. Sie empfängt das Subject, den Rückgabewert der Originalmethode ($result) und optional die Argumente der Originalmethode (seit Magento 2.2).
<?php
declare(strict_types=1);
namespace Mironsoft\Catalog\Plugin;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Catalog\Api\Data\ProductInterface;
/**
* After Plugin: Enriches product data with additional information after loading.
*/
class EnrichProductDataPlugin
{
public function __construct(
private readonly \Mironsoft\Catalog\Service\StockBadgeService $stockBadgeService
) {}
/**
* Intercepts ProductRepository::getById() after it completes.
*
* @param ProductInterface $result the return value of the original method
* @return ProductInterface potentially modified result
*/
public function afterGetById(
ProductRepositoryInterface $subject,
ProductInterface $result,
int $productId, // Original method arguments available since Magento 2.2
bool $editMode = false
): ProductInterface {
// Add custom data to the returned product object
$hasBadge = $this->stockBadgeService->productHasSaleBadge($result);
$result->setCustomAttribute('has_sale_badge', $hasBadge ? '1' : '0');
return $result;
}
/**
* After Plugin on getList: filter out out-of-stock items for guests.
*/
public function afterGetList(
ProductRepositoryInterface $subject,
\Magento\Catalog\Api\Data\ProductSearchResultsInterface $result
): \Magento\Catalog\Api\Data\ProductSearchResultsInterface {
// Filter items in the search results
$filteredItems = array_filter(
$result->getItems(),
fn(ProductInterface $p) => $p->isSaleable()
);
$result->setItems(array_values($filteredItems));
return $result;
}
}
4. Around Plugin: Methode vollständig umschließen
Ein Around Plugin umschließt die gesamte Methode – inklusive aller anderen Plugins. Es entscheidet selbst, ob und wie die ursprüngliche Methode (oder die nächste Plugin-Stufe) aufgerufen wird. Die Plugin-Methode heißt around + MethodenName und erhält als zweites Argument ein Callable $proceed, das den nächsten Schritt in der Chain repräsentiert.
<?php
declare(strict_types=1);
namespace Mironsoft\Catalog\Plugin;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Catalog\Api\Data\ProductInterface;
use Psr\Log\LoggerInterface;
/**
* Around Plugin: Adds timing/profiling around product repository calls.
* Use Around only when you need to control whether the original executes.
*/
class ProfileProductLoadPlugin
{
public function __construct(
private readonly LoggerInterface $logger
) {}
/**
* Around Plugin for ProductRepository::getById().
*
* @param callable $proceed calls the next plugin or the original method
*/
public function aroundGetById(
ProductRepositoryInterface $subject,
callable $proceed,
int $productId,
bool $editMode = false,
?int $storeId = null,
bool $forceReload = false
): ProductInterface {
$start = microtime(true);
try {
// MUST call $proceed() to execute the original (and other plugins)!
$result = $proceed($productId, $editMode, $storeId, $forceReload);
} catch (\Exception $e) {
// Around plugin can also catch and handle exceptions
$this->logger->error('Product load failed', [
'product_id' => $productId,
'error' => $e->getMessage(),
]);
throw $e; // rethrow — don't swallow exceptions silently
}
$duration = microtime(true) - $start;
if ($duration > 0.1) {
$this->logger->warning('Slow product load', [
'product_id' => $productId,
'duration_ms' => round($duration * 1000, 2),
]);
}
return $result;
}
}
Warnung: Around Plugins, die $proceed() nicht aufrufen, unterbrechen die gesamte Plugin-Chain. Das ist manchmal gewünscht (z.B. Caching, Feature-Flags), aber gefährlich: Andere Module in der Chain werden nicht mehr ausgeführt. Around Plugins sparsam einsetzen – Before und After reichen in den meisten Fällen.
5. Plugin Registrierung in di.xml
<!-- app/code/Mironsoft/Catalog/etc/di.xml -->
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<type name="Magento\Catalog\Api\ProductRepositoryInterface">
<!-- Before Plugin: normalize SKU -->
<plugin name="mironsoft_normalize_sku"
type="Mironsoft\Catalog\Plugin\NormalizeSkuPlugin"
sortOrder="10"/>
<!-- After Plugin: enrich product data -->
<plugin name="mironsoft_enrich_product"
type="Mironsoft\Catalog\Plugin\EnrichProductDataPlugin"
sortOrder="20"/>
<!-- Around Plugin: profiling (disabled in production via config) -->
<plugin name="mironsoft_profile_product_load"
type="Mironsoft\Catalog\Plugin\ProfileProductLoadPlugin"
sortOrder="100"
disabled="true"/>
</type>
<!-- Plugin on a specific scope only (frontend) -->
<!-- In etc/frontend/di.xml: -->
<!-- <type name="..."><plugin name="..." type="..." sortOrder="..."/></type> -->
</config>
6. sortOrder: Plugin-Reihenfolge präzise steuern
Das sortOrder-Attribut bestimmt die Reihenfolge, in der mehrere Plugins auf derselben Methode ausgeführt werden. Kleinere Zahlen werden zuerst ausgeführt. Das ist besonders wichtig, wenn Plugins voneinander abhängen oder wenn ein Plugin eine Änderung rückgängig machen könnte, die ein anderes vorgenommen hat.
7. Plugin-Chain: Mehrere Plugins auf einer Methode
Mehrere Module können Plugins auf dieselbe Methode registrieren. Magento führt sie alle aus – in der Reihenfolge ihrer sortOrder. Kein Modul muss wissen, ob andere Module ebenfalls Plugins registriert haben.
<?php
// Plugin A von Modul 1 (sortOrder=10):
public function beforeSave(ProductRepositoryInterface $subject, ProductInterface $product): array
{
$product->setData('enriched_by_module_a', true);
return [$product];
}
// Plugin B von Modul 2 (sortOrder=20) sieht die Änderung von Plugin A:
public function beforeSave(ProductRepositoryInterface $subject, ProductInterface $product): array
{
if ($product->getData('enriched_by_module_a')) {
$product->setData('double_enriched', true);
}
return [$product];
}
// Reihenfolge der Ausführung:
// 1. Plugin A before (sortOrder=10)
// 2. Plugin B before (sortOrder=20)
// 3. Original save() Methode
// 4. Plugin B after (sortOrder=20) — After-Reihenfolge: niedrigste zuerst
// 5. Plugin A after (sortOrder=10)
8. Einschränkungen: Wann Plugins nicht funktionieren
Plugins haben klare Grenzen. Es ist wichtig, diese zu kennen, um nicht Zeit mit einem nicht funktionierenden Plugin zu verlieren:
- Finale Klassen (
final class): Können nicht von Interceptors beerbt werden → kein Plugin möglich. - Finale Methoden (
final function): Können nicht überschrieben werden → kein Plugin möglich. - Private Methoden: Private Methoden sind nicht Teil der Klassen-API → kein Plugin.
- Static Methoden: Statische Methoden werden nicht intercepted.
- Konstruktoren:
__construct()kann nicht mit Plugins belegt werden. - Direkte ObjectManager-Aufrufe: Klassen, die direkt via ObjectManager instanziiert werden (ohne DI), können keine Plugins haben.
<?php
// Diese Methoden können NICHT mit Plugins belegt werden:
final class UnpluggableClass // final class → kein Plugin
{
public function doSomething(): void {}
}
class SomeClass
{
final public function alsoUnpluggable(): void {} // final method → kein Plugin
private function alsoNotPluggable(): void {} // private → kein Plugin
public static function staticNotPluggable(): void {} // static → kein Plugin
}
// Lösung wenn Plugin nicht funktioniert:
// → Observer/Event verwenden
// → Preference (letzter Ausweg)
// → Klasse refactoren (falls eigener Code)
9. Praxisbeispiele aus dem Magento-Alltag
Beispiel 1: Produkt-URL nach Speichern leeren (Cache-Invalidierung)
<?php
namespace Mironsoft\SeoTools\Plugin;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Framework\App\Cache\TypeListInterface;
class InvalidateProductUrlCachePlugin
{
public function __construct(
private readonly TypeListInterface $cacheTypeList
) {}
/**
* After saving a product, invalidate the URL and FPC cache.
*/
public function afterSave(
ProductRepositoryInterface $subject,
ProductInterface $result
): ProductInterface {
// Invalidate Full Page Cache after product save
$this->cacheTypeList->invalidate(['full_page', 'block_html']);
return $result;
}
}
Beispiel 2: Gastbestellungen verhindern (Feature Flag)
<?php
namespace Mironsoft\B2B\Plugin;
use Magento\Checkout\Model\Type\Onepage;
use Magento\Customer\Model\Session;
class RequireLoginForCheckoutPlugin
{
public function __construct(
private readonly Session $customerSession,
private readonly \Mironsoft\B2B\Helper\Config $config
) {}
/**
* Around Plugin: prevent guest checkout when B2B mode is enabled.
*/
public function aroundGetCheckoutMethod(
Onepage $subject,
callable $proceed
): string {
if ($this->config->isB2BModeEnabled() && !$this->customerSession->isLoggedIn()) {
// Return 'login_in' to redirect to login — do NOT call $proceed()
return Onepage::METHOD_REGISTER;
}
return $proceed();
}
}
10. Plugin vs. Preference vs. Observer – die Entscheidungsmatrix
Mironsoft
Magento 2 Modul-Entwicklung
Magento Core-Override bereinigen?
Bestehende Preferences und Core-Overrides durch saubere Plugins ersetzen – für upgrade-sichere Magento 2 Erweiterungen ohne Konflikte.
11. Zusammenfassung
Das Plugin Pattern ist Magentos leistungsfähigster Erweiterungsmechanismus. Before, After und Around Plugins ermöglichen es, das Verhalten beliebiger öffentlicher Methoden zu erweitern – ohne Core-Dateien zu bearbeiten, ohne andere Module zu blockieren und ohne Upgrade-Risiken.
Plugin Pattern in Magento 2 – Regeln auf einen Blick
Before Plugin
Argumente modifizieren. Gibt Array mit Argumenten zurück (oder null für keine Änderung). Methoden-Name: before + MethodeName.
After Plugin
Rückgabewert modifizieren. Zweites Argument ist $result. Gibt modifizierten Result zurück. Methoden-Name: after + MethodeName.
Around Plugin
Immer $proceed() aufrufen außer bei bewusstem Abbruch. Zweites Argument ist callable $proceed. Sparsam einsetzen.
Einschränkungen
Nicht möglich auf: final class, final method, private method, static method, __construct. Alternative: Observer oder Preference.
12. FAQ: Plugin Pattern in Magento 2
1 Warum funktioniert mein Plugin nicht?
bin/magento setup:di:compile && cache:flush. (2) Methode nicht final/private/static? (3) Methodenname korrekt? (beforeGet, nicht before_get). (4) Plugin nicht als disabled="true"? (5) Modul aktiv? (6) Klasse via DI instanziiert (nicht via new)?2 Around Plugin ohne $proceed – was passiert?
$proceed nicht aufgerufen wird.3 Kann ich Plugins für eigene Klassen erstellen?
4 Was ist der Unterschied zwischen Before, After und Around?
$result und kann ihn ändern. Around: umschließt alles, $proceed() ruft die Chain weiter auf. Faustregel: Before/After reichen für 90% der Fälle.5 Kann ich Plugins auf Interfaces statt Klassen registrieren?
ProductRepositoryInterface greift auf alle Implementierungen. Stabiler als konkreter Klassenname, der sich durch Preferences ändern kann. In di.xml: <type name="Magento\Catalog\Api\ProductRepositoryInterface">.6 Wie teste ich ein Plugin mit PHPUnit?
$result als Mock übergeben. Für Around-Plugins $proceed als Closure mocken und prüfen ob sie aufgerufen wurde. Kein DI-Compile nötig — Plugin-Klasse direkt instanziieren mit gemockten Dependencies.7 Was passiert wenn zwei Module dasselbe Plugin registrieren?
sortOrder bestimmt die Reihenfolge. Plugin-Namen müssen eindeutig sein: gleiche Namen überschreiben sich. Empfehlung: eindeutige Namen mit Modul-Präfix (vendor_module_pluginname).8 Kann ich ein Plugin deaktivieren ohne Code-Änderung?
disabled="true" eintragen. Funktioniert auch für Plugins anderer Module. Scope-spezifisch: in etc/frontend/di.xml nur im Frontend deaktivieren.9 Wie finde ich aktive Plugins auf einer Methode?
bin/magento dev:di:info 'Magento\Catalog\Api\ProductRepositoryInterface' listet alle Plugins. Alternativ: generierte Interceptor-Datei lesen (generated/code/). Oder: grep -r 'plugin name' app/code/ --include="di.xml".10 Gibt es Performance-Unterschiede zwischen den Plugin-Typen?
getById in Schleifen) können spürbar werden.