Preference oder Plugin: Entscheidungsregel für Magento 2
AI generated
M2
di.xml
Magento 2 · Dependency Injection · Architektur · Code Review
Preference oder Plugin: eine klare Entscheidungsregel für die Praxis
Checkliste statt Theorie, für Versand, Quote Totals und Order Email

Wer bei jeder neuen Anforderung neu diskutiert, ob eine Preference oder ein Plugin die richtige Wahl ist, verliert Zeit im Code Review und riskiert Konflikte mit Fremdmodulen. Diese Entscheidungsregel liefert eine feste Checkliste mit Scoring, angewendet auf vier reale Magento-2.4.8-Szenarien, die in der Agentur-Praxis regelmäßig vorkommen.

18 Min. Lesezeit di.xml · Plugin sortOrder · dev:di:info · PHP 8.4 Magento 2.4.8

1. Warum eine Entscheidungsregel nötig ist, nicht noch mehr Theorie

Die Grundlagen sind in unseren früheren Artikeln zu Preferences und Plugins ausführlich erklärt: was ein Interceptor technisch macht, wie das Proxy-Objekt entsteht, welche Methodenpräfixe before, around und after unterscheiden. Diese Theorie wird hier bewusst nicht wiederholt. Das eigentliche Problem in der Agentur-Praxis liegt woanders: Ein Entwickler steht vor einer konkreten Anforderung, zum Beispiel einer angepassten Versandkostenberechnung, und muss innerhalb weniger Minuten entscheiden, ob er eine Preference oder ein Plugin schreibt, ohne dass diese Entscheidung im Code Review wieder aufgerollt wird.

Genau hier setzt die Entscheidungsregel in diesem Artikel an. Statt erneut zu erklären, was ein Plugin ist, liefert dieser Text ein Ablaufschema mit vier harten Kriterien, das in Sekunden beantwortet werden kann: Ist die Zielklasse final oder laut Backward-Compatibility-Policy nicht erweiterbar? Muss die Methodensignatur geändert werden oder reicht es, Verhalten drumherum zu hängen? Existiert bereits ein konkurrierendes Plugin eines anderen Vendors auf derselben Methode? Ist das Ziel ein Resource Model oder eine Collection, wo Plugins besonders fehleranfällig sind? Wer diese vier Fragen beantwortet hat, hat die Entscheidung Preference oder Plugin in der Praxis bereits getroffen, ohne eine einzige Zeile Code geschrieben zu haben.

2. Kriterium 1: final-Klassen und Backward-Compatibility-Grenzen

Das erste und härteste Kriterium jeder Entscheidungsregel ist technisch bindend: Ist die Zielklasse als final deklariert, kann kein Preference sie per preference-Knoten in di.xml ersetzen und gleichzeitig erben, weil PHP das Erben von final-Klassen strikt verbietet. In diesem Fall bleibt nur ein Plugin, sofern die Methode nicht ebenfalls final ist, oder eine vollständige Neuimplementierung samt eigenem Interface, was in den allermeisten Fällen unverhältnismäßig aufwendig ist. Magento markiert zunehmend Klassen als final, insbesondere in neueren Modulen und im Checkout-Bereich, gemäß der offiziellen Backward-Compatibility-Policy, die zwischen "internal" und "api" Code unterscheidet.

Weniger offensichtlich, aber genauso wichtig: Auch bei nicht-finalen Klassen kennzeichnet Magento über die @api-Annotation und die Verzeichnisstruktur, welche Klassen als stabile Erweiterungspunkte gedacht sind. Eine Preference auf eine interne Model-Klasse ohne @api-Kennzeichnung funktioniert technisch, bricht aber bei jedem Minor-Update ohne Vorwarnung, weil Magento diese Klassen ohne Rücksicht auf Abwärtskompatibilität refactort. Die praktische Konsequenz für die Checkliste: Vor jeder Preference-Entscheidung prüft man den Quellcode auf final class und auf das Vorhandensein einer @api-Annotation im Klassen-Docblock oder in der zugehörigen api.xml-Deklaration des Moduls.

3. Kriterium 2: Methodensignatur ändern oder Verhalten umschließen

