Du erbst eine Magento-2-Codebase. Du willst wissen, was du da vor dir hast, und fährst das eine Kommando, das immer die Wahrheit sagt: find app/code -name di.xml | xargs grep -l '<preference'. Die Ausgabe ist eine Liste von dutzenden Dateien, und ein einzelnes Modul in der Codebase hat seine eigene Liste von über zwanzig Preferences. Du hörst nach dem dritten Screen auf zu lesen. Nichts davon stand im README.
<preference> selbst ist nicht der Schurke. An den richtigen Stellen ist es das richtige Werkzeug. Der Schurke ist, <preference> als Reflex für „hier Verhalten ändern" zu nutzen, wenn ein Plugin oder ein Event-Observer die Aufgabe ohne jeden Preis erledigt hätte. Wenn wir das sehen, hat sich der Preis über drei Jahre und sieben Entwickler aufgezinst, und niemand weiß mehr, welche Preferences tragend sind und welche Experimente waren, die einfach ausgeliefert wurden.
TL;DR
- Die richtige Hierarchie lautet: Observer zuerst, Plugin zweite Wahl, Preference als letztes Mittel. Observer sind die Ereignis-Hooks, die das Framework vorgesehen hat. Plugins sind ein komponierbarer Workaround für Verhaltensänderungen. Preferences sind ein globaler Klassen-Tausch und sollten selten und begründet sein.
- Ein
<preference>indi.xmlersetzt eine Klasse global. Jedesnew $classin der Codebase bekommt deine Ersetzung, überall, ob der Caller damit gerechnet hat oder nicht. - Ein Plugin auf derselben Klasse wrappt Methoden. Es komponiert mit anderen Plugins, überlebt Upstream-Signature-Änderungen besser und verhindert nicht, dass andere die Klasse erweitern.
- Preference ist die richtige Wahl für abstrakte Factories, Adapter hinter einem Interface und einen schmalen Satz Service-Contract-Implementierungen. Es ist die falsche Wahl für Core-Models, HTTP-Controller, Blocks und fast alles unter
Magento\Framework\. - Die vier Fehlermodi, die wir immer wieder sehen: zwei Module überschreiben dieselbe Klasse (einer gewinnt still), Upgrade-Fäule, wenn die Core-Method-Signatur sich ändert, Security-Plugins durch No-op-Stubs ersetzt, und Child-Klassen, die
parent::__construct()auskommentiert haben, um einem Argument-Wechsel auszuweichen. - Der Audit ist billig. Ein Grep, ein Nachmittag Lesen, eine Tabelle mit „safe / risky / incident" pro Treffer. Das ist die Arbeit.
Der Mechanismus: Observer, Plugin und <preference> auf DI-Ebene
Magento bietet drei Erweiterungspunkte, und sie sind nicht gleichwertig.
Ein Event-Observer (events.xml + Observer-Klasse) feuert, wenn das Framework ein benanntes Event dispatcht. Er ist der vorgesehene Mechanismus, um auf Dinge zu reagieren, die passiert sind: Bestellung aufgegeben, Produkt gespeichert, Kunde eingeloggt. Observer verändern keine Rückgabewerte, wrapppen keine Konstruktoren und streiten mit nichts. Sie sind der Extension-Point mit dem geringsten Reibungswiderstand und sollten deine erste Wahl sein.
Ein Plugin (in di.xml per <type>-Knoten deklariert) wrappt eine konkrete Methode einer Klasse, ohne die Klasse zu ersetzen. beforeSave-, aroundExecute- und afterGetName-Hooks komponieren sich um die Methode. Das Originalobjekt bleibt erhalten, der Originalkonstruktor läuft unverändert, und mehrere Plugins aus mehreren Modulen stapeln sich in einer definierten Sort-Order. Plugins sind das Mittel der Wahl, wenn du Methodeneingaben oder -ausgaben ändern musst und es kein Event gibt, das deinen Fall abdeckt.
Eine <preference> sagt Magentos DI-Compiler: „immer wenn jemand den Object-Manager nach einer Instanz von Klasse A fragt, gib ihm stattdessen eine Instanz von Klasse B." Das ist eine globale, zur Compile-Zeit wirksame Klassen-Ersetzung. new \Magento\Catalog\Model\Product(...) innerhalb von Core-Code liefert weiterhin Ihre Product-Subklasse zurück, weil der Object-Manager die Preference aufgelöst hat. Greif hierzu zuletzt, und nur in den engen Fällen unten.
Der Unterschied zwischen Plugins und Preferences zeigt sich an drei Stellen, die zählen:
Komposition. Zwei Module können Plugins auf derselben Methode deklarieren und beide laufen. Zwei Module können keine Preferences auf derselben Klasse deklarieren, bei denen beide laufen. Einer gewinnt, still, basierend auf Modul-Ladereihenfolge.
Upgrade-Oberfläche. Ein Plugin kümmert sich um die Signatur einer Methode. Eine Preference kümmert sich um die gesamte Klasse: Konstruktorargumente, Methodensignaturen, die Reihenfolge privater Properties, die irgendwo via Reflection angesprochen werden. Wenn Magento 2.4.7 ein Konstruktorargument auf einem Core-Model ändert, bricht oder, schlimmer, hinkt jede Preference auf diesem Model.
Observability. Ein Plugin taucht in Stacktraces unter seinem eigenen Klassennamen auf. Eine Preference ersetzt die Klasse komplett; ein Stacktrace zeigt Ihre Klasse, aber wenn Sie keinen Marker gesetzt haben, finden Code-Suchtools den Override nur, wenn sie di.xml crawlen.
Wann <preference> die richtige Wahl ist
Das Framework selbst liefert Preferences. Die Intention ist nicht „nie benutzen". Die Intention ist „dort benutzen, wo es der designte Erweiterungspunkt ist".
Zwei Szenarien, in denen Preference die richtige Wahl ist:
-
Abstrakte Factories und Interfaces ohne Default.
Magento\Framework\Filesystem\DriverInterfacewird mitFileals Default und einer Preference ausgeliefert, die das pinnt. Das ist der Core-DI-Mechanismus, wie er designt ist. - Adapter-Klassen hinter einem Service-Contract. Wenn du den Search-Client-Adapter oder den Messaging-Queue-Driver durch eine eigene Implementierung ersetzt, ist Preference die idiomatische Wahl, weil das Interface der Vertrag ist und der Adapter ein Implementierungsdetail.
Wenn die Klasse, die du preferencen willst, konkret ist, greif stattdessen zu Observer oder Plugin. Wenn keins davon sauber passt, ist das ein Signal, dass das Design neu gedacht werden muss, nicht ein Signal, zur Preference zu greifen.
Fehlermodus 1: zwei Module, ein Gewinner, stiller Datenverlust
Zwei Marketplace-Module deklarieren beide ein <preference> für Magento\Catalog\Model\Product. Modul A schreibt die Klasse um, um SKU-Groß-/Kleinschreibung beim Save zu normalisieren. Modul B schreibt die Klasse um, um einen Custom-Attribute-Observer zu ergänzen. Der DI-Compiler löst eine der beiden auf, basierend auf Modul-Ladereihenfolge. Die Logik des anderen Moduls ist still unerreichbar. Keine Warnung zur Compile-Zeit. Keine Log-Zeile zur Laufzeit. Nur ein Feature, das funktioniert hat und nicht mehr funktioniert.
Wir haben das zwischen zwei Inhouse-Modulen gesehen, zwischen Inhouse-Code und einer Marketplace-Extension, und zwischen zwei Marketplace-Extensions. Das Symptom ist immer dasselbe: „hat in Staging funktioniert, ist in Production gebrochen" nach einem Deploy, der die Modul-Ladereihenfolge verändert hat. Oder „läuft auf Dev, läuft nicht auf Prod", weil die composer.lock anders aufgelöst hat.
<preference>-Äquivalent zu Plugin-sortOrder. Du kannst zwei konkurrierende Preferences nicht per Konfiguration auflösen. Der Fix ist immer, eine davon herauszureißen, üblicherweise durch Umbau zu einem Plugin oder Event-Observer. Weitere Preferences dagegenzustellen ist der Weg, auf dem Shops mit Dutzenden Preferences in einer Codebase enden.
Fehlermodus 2: Upgrade-Fäule
Preferences sind eng an die Signaturen der umgeschriebenen Klasse gekoppelt: Konstruktorargumente, öffentliche Methodensignaturen, manchmal protected Methods, die die Child-Klasse überschreibt. Wenn Magento eine neue Minor-Version ausliefert, die ein pflichtiges Konstruktorargument auf einer Core-Klasse ergänzt, muss jede Preference auf dieser Klasse aktualisiert werden, sonst scheitert die DI-Kompilierung.
Das „oder hinkt"-Ergebnis ist häufiger als das „oder scheitert"-Ergebnis. Entwickler patchen den direkten Compile-Fehler, indem sie das fehlende Argument ergänzen, oft ohne zu lesen, wofür es da ist, oder es korrekt durchzuverdrahten. Die Klasse kompiliert und läuft. Das Verhalten, das das neue Konstruktorargument ermöglichen sollte, ist still abgeschaltet, weil die Preference-Implementierung es nicht nutzt.
Deshalb bekommen preference-lastige Codebases mit den Jahren eine eigene Textur. Kleine Stücke Core-Verhalten hören nach jedem Upgrade auf zu funktionieren, und niemand merkt es, weil jedes einzelne klein ist. Die kumulative Wirkung ist ein Shop, auf dem die Hälfte der Plattform-Features still tot ist.
Fehlermodus 3: Security-Plugins still ersetzt
Die schlimmere Variante von Fehlermodus 1. Statt zweier Business-Logik-Module, die sich um Product streiten, deklariert ein Custom-Modul eine Preference auf ein Magento-Security-Plugin und ersetzt es durch ein No-op.
Das kanonische Beispiel: Magento\ReCaptchaWebapiRest\Plugin\RestValidationPlugin. Eine Preference, die auf eine Custom-Klasse zeigt, die ohne Arbeit zurückgibt, schaltet reCAPTCHA-Validation auf jedem REST-fronted Login-Flow ab. Das Admin-UI zeigt reCAPTCHA weiter als aktiviert. Die Config liest sich gesund. Das Plugin wird zur Laufzeit umgangen, still, und kein Health-Check meldet es.
Wir haben genau dieses Finding in Die 7 Security-Findings, die wir in fast jedem Magento-2-Code-Audit sehen unter Finding #6 behandelt. Das breitere Prinzip: jede <preference>, die eine Magento\*\Plugin\*-Klasse nimmt, ist ein Warnsignal. Plugins existieren, weil das Framework dieses konkrete Verhalten komponierbar haben will. Ein Plugin durch eine Preference zu ersetzen heißt „Komposition ist mir egal, ich will dieses Verhalten vollständig besitzen", was manchmal legitim ist und meistens nicht.
Fehlermodus 4: gebrochene Konstruktorketten
Das Muster: ein Modul-Autor schreibt eine Child-Klasse, die eine Magento-Framework-Klasse preferencen soll. Die Framework-Klasse hat viele Konstruktorargumente. Alle durchzuverdrahten ist langweilig. Der Autor überspringt entweder parent::__construct() komplett, ruft es mit einer Teilmenge der Argumente auf oder kommentiert es „vorübergehend" aus und liefert so aus.
Wir haben das konkret auf Magento\Framework\Stdlib\Cookie\PhpCookieManager gesehen, auf TransportBuilder und auf genug Core-Collections, dass es kein Einzelfall ist. Jeder nächste Entwickler, der die Child-Klasse liest, sieht den auskommentierten Parent-Aufruf und fragt sich, ob das Absicht war. Niemand will der sein, der ihn einkommentiert, weil „könnte ja was kaputt machen". Er bleibt auskommentiert. Die Klasse funktioniert in den meisten Flows, weil die meisten Flows das, was parent::__construct initialisiert hätte, nicht berühren. Die Flows, die es berühren, brechen subtil, in Production, deutlich später.
Der Fix ist immer derselbe: den parent::__construct()-Aufruf mit allen pflichtigen Argumenten wiederherstellen, Tests laufen lassen, die brechenden Tests fixen. Der Bug, den der auskommentierte Aufruf umgangen hat, ist meistens ein Konstruktorargument, das in einem Magento-Release ergänzt wurde und nicht dem entspricht, was die Child-Klasse übergeben hat. Das Signaturmismatch lösen, nicht das Symptom.
Die Preferences in deiner Codebase auditieren
Das eine Kommando fahren. Jeden Treffer klassifizieren. Das ist der Audit.
1find app/code -name di.xml | xargs grep -l '<preference' | while read f; do echo "--- $f ---"; grep -oE '<preference[^>]+for="[^"]+"[^>]+type="[^"]+"' "$f"; done
Die Ausgabe in eine Tabelle kopieren. Drei Spalten: Modul, Zielklasse, dein Urteil. Urteile sind eins von:
- SAFE. Ziel ist ein Interface ohne Default, ein Adapter hinter einem Service-Contract, eine Factory. Die Preference tut das idiomatische Ding.
- RISKY. Ziel ist eine konkrete Core-Klasse. Die Preference mag heute funktionieren; sie ist eine Wartungslast für jedes zukünftige Upgrade. Kandidat für ein Plugin-Refactor.
-
INCIDENT. Ziel ist eine Plugin-Klasse, besonders ein Security-Plugin. Ziel ist eine
Magento\Framework\*-Klasse. Oder die Child-Klasse enthält ein auskommentiertesparent::__construct(). Heute lesen, heute fixen.
Eine Codebase mit dreißig oder mehr Preferences hat eine Handvoll von jeder Sorte. Die INCIDENT-Zeilen fliegen raus. Die RISKY-Zeilen gehen ins Refactor-Backlog mit einer Plugin-Umbau-Schätzung. Die SAFE-Zeilen bleiben, wie sie sind, und werden dokumentiert, damit der nächste Entwickler weiß, warum sie da sind.
<preference> deployen, verifizieren, dass der neue Code zuerst läuft, dann die Preference löschen. Die Preference zu entfernen, bevor der Ersatz steht, ist der Weg, eine Regression auszuliefern.
Weiterführende Artikel
- Observer-Rekursion in Magento 2: die versteckten Kosten von sales_order_save_after. Preference-lastige Codebases liefern tendenziell Observer-Kaskaden-Muster neben ihren Rewrites aus. Wenn du einen findest, prüf auf den anderen.
- Die 7 Security-Findings, die wir in fast jedem Magento-2-Code-Audit sehen. Finding #6 ist das reCAPTCHA-Preference-Muster, im Kontext mit Detection-Snippets.
Die Preference-Audit-Checkliste
-
01
Jede Preference in der Codebase aufzählen
Ausführen:
find app/code -name di.xml | xargs grep -l '<preference'und die for/type-Paare extrahieren. vendor/ ist für den ersten Durchgang meist außer Scope, aber einen zweiten Grep wert, wenn du ein Vendor-Modul als Verursacher vermutest. -
02
Jede als SAFE, RISKY oder INCIDENT klassifizieren
Interface ohne Default oder Adapter hinter einem Service-Contract = SAFE. Konkrete Core-Klasse = RISKY. Plugin-Klasse oder
Magento\Framework\*-Ziel = INCIDENT. Auskommentiertesparent::__construct()in der Child-Klasse = INCIDENT, unabhängig vom Ziel. -
03
Nach konkurrierenden Preferences auf derselben Klasse prüfen
Die extrahierte Zielklassen-Spalte auf Duplikate greppen. Jedes Ziel mit mehr als einer Preference-Deklaration heißt, dass ein Modul still verliert. Fixen, bevor Sie irgendetwas anderes ausliefern.
-
04
INCIDENT-Zeilen zuerst triagieren
Die Child-Klasse lesen. Wenn sie
parent::__constructüberspringt, wiederherstellen und die Test-Suite laufen lassen. Wenn sie ein Security-Plugin nimmt, verifizieren, dass die Ersetzung das Richtige tut, oder zu einem sauberen Plugin umbauen. -
05
Die RISKY-Umbauten planen
Für jede RISKY-Zeile den Plugin-Refactor schätzen. Die meisten konkrete-Core-Klasse-Preferences lassen sich als ein oder zwei Plugins (
beforeSave/afterLoad/aroundExecute) abbilden, mit einem Bruchteil des Upgrade-Risikos. Priorisierung danach, wie oft die Klasse auf heißen Pfaden liegt. -
06
Die SAFE-Zeilen dokumentieren
Einen Kommentar in der di.xml ergänzen, der erklärt, warum jede SAFE-Preference die idiomatische Wahl ist. Ihr zukünftiges Ich oder der nächste Entwickler wird sich in 18 Monaten dafür bedanken, wenn die Frage wiederkommt.
-
07
Einen CI-Guard gegen neue Preferences ergänzen
Ein einfacher Check, der den Build scheitern lässt, wenn eine neue
<preference>in einem PR auftaucht, ohne ein verlinktes Issue, das die SAFE-Klassifizierung erklärt. Verhindert, dass der Bestand nachwächst. -
08
Nach jedem Magento-Minor-Release neu auditieren
Jedes Magento-Upgrade ändert Konstruktorsignaturen auf einigen Core-Klassen. Die Aufzählung vor und nach dem Upgrade erneut fahren. Jede Preference, deren Zielklasse ein neues pflichtiges Argument bekommen hat, braucht Aufmerksamkeit.
Dreißig Preferences in einer Codebase ist ein Refactor-Projekt
Wir machen Magento-2-Code-Audits und Custom-Development-Engagements, die einen Preference-Bestand in einen Plugin-Migrationsplan verwandeln. Du bekommst die Tabelle, die Umbau-Schätzungen und die PRs. Keine 80-seitige PDF.