Plugins (Interceptors) im Detail: before/after/around
Plugins (Interceptors) im Detail: before/after/around
~8 Min. Lesezeit Zuletzt aktualisiert am 9. August 2026
Kapitel 37 hat die Grenze gezogen: Observer können reagieren, aber nie den Rückgabewert einer Methode verändern. Block 5 macht genau dort weiter - mit Plugins (offiziell: Interceptors), Magentos Mechanismus, um in eine bestehende Methode aktiv einzugreifen, ohne ihren Quellcode zu berühren. Dieses Kapitel klärt die Mechanik im Detail, bevor Kapitel 39 den ersten produktiven Plugin dieser Serie schreibt.
Was passiert technisch bei einem Plugin?
Registriert di.xml einen Plugin für eine Klasse oder ein Interface, generiert Magento beim ersten Aufruf (oder vorab per bin/magento setup:di:compile) eine Interceptor-Klasse unter generated/code/. Diese Interceptor-Klasse extends die Zielklasse und überschreibt genau die geplugte Methode - die überschriebene Version ruft intern ___callPlugins() auf, das alle registrierten before-, around- und after-Methoden in der durch sortOrder festgelegten Reihenfolge abarbeitet (Kapitel 43 vertieft die genaue Ausführungsreihenfolge).
Weil ein Plugin technisch eine Subklassen-Überschreibung ist, gelten dieselben Regeln wie für jede normale PHP-Vererbung:
- Die Zielmethode muss
publicsein -privateundprotectedMethoden lassen sich in PHP grundsätzlich nicht überschreiben. - Die Zielmethode darf nicht
finalsein - einefinal-Methode zu überschreiben ist ein PHP-Fatal-Error, nicht nur eine Magento-Einschränkung (Kapitel 40/41 zeigen den echten Umgang mit diesem Fall). - Die Zielmethode darf nicht
staticsein - statische Aufrufe laufen nicht über die vom DI-Container erzeugte Instanz und damit nie durch den Interceptor. - Die Zielklasse muss über den DI-Container (Constructor Injection oder
ObjectManager::create()/get()) erzeugt werden - ein hartkodiertesnew Klasse()beim Aufrufer umgeht den Interceptor komplett.
before-Plugins: Argumente abfangen und verändern
Eine before-Methode heißt before<Methodenname>, bekommt als ersten Parameter $subject (die Originalinstanz) sowie danach dieselben Parameter wie die Zielmethode, und gibt entweder null (Argumente unverändert lassen) oder ein Array mit den - möglicherweise veränderten - Argumenten in Original-Reihenfolge zurück. Als durchgängiges Beispiel für dieses Kapitel dient PointsLedgerRepositoryInterface::getListByCustomerId() aus Kapitel 6.
<?php
declare(strict_types=1);
namespace Mironsoft\Loyalty\Plugin\PointsLedger;
use Magento\Framework\Exception\LocalizedException;
use Mironsoft\Loyalty\Api\PointsLedgerRepositoryInterface;
/**
* Rejects clearly invalid customer IDs before the repository even queries the
* database - a cheap, in-memory guard applied to every caller of this method.
*/
class ValidateCustomerIdBeforeGetListPlugin
{
/**
* Validates the customer ID argument before getListByCustomerId() runs.
*
* @param PointsLedgerRepositoryInterface $subject The intercepted repository instance.
* @param int $customerId Customer entity ID as passed by the caller.
* @return array{0: int}|null
* @throws LocalizedException
*/
public function beforeGetListByCustomerId(
PointsLedgerRepositoryInterface $subject,
int $customerId
): ?array {
if ($customerId <= 0) {
throw new LocalizedException(__('Customer ID must be a positive integer.'));
}
return null;
}
}return null; heißt hier ausdrücklich "Argumente unverändert lassen" - ein before-Plugin muss die Argumente nicht zwingend anfassen, um sinnvoll zu sein; reine Validierung wie hier ist ein legitimer Einsatzzweck.
after-Plugins: Rückgabewerte verändern
Eine after-Methode heißt after<Methodenname>, bekommt als ersten Parameter $subject, als zweiten den bereits berechneten Rückgabewert der Originalmethode ($result), danach optional dieselben Argumente wie die Zielmethode, und muss den - eventuell veränderten - Rückgabewert selbst zurückgeben.
<?php
declare(strict_types=1);
namespace Mironsoft\Loyalty\Plugin\PointsLedger;
use Mironsoft\Loyalty\Api\PointsLedgerRepositoryInterface;
/**
* Caps the number of ledger entries returned to any caller as a defensive
* safety net - protects memory if a customer accumulates an unusually long
* history and a future caller forgets to paginate.
*/
class LimitLedgerListResultPlugin
{
private const MAX_ENTRIES = 500;
/**
* Truncates the ledger list to MAX_ENTRIES most recent entries.
*
* @param PointsLedgerRepositoryInterface $subject The intercepted repository instance.
* @param \Mironsoft\Loyalty\Api\Data\PointsLedgerInterface[] $result Entries returned by the original method.
* @param int $customerId Customer entity ID as passed by the caller.
* @return \Mironsoft\Loyalty\Api\Data\PointsLedgerInterface[]
*/
public function afterGetListByCustomerId(
PointsLedgerRepositoryInterface $subject,
array $result,
int $customerId
): array {
return \array_slice($result, 0, self::MAX_ENTRIES);
}
}Der dritte Parameter ($customerId) ist hier ungenutzt, aber trotzdem Pflicht in der Signatur - after-Plugins bekommen immer auch die Original-Argumente mitgereicht, selbst wenn sie sie nicht brauchen.
around-Plugins: die Ausführung komplett kontrollieren
Eine around-Methode heißt around<Methodenname>, bekommt als zweiten Parameter ein callable $proceed - einen Aufruf in die nächste Schicht der Plugin-Kette bzw. am Ende in die Originalmethode - und entscheidet selbst, ob, wann und wie oft $proceed() aufgerufen wird. Das macht around zum mächtigsten, aber auch teuersten der drei Typen.
<?php
declare(strict_types=1);
namespace Mironsoft\Loyalty\Plugin\PointsLedger;
use Mironsoft\Loyalty\Api\PointsLedgerRepositoryInterface;
/**
* Caches the ledger list per customer ID for the duration of the current
* request - the account page (chapter 48) and the checkout totals plugin
* (chapter 39) can both ask for the same customer's history without
* triggering two identical database queries.
*/
class CacheLedgerListPlugin
{
/**
* @var array<int, \Mironsoft\Loyalty\Api\Data\PointsLedgerInterface[]>
*/
private array $requestCache = [];
/**
* Serves cached results within the same request, delegates to the original
* method on a cache miss and stores the result for subsequent calls.
*
* @param PointsLedgerRepositoryInterface $subject The intercepted repository instance.
* @param callable $proceed Invokes the next plugin layer or the original method.
* @param int $customerId Customer entity ID as passed by the caller.
* @return \Mironsoft\Loyalty\Api\Data\PointsLedgerInterface[]
*/
public function aroundGetListByCustomerId(
PointsLedgerRepositoryInterface $subject,
callable $proceed,
int $customerId
): array {
if (!isset($this->requestCache[$customerId])) {
$this->requestCache[$customerId] = $proceed($customerId);
}
return $this->requestCache[$customerId];
}
}Verdrahtung in 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="Mironsoft\Loyalty\Api\PointsLedgerRepositoryInterface">
<plugin name="mironsoft_loyalty_validate_customer_id_before_get_list"
type="Mironsoft\Loyalty\Plugin\PointsLedger\ValidateCustomerIdBeforeGetListPlugin"
sortOrder="10"/>
<plugin name="mironsoft_loyalty_cache_ledger_list"
type="Mironsoft\Loyalty\Plugin\PointsLedger\CacheLedgerListPlugin"
sortOrder="20"/>
<plugin name="mironsoft_loyalty_limit_ledger_list_result"
type="Mironsoft\Loyalty\Plugin\PointsLedger\LimitLedgerListResultPlugin"
sortOrder="30"/>
</type>
</config>Tipp: Der name-Wert eines <plugin>-Eintrags muss projektweit eindeutig sein - ansonsten überschreibt eine spätere Deklaration mit identischem Namen die frühere stillschweigend. Die Konvention mironsoft_loyalty_<verb> aus Kapitel 30/31 (Observer-Namen) wird hier eins zu eins auf Plugin-Namen übertragen.
Achtung: around-Plugins sind spürbar teurer als before/after: jede Ebene fügt einen zusätzlichen Funktionsaufruf hinzu, und $proceed() muss in aller Regel genau einmal aufgerufen werden - wird es vergessen, läuft die Originalmethode nie; wird es mehrfach aufgerufen, läuft sie mehrfach. Die Faustregel: reicht before oder after aus, ist around die falsche Wahl. Kapitel 39 nutzt deshalb bewusst after, obwohl auf den ersten Blick auch around denkbar wäre.
Mit der Mechanik im Werkzeugkasten setzt Kapitel 39 den ersten echten Produktions-Plugin dieser Serie um: einen Punkte-Rabatt, der im Checkout tatsächlich in die Bestellsumme einfließt.