Das zweite Kriterium der Entscheidungsregel betrifft die Art der gewünschten Änderung. Muss der Rückgabetyp einer Methode geändert werden, muss ein neuer Pflichtparameter eingeführt werden, oder muss die interne Konstruktionslogik eines Objekts komplett ausgetauscht werden, etwa weil ein anderer Collection-Typ oder ein anderes Repository injiziert werden soll, dann ist eine Preference fast immer die richtige Wahl. Ein Plugin kann keine neue Signatur einführen, die von anderem Code im System nicht erwartet wird, weil alle Aufrufer weiterhin gegen das ursprüngliche Interface programmieren. Muss dagegen nur zusätzliches Verhalten vor, nach oder um einen bestehenden Aufruf gelegt werden, etwa eine zusätzliche Validierung vor der Ausführung, eine Log-Zeile nach dem Ergebnis, oder eine bedingte Anpassung des Rückgabewerts, ist ein Plugin fast immer vorzuziehen, weil es additiv bleibt und mit anderen Modulen koexistieren kann.

Ein Praxisbeispiel für diese Grenze: Soll die komplette Berechnungslogik einer Preisklasse ausgetauscht werden, weil ein völlig neues Preismodell mit eigenen Datenquellen benötigt wird, ist das eine Preference, weil im Kern eine neue Implementierung entsteht. Soll dagegen nur ein Rabatt-Aufschlag auf das bereits berechnete Ergebnis angewendet werden, ist ein after-Plugin die richtige Entscheidungsregel, weil die ursprüngliche Berechnung erhalten bleibt und nur das Ergebnis modifiziert wird. Wer diesen Unterschied nicht sauber trennt, landet häufig bei Preferences, die den kompletten Originalcode kopieren und nur eine Zeile ändern, was bei jedem Magento-Update zu stillen Merge-Konflikten führt.

4. Kriterium 3: Kompatibilität mit bereits bestehenden Plugins

Das dritte Kriterium wird in vielen Projekten unterschätzt: Existiert bereits ein Plugin eines Drittanbieter-Moduls, eines anderen Vendors im Dual-Vendor-Setup oder eines Marketplace-Extensions auf derselben Methode, verschiebt sich die Entscheidungsregel fast automatisch in Richtung Plugin, weil zwei Plugins auf derselben Methode koexistieren können, solange die sortOrder-Werte sauber gepflegt sind. Eine Preference dagegen ist exklusiv: Nur eine einzige Preference kann pro Typ aktiv sein. Installiert ein zweites Modul ebenfalls eine Preference auf dieselbe Klasse, gewinnt die zuletzt geladene, und die andere wird stillschweigend ignoriert, ohne dass Magento einen Fehler wirft.

In der Agentur-Praxis bedeutet das: Vor jeder Preference-Entscheidung auf eine Core-Klasse, die potenziell von Drittanbieter-Extensions ebenfalls angefasst wird, etwa Versandmodule, Zahlungsmodule oder Steuerberechnung, prüft man mit bin/magento dev:di:info, ob bereits eine Preference existiert. Findet sich dort bereits ein Eintrag eines Fremdmoduls, ist eine eigene Preference in den meisten Fällen keine Option mehr, außer man ersetzt bewusst die Fremd-Preference durch Vererbung von deren Klasse, was eine explizite Abhängigkeit im module.xml nach sich zieht. Ein Plugin dagegen reiht sich einfach mit passendem sortOrder in die bestehende Kette ein.

5. Kriterium 4: Resource Models und Collections als Risikozone

Das vierte Kriterium betrifft eine Zielklassen-Kategorie, bei der Plugins besonders fehleranfällig sind: Resource Models und Collections. Methoden wie Magento\Framework\Model\ResourceModel\Db\AbstractDb::save() oder Collection-Methoden wie addFieldToFilter() und load() werden intern extrem häufig aufgerufen, teils rekursiv, teils in Schleifen über tausende Datensätze. Ein around-Plugin auf einer solchen Methode addiert bei jedem Aufruf zusätzlichen Overhead durch die Proxy-Generierung und kann bei falscher Implementierung, etwa durch fehlendes Aufrufen von proceed(), den kompletten Speichervorgang stillschweigend verhindern.

Die praktische Konsequenz für die Entscheidungsregel: Bei Resource Models und Collections bevorzugt man, wo möglich, Events (catalog_product_save_before, sales_order_save_after und ähnliche) gegenüber Plugins, weil Events keine Proxy-Klasse erzeugen und keinen Performance-Overhead bei jedem einzelnen Methodenaufruf verursachen. Ist ein Plugin dennoch notwendig, etwa weil kein passendes Event existiert, dann ausschließlich als before oder after, niemals als around, außer die Anforderung verlangt zwingend das Abfangen des Rückgabewerts oder das bedingte Verhindern des ursprünglichen Aufrufs. Eine Preference auf ein Resource Model ist in der Regel die schlechteste Wahl aller vier Optionen, weil sie exklusiv ist und mit hoher Wahrscheinlichkeit mit Enterprise-Modulen wie Multi-Source-Inventory kollidiert, die selbst tief in diese Klassen eingreifen.

6. Szenario A: Custom-Versandkostenberechnung

Ein Agentur-Kunde benötigt eine Versandkostenberechnung, die zusätzlich zur Standard-Tabellenrate einen Aufschlag für Sperrgut-Artikel berechnet, basierend auf einem Custom-Attribut am Produkt. Nach der Checkliste: Die Zielklasse Magento\OfflineShipping\Model\Carrier\Tablerate ist nicht final, es wird keine neue Methodensignatur benötigt, es ist unwahrscheinlich, dass ein anderes Modul dieselbe Carrier-Klasse pluggt, und es handelt sich nicht um ein Resource Model. Die entscheidende Frage bleibt Kriterium 2: Wird nur ein Aufschlag auf das bereits berechnete Ergebnis addiert, oder wird die komplette Berechnungslogik ersetzt?

Da hier lediglich ein zusätzlicher Betrag auf das Ergebnis der Methode collectRates() aufgeschlagen werden soll, während die ursprüngliche Tabellenraten-Logik vollständig erhalten bleibt, fällt die Entscheidungsregel eindeutig auf ein after-Plugin. Ein Plugin bleibt additiv, kann parallel zu anderen Versandmodulen bestehen, die möglicherweise ebenfalls die Tablerate-Klasse erweitern, und erzeugt bei einem Magento-Update kein Merge-Risiko, weil kein Originalcode kopiert wurde. Würde stattdessen ein komplett neues Tarifmodell mit externer API-Anbindung benötigt, das die komplette Methode ersetzt und eine andere interne Datenstruktur zurückgibt, wäre eine Preference oder besser eine eigenständige neue Carrier-Klasse die richtige Wahl, keine Preference auf die bestehende Tablerate-Klasse.

Kriterium Spricht für Preference Spricht für Plugin
Klasse final oder ohne @api Nicht möglich bei final, riskant ohne @api Meist einzige Option bei final-Klassen
Methodensignatur ändern Erforderlich bei neuem Parameter/Rückgabetyp Technisch nicht möglich
Fremdmodul-Plugin existiert bereits Exklusiv, überschreibt Fremdmodul komplett Koexistiert über sortOrder
Resource Model / Collection Hohes Kollisionsrisiko mit MSI/Enterprise Nur before/after, around vermeiden
Nur Ergebnis anreichern (additiv) Unnötiger Aufwand, kopiert Originalcode Ideal, minimaler Eingriff
Objektkonstruktion vollständig ersetzen Einzige saubere Lösung Nicht vorgesehen für Konstruktorlogik
Update-Sicherheit bei Core-Refactoring Bricht bei geänderter interner Struktur Bricht nur bei geänderter Methodensignatur

<!-- app/code/Mironsoft/Shipping/etc/di.xml -->
<!-- Scenario A: Plugin approach for surcharge on top of Tablerate result -->
<?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\OfflineShipping\Model\Carrier\Tablerate">
        <plugin name="mironsoft_bulky_item_surcharge"
                type="Mironsoft\Shipping\Plugin\BulkyItemSurchargePlugin"
                sortOrder="20"/>
    </type>
</config>

<?php
// app/code/Mironsoft/Shipping/Plugin/BulkyItemSurchargePlugin.php
// Scenario A: after-plugin, adds a surcharge to the calculated shipping result
declare(strict_types=1);

namespace Mironsoft\Shipping\Plugin;

use Magento\OfflineShipping\Model\Carrier\Tablerate;
use Magento\Quote\Model\Quote\Address\RateRequest;
use Magento\Shipping\Model\Rate\Result;

/**
 * Adds a bulky item surcharge to the tablerate shipping result.
 * Uses after-plugin because only the already computed result is enriched,
 * the original tablerate calculation stays untouched.
 */
final class BulkyItemSurchargePlugin
{
    /**
     * Surcharge amount per bulky item in the quote, in store currency.
     */
    private const BULKY_SURCHARGE = 12.50;

    /**
     * @param Tablerate $subject Original carrier instance.
     * @param Result|bool $result Result of collectRates(), or false if no rate found.
     * @param RateRequest $request Original rate request with quote items.
     * @return Result|bool Modified result with surcharge applied, or unchanged bool.
     */
    public function afterCollectRates(
        Tablerate $subject,
        Result|bool $result,
        RateRequest $request
    ): Result|bool {
        if ($result === false) {
            return $result;
        }

        $hasBulkyItem = false;
        foreach ($request->getAllItems() ?? [] as $item) {
            if ((bool) $item->getProduct()->getData('is_bulky_item')) {
                $hasBulkyItem = true;
                break;
            }
        }

        if ($hasBulkyItem) {
            foreach ($result->getAllRates() as $rate) {
                $rate->setPrice($rate->getPrice() + self::BULKY_SURCHARGE);
            }
        }

        return $result;
    }
}

Zum Vergleich der Gegenfall im selben Szenario: Müsste die komplette Tarifberechnung durch eine externe Fracht-API ersetzt werden, inklusive eigener interner Datenstruktur für Zonen und Gewichtsklassen, wäre die Entscheidungsregel eine andere. Hier reicht kein additives Plugin mehr, weil die gesamte Berechnungslogik von collectRates() ausgetauscht werden müsste, nicht nur das Ergebnis angereichert. In diesem Fall wäre eine Preference auf die Carrier-Klasse die richtige Wahl, wie das folgende Gegenbeispiel zeigt.


<!-- app/code/Mironsoft/Shipping/etc/di.xml -->
<!-- Counter-example for the same scenario: preference approach if the entire -->
<!-- rate calculation had to be replaced by an external freight API -->
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <preference for="Magento\OfflineShipping\Model\Carrier\Tablerate"
                 type="Mironsoft\Shipping\Model\Carrier\ExternalFreightRate"/>
</config>

<?php
// app/code/Mironsoft/Shipping/Model/Carrier/ExternalFreightRate.php
// Counter-example: preference class, full replacement of the rate calculation
declare(strict_types=1);

namespace Mironsoft\Shipping\Model\Carrier;

use Magento\OfflineShipping\Model\Carrier\Tablerate;
use Magento\Quote\Model\Quote\Address\RateRequest;
use Magento\Shipping\Model\Rate\Result;
use Magento\Shipping\Model\Rate\ResultFactory;
use Mironsoft\Shipping\Api\ExternalFreightClientInterface;

/**
 * Replaces the tablerate calculation entirely with rates from an external
 * freight API. Implemented as a preference because the whole calculation
 * and its internal zone and weight class data structure are replaced,
 * not just enriched with an additional plugin step.
 */
final class ExternalFreightRate extends Tablerate
{
    /**
     * @param ExternalFreightClientInterface $freightClient External freight API client.
     * @param ResultFactory $rateResultFactory Factory for building the shipping rate result.
     */
    public function __construct(
        private readonly ExternalFreightClientInterface $freightClient,
        private readonly ResultFactory $rateResultFactory,
        mixed ...$parentArgs
    ) {
        parent::__construct(...$parentArgs);
    }

    /**
     * Collects shipping rates entirely from the external freight API,
     * replacing the standard tablerate zone and weight lookup.
     *
     * @param RateRequest $request Rate request with quote items and destination.
     * @return Result|bool
     */
    public function collectRates(RateRequest $request): Result|bool
    {
        if (!$this->getConfigFlag('active')) {
            return false;
        }

        $quote = $this->freightClient->getQuote($request);
        if ($quote === null) {
            return false;
        }

        $result = $this->rateResultFactory->create();
        $result->append($quote->toShippingRate());

        return $result;
    }
}

7. Szenario B: Quote Address Total Collector überschreiben

Ein zweiter realer Fall: Der Kunde benötigt einen komplett neuen Total-Collector, der einen firmenspezifischen Treuebonus als eigene Zeile in der Warenkorb-Summenübersicht ausgibt, mit eigener Berechnungsreihenfolge relativ zu Rabatt und Steuer. Total-Collectors implementieren Magento\Quote\Model\Quote\Address\Total\AbstractTotal und werden nicht über di.xml Type-Konfiguration eines einzelnen Interfaces angesprochen, sondern über die Sales-Sequence-Konfiguration in sales.xml, wo jeder Collector einen eigenen Code und eine Sortierposition erhält.

Nach der Checkliste ergibt sich hier ein eindeutiges Bild: Es wird eine komplett neue Berechnungslogik benötigt, kein bestehender Collector wird nur angereichert, also ist dies keine Kandidatensituation für ein Plugin auf eine bestehende Methode. Stattdessen erstellt man eine neue Collector-Klasse, die AbstractTotal implementiert, und registriert sie zusätzlich über sales.xml. Sollte zusätzlich das Verhalten eines bestehenden Collectors wie des Rabatt-Collectors angepasst werden müssen, etwa um den Treuebonus vom Rabatt auszuschließen, ist dafür ein separates Plugin auf die collect()-Methode des bestehenden Collectors die richtige Entscheidungsregel, keine Preference auf den gesamten Collector. Diese Kombination aus neuer Collector-Klasse plus punktuellem Plugin auf einen bestehenden Collector ist in der Praxis der häufigste Fall bei Anpassungen an der Summenberechnung.

In der praktischen Umsetzung erhält die neue Collector-Klasse einen eindeutigen Code in sales.xml sowie eine sortOrder-Position relativ zu Rabatt, Steuer und Versandkosten. Der Konstruktor der Collector-Klasse folgt konsequent PHP-8.4-Constructor-Property-Promotion für injizierte Services wie einen Berechnungsservice für den Treuebonus. Die Methode collect() ruft zunächst parent::collect() auf und ergänzt danach den eigenen Betrag über addTotalAmount(), während fetch() die Anzeige-Zeile für die Warenkorb-Summenübersicht liefert. Diese Struktur bleibt unabhängig davon, ob zusätzlich ein punktuelles Plugin auf einen bestehenden Collector registriert wird oder nicht.

8. Szenario C und D: Order Email Sender und Produktpreis-Berechnung

Szenario C: Der Kunde möchte, dass Bestellbestätigungen für B2B-Kunden mit hohem Bestellwert eine zusätzliche interne Kopie an das Vertriebsteam erhalten, ohne die eigentliche Versandlogik der E-Mail an den Kunden zu verändern. Zielklasse ist Magento\Sales\Model\Order\Email\Sender\OrderSender. Nach der Checkliste: Die Klasse ist nicht final, es wird keine Signaturänderung benötigt, es ist plausibel, dass andere Module bereits Plugins auf denselben Sender registriert haben (zum Beispiel Newsletter- oder CRM-Integrationen), und es handelt sich nicht um ein Resource Model. Die Entscheidungsregel fällt eindeutig auf ein after-Plugin auf die send()-Methode, das zusätzlich eine BCC-Kopie auslöst, ohne den Versand der eigentlichen Kunden-E-Mail zu beeinflussen. Eine Preference wäre hier grob überdimensioniert und würde bei jedem Magento-Security-Patch am Order-Email-Modul zu Konflikten führen.

Szenario D: Der Kunde benötigt eine komplett andere Preisberechnungslogik für eine Produktgruppe mit dynamischen, tagesaktuellen Rohstoffpreisen, die aus einer externen API kommen und die Standard-Preisberechnung von Magento\Catalog\Model\Product\Type\Price vollständig ersetzen sollen, inklusive geänderter interner Caching-Struktur für die berechneten Preise. Hier greifen gleich zwei Kriterien: Es wird eine komplett neue interne Berechnungslogik mit eigenem Caching benötigt, keine additive Ergänzung, und die Preisberechnung wird an vielen Stellen im Checkout, in der Produktliste und im Warenkorb aufgerufen, wo ein Plugin mit falscher around-Implementierung zu inkonsistenten Preisanzeigen führen könnte. Die Entscheidungsregel fällt hier auf eine Preference, weil die komplette interne Konstruktionslogik der Preisermittlung ausgetauscht wird und kein bestehendes Verhalten lediglich umschlossen werden soll.

Für Szenario D bedeutet die Entscheidung in der Praxis: Die Preference-Klasse erbt von Magento\Catalog\Model\Product\Type\Price, injiziert per Constructor Property Promotion einen externen Preisdienst sowie einen internen Preis-Cache, und überschreibt ausschließlich getFinalPrice() für Produkte der betroffenen Gruppe, während sie für alle anderen Produkte transparent auf parent::getFinalPrice() zurückfällt. Genau diese Struktur, ein neuer Berechnungspfad plus sauberer Fallback auf die Standardlogik, unterscheidet eine sinnvoll begrenzte Preference von einer riskanten Preference, die den gesamten Originalcode unnötig kopiert.

9. sortOrder-Konflikte erkennen mit dev:di:info

Sobald mehrere Vendoren, etwa das Mironsoft-Modul und ein installiertes Marketplace-Modul, Plugins auf dieselbe Methode registrieren, entscheidet ausschließlich der sortOrder-Wert über die Ausführungsreihenfolge, nicht die alphabetische Reihenfolge der Modulnamen und nicht der Installationszeitpunkt. Zwei Plugins mit identischem sortOrder werden zwar deterministisch, aber implizit nach interner Modulreihenfolge sortiert, was bei einem Update der Modulreihenfolge zu einem stillen Verhaltenswechsel führen kann. Deshalb gehört die Prüfung von sortOrder-Konflikten in jede Code-Review-Checkliste, bevor ein neues Plugin auf eine bereits von Dritten gepluggte Methode registriert wird.

Das CLI-Kommando bin/magento dev:di:info "Klassenname" listet für eine gegebene Klasse alle registrierten Plugins inklusive ihrer sortOrder-Werte und ihrer Ausführungsreihenfolge auf. Für Total-Collectors und Fragen zur Preference-Auflösung ergänzt dev:di:info zusätzlich die aktuell aktive Preference-Zielklasse, sofern eine konfiguriert ist. Diese Ausgabe ist der schnellste Weg, um vor dem Schreiben eines neuen Plugins zu prüfen, ob bereits Fremdcode auf derselben Methode aktiv ist, und um im Falle eines Bugs nach einem Update herauszufinden, welches Modul mit welcher Priorität in eine bestimmte Methode eingreift.


# Detect plugin sortOrder conflicts before registering a new plugin
bin/magento dev:di:info "Magento\Sales\Model\Order\Email\Sender\OrderSender"

# Expected output includes a plugin list similar to:
# Plugins for class Magento\Sales\Model\Order\Email\Sender\OrderSender
# Type: Magento\Sales\Model\Order\Email\Sender\OrderSender
# [around] someVendor_crm_sync (sortOrder=10)
# [after]  mironsoft_bcc_sales_team (sortOrder=20)

# Also check which preference (if any) is currently active for a type
bin/magento dev:di:info "Magento\Catalog\Model\Product\Type\Price"

# Clear generated interception cache after adding or changing plugin sortOrder
bin/magento cache:clean config di

10. Zusammenfassung

Die Entscheidung Preference oder Plugin lässt sich in der Praxis auf vier feste Kriterien reduzieren, die vor jeder Code-Zeile beantwortet werden sollten: Ist die Zielklasse final oder ohne @api-Kennzeichnung, ist eine Änderung der Methodensignatur nötig, existiert bereits ein Fremdmodul-Plugin auf derselben Methode, und handelt es sich um ein Resource Model oder eine Collection. Die vier neuen Praxisszenarien in diesem Artikel, von der Versandkostenberechnung über den Quote-Total-Collector bis zum Order-Email-Sender und der Produktpreis-Berechnung, zeigen, dass diese Entscheidungsregel in jedem Fall zu einer eindeutigen, begründbaren Antwort führt, statt zu einer Bauchentscheidung im Code Review.

Wer diese Checkliste konsequent anwendet, reduziert nicht nur die Anzahl der Preferences im Projekt, was direkt die Update-Sicherheit bei Magento-Patches erhöht, sondern verhindert auch stille Kollisionen mit Drittanbieter-Modulen, die über sortOrder-Konflikte oder überschriebene Preferences entstehen. Das Kommando dev:di:info gehört fest in den Entwicklungs-Workflow, nicht nur zur nachträglichen Fehlersuche, sondern als Standardschritt vor jeder neuen Registrierung in di.xml.

Preference oder Plugin, die Entscheidungsregel auf einen Blick

final und @api prüfen

Bei final-Klassen oder fehlender @api-Kennzeichnung ist eine Preference oft technisch unmöglich oder update-unsicher. Erste Frage jeder Entscheidungsregel.

Signatur vs. additives Verhalten

Neue Signatur oder komplette Neuimplementierung: Preference. Zusätzliches Verhalten um bestehenden Aufruf: Plugin.

Fremdmodul-Kompatibilität

Bestehendes Plugin eines anderen Vendors auf derselben Methode: mit dev:di:info prüfen, dann eigenes Plugin über sortOrder einreihen.

Resource Models absichern

Bei Resource Models und Collections Events bevorzugen, Plugins nur als before/after, around vermeiden, Preferences meiden.

11. FAQ: Preference oder Plugin Entscheidungsregel

1Wie entscheide ich schnell zwischen Preference oder Plugin?
Vier Fragen: final/@api-Status, Signaturänderung nötig, bestehendes Fremdmodul-Plugin, Resource Model oder Collection. Diese Checkliste liefert fast immer eine eindeutige Antwort.
2Preference auf eine final-Klasse möglich?
Nein, PHP verbietet Vererbung von final-Klassen. Nur ein Plugin bleibt möglich, sofern die Methode selbst nicht final ist.
3Zwei Preferences auf dieselbe Klasse?
Nur eine ist aktiv, die zuletzt geladene gewinnt still, ohne Fehlermeldung. Deshalb bei Fremdmodul-Kompatibilität Plugins bevorzugen.
4Warum sind Plugins auf Resource Models riskant?
Extrem häufige Aufrufe, Proxy-Overhead bei around, Gefahr des vollständig blockierten Speichervorgangs bei fehlendem proceed(). Events sind meist sicherer.
5Wie finde ich bestehende Plugins auf einer Methode?
bin/magento dev:di:info "Klassenname" listet alle Plugins mit sortOrder und die aktive Preference-Zielklasse auf.
6Wie bestimmt sortOrder die Reihenfolge?
Niedrigerer Wert zuerst bei before/around, gleiche Reihenfolge bei after. Identische Werte führen zu implizitem, modulreihenfolgeabhängigem Verhalten.
7Neuer Quote Total Collector: Preference oder eigene Klasse?
Eigene Klasse, registriert über sales.xml. Total Collectors werden nicht per Preference-Type angesprochen, sondern über die Sales-Sequence.
8Preference bei Order-Email gerechtfertigt?
Fast nie. Ein Plugin auf OrderSender reicht für BCC-Kopien oder zusätzliche Validierungen und ist update-sicherer.
9Auswirkung auf Update-Sicherheit?
Plugins brechen meist nur bei Signaturänderungen. Preferences brechen häufiger bei internen Refactorings ohne öffentliche Signaturänderung.
10Muss die Checkliste jedes Mal neu durchlaufen werden?
Ja, dauert aber nur wenige Minuten: final/@api prüfen, Signaturbedarf bewerten, dev:di:info ausführen, Zielklassen-Typ feststellen.

Mironsoft

Magento 2 Architektur, Code Review und Dependency-Injection-Beratung

Bereit für saubere Preference- und Plugin-Entscheidungen im Team?

Wir helfen bei der Etablierung einer festen Entscheidungsregel, dem Audit bestehender di.xml-Konfigurationen und der Auflösung von sortOrder-Konflikten mit Drittanbieter-Modulen.

DI-Audit

Bestehende Preferences und Plugins mit dev:di:info systematisch prüfen

Team-Standards

Entscheidungscheckliste als festen Bestandteil des Code Reviews etablieren

Refactoring

Riskante Preferences durch sichere Plugins oder Events ersetzen