mironsoft.de › Blog › Design Patterns
Magento 2 · Design Patterns
Preferences Pattern
in Magento 2

Preferences in di.xml verknüpfen Interfaces mit konkreten Implementierungen — oder überschreiben bestehende Klassen. Das ist das mächtigste Werkzeug im DI-System und zugleich das am häufigsten falsch eingesetzte. Interface-first Design, Preference-Konflikte und Virtual Types als Alternative.

15 Min. Lesezeit PHP 8.4 Magento 2.4.8

Was sind Preferences in Magento 2?

Preferences sind DI-Konfigurationen in der di.xml-Datei, die dem Magento 2 ObjectManager mitteilen, welche konkrete PHP-Klasse er instanziieren soll, wenn ein bestimmtes Interface oder eine bestimmte Klasse angefordert wird. Der Begriff "Preference" bedeutet wörtlich "Bevorzugung" — man teilt dem DI-Container mit, welche Implementierung er "bevorzugen" soll, wenn er eine bestimmte Abhängigkeit auflösen muss. Ohne Preferences könnte der DI-Container Interfaces nicht instanziieren, weil PHP selbst Interfaces nicht instanziieren kann.

Es gibt zwei grundlegende Einsatzszenarien für Preferences: Das erste und bei weitem häufigere Szenario ist die Interface-Bindung, bei der ein Interface mit seiner konkreten Implementierung verknüpft wird. Dieses Szenario ist für alle Module, die Service Contracts (Interfaces) verwenden, unverzichtbar. Das zweite Szenario ist der Klassen-Override, bei dem eine bestehende Magento-Klasse durch eine eigene Implementierung ersetzt wird. Dieses Szenario ist mächtig, aber riskant und sollte mit Bedacht eingesetzt werden.

Die Preference-Syntax in der di.xml ist denkbar einfach: Das <preference>-Element hat zwei Attribute: for (das Interface oder die Klasse, die überschrieben werden soll) und type (die konkrete Klasse, die stattdessen verwendet werden soll). Der vollständige Klassen-Namespace muss angegeben werden. Preferences in der di.xml des eigenen Moduls haben Priorität über Preferences in Core-Modulen, sofern das eigene Modul nach dem Core-Modul geladen wird.

Preferences sind ein Teil des Magento 2 Dependency Injection Containers, der seinerseits auf dem Principle of Inversion of Control (IoC) basiert. High-Level-Module (Geschäftslogik) sollen nicht von Low-Level-Modulen (konkrete Implementierungen) abhängen, sondern beide sollen von Abstraktionen (Interfaces) abhängen. Preferences sind der Mechanismus, der diese Abstraktion ermöglicht, indem er die Verbindung zwischen Interface und Implementierung außerhalb des Codes konfiguriert.

Interface-first Design: Das Fundament

Interface-first Design ist das Architekturprinzip, nach dem Magento 2 Module gebaut werden sollen. Die Idee ist einfach: Bevor man eine Implementierung schreibt, definiert man das Interface. Das Interface beschreibt das "Was" — welche Operationen sind möglich, welche Parameter werden erwartet, welche Rückgabetypen werden versprochen. Die Implementierung beschreibt das "Wie". Andere Module kennen nur das Interface, nie die Implementierung.

In der Praxis bedeutet Interface-first Design für ein Magento 2 Modul folgende Struktur: Im Api/-Verzeichnis liegen alle Interfaces. Das Api/Data/-Unterverzeichnis enthält Data Interfaces (wie PostInterface), die typisierte Getter und Setter definieren. Das Api/-Hauptverzeichnis enthält Repository- und Service-Interfaces (wie PostRepositoryInterface). Im Model/-Verzeichnis liegen die konkreten Implementierungen dieser Interfaces. In der etc/di.xml werden die Verbindungen über Preferences hergestellt.

<?php
declare(strict_types=1);

namespace Mironsoft\Blog\Api\Data;

/**
 * Blog post data interface — defines the contract for post entities.
 * All other modules depend on this interface, never on the concrete model.
 */
interface PostInterface
{
    public const POST_ID = 'post_id';
    public const TITLE = 'title';
    public const CONTENT = 'content';
    public const URL_KEY = 'url_key';
    public const IS_ACTIVE = 'is_active';
    public const PUBLISH_DATE = 'publish_date';
    public const AUTHOR_ID = 'author_id';

    /**
     * Get post ID.
     */
    public function getId(): ?int;

    /**
     * Get post title.
     */
    public function getTitle(): string;

    /**
     * Set post title.
     */
    public function setTitle(string $title): static;

    /**
     * Get post content.
     */
    public function getContent(): string;

    /**
     * Set post content.
     */
    public function setContent(string $content): static;

    /**
     * Get URL key.
     */
    public function getUrlKey(): string;

    /**
     * Check if post is active.
     */
    public function isActive(): bool;

    /**
     * Set active status.
     */
    public function setIsActive(bool $isActive): static;

    /**
     * Get publish date.
     */
    public function getPublishDate(): string;
}

Interface-first Design hat einen weitreichenden Vorteil: Die Implementierung kann ausgetauscht werden, ohne dass andere Module geändert werden müssen. Wenn morgen eine bessere Implementierung des PostRepositoryInterface existiert — etwa eine, die einen Cache integriert — reicht es, die Preference in der di.xml zu ändern. Alle Module, die das Interface verwenden, arbeiten automatisch mit der neuen Implementierung, ohne Code-Änderungen.

In PHP 8.4 mit strikten Typen und den neuen Property-Hook-Features können Interfaces noch präziser definiert werden. Die Rückgabetypen sind vollständig typisiert, Parameter ebenfalls. PHPStan analysiert die Übereinstimmung zwischen Interface-Definition und Implementierung. Wenn die Implementierung eine Methode mit einem anderen Rückgabetyp implementiert, gibt PHPStan einen Fehler aus — so werden Interface-Verletzungen bereits zur Entwicklungszeit erkannt.

Interface-zu-Klasse Bindung: Der richtige Einsatz

Die Interface-zu-Klasse Bindung ist der korrekte und unverzichtbare Einsatz von Preferences. Jedes Mal, wenn ein Interface per Constructor Injection in eine Klasse injiziert wird, braucht der DI-Container eine Preference, um zu wissen welche konkrete Klasse er instanziieren soll. Ohne diese Preference wirft der ObjectManager beim ersten Aufruf eine LogicException oder eine vergleichbare Exception mit der Meldung, dass das Interface nicht instanziiert werden kann.

Für ein vollständiges Modul mit Repository Pattern braucht man typischerweise mindestens drei Preferences: Die erste verbindet das Repository Interface (PostRepositoryInterface) mit der Repository-Implementierung (PostRepository). Die zweite verbindet das Data Interface (PostInterface) mit dem Model (Post). Die dritte verbindet das SearchResults Interface (PostSearchResultsInterface) mit der SearchResults-Klasse (PostSearchResults).

<?xml version="1.0"?>
<!-- app/code/Mironsoft/Blog/etc/di.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">

    <!-- Repository Interface → Concrete Implementation -->
    <preference for="Mironsoft\Blog\Api\PostRepositoryInterface"
                type="Mironsoft\Blog\Model\PostRepository"/>

    <!-- Data Interface → Model (for type-safe entity handling) -->
    <preference for="Mironsoft\Blog\Api\Data\PostInterface"
                type="Mironsoft\Blog\Model\Post"/>

    <!-- SearchResults Interface → Concrete SearchResults -->
    <preference for="Mironsoft\Blog\Api\Data\PostSearchResultsInterface"
                type="Mironsoft\Blog\Model\PostSearchResults"/>

    <!-- Optional: Area-specific logger using Virtual Type -->
    <type name="Mironsoft\Blog\Model\PostRepository">
        <arguments>
            <argument name="logger" xsi:type="object">Mironsoft\Blog\Logger\Logger</argument>
        </arguments>
    </type>

</config>

Ein wichtiges Detail: Die Preference-Konfiguration gilt auch für die generierten Factories. Wenn PostFactory aufgerufen wird und die Preference für PostInterface auf Post gesetzt ist, erstellt die Factory ein Post-Objekt. Das ist konsistent und bedeutet: Man kann immer mit dem Interface arbeiten, auch wenn man Factories verwendet. Die Fabrik respektiert die Preference-Konfiguration.

Klassen-Override via Preference

Der zweite Einsatz von Preferences ist der Klassen-Override: Eine bestehende Magento-Klasse wird durch eine eigene Implementierung ersetzt. Die neue Klasse muss die originale erweitern (per PHP-Vererbung) und kann Methoden überschreiben oder neue Methoden hinzufügen. Das ist der einzige Weg, neue öffentliche Methoden zu einer bestehenden Klasse hinzuzufügen — Plugins können das nicht, weil sie keine neuen Methoden zu einer Klasse hinzufügen können.

Ein typisches Beispiel für einen legitimen Klassen-Override: Man möchte dem Magento Customer-Modell eine neue Methode hinzufügen, die in Plugins nicht ausdrückbar ist. Man erstellt eine eigene Klasse, die Magento\Customer\Model\Customer erweitert und die neue Methode enthält. Dann setzt man in der di.xml eine Preference, die den Core-Customer durch den eigenen Customer ersetzt. Alle Code-Stellen, die Magento\Customer\Model\Customer injiziert bekommen, erhalten jetzt automatisch die erweiterte Version.

<?php
declare(strict_types=1);

namespace Mironsoft\CustomerExtension\Model;

use Magento\Customer\Model\Customer as CoreCustomer;

/**
 * Extended customer model — adds VIP tier management.
 * Used as preference for Magento\Customer\Model\Customer in di.xml.
 */
class Customer extends CoreCustomer
{
    /**
     * Check if customer has VIP status.
     * New method — not expressible via Plugin.
     */
    public function isVip(): bool
    {
        return (bool) $this->getData('is_vip');
    }

    /**
     * Get customer VIP tier level.
     * Returns: 'none', 'silver', 'gold', 'platinum'
     */
    public function getVipTier(): string
    {
        return (string) ($this->getData('vip_tier') ?? 'none');
    }

    /**
     * Set VIP tier.
     */
    public function setVipTier(string $tier): static
    {
        $this->setData('vip_tier', $tier);
        return $this;
    }
}

Klassen-Overrides haben jedoch ein erhebliches Risiko: Sie brechen bei Magento-Upgrades wenn die Core-Klasse geändert wird. Wenn Magento eine neue Methode zur Customer-Klasse hinzufügt oder die Signatur einer bestehenden Methode ändert, muss die eigene Klasse ebenfalls angepasst werden. Das ist wartungsintensiv. Plugins hingegen sind deutlich upgrade-sicherer, weil sie nicht von der vollständigen Klassen-Signatur abhängen.

Preferences vs. Plugins: Wann was verwenden?

Die Entscheidung zwischen Preference und Plugin ist eine der wichtigsten Architekturentscheidungen beim Magento 2 Modulbau. Die grundlegende Regel: Preferences für Interface-Bindungen (immer notwendig) und für das Hinzufügen neuer Methoden (wenn unbedingt nötig). Plugins für die Modifikation oder Erweiterung bestehender Methoden (der Normalfall bei Core-Erweiterungen).

Plugins sind das flexiblere und upgrade-sicherere Werkzeug. Ein Plugin kann eine Methode vor (before), nach (after) oder um (around) die originale Methode ergänzen. Mehrere Plugins für dieselbe Methode können koexistieren und werden in der Reihenfolge ihrer sortOrder ausgeführt. Das ist der entscheidende Vorteil gegenüber Preferences: Wenn zwei Module dieselbe Methode per Plugin erweitern, funktionieren beide gleichzeitig. Wenn zwei Module dieselbe Klasse per Preference überschreiben, funktioniert nur eines.

<?php
declare(strict_types=1);

namespace Mironsoft\Catalog\Plugin;

use Magento\Catalog\Model\Product;
use Magento\Catalog\Api\Data\ProductInterface;

/**
 * Product plugin — extends product functionality without Preference.
 * Multiple plugins for the same class can coexist — unlike Preferences.
 */
class ProductPlugin
{
    /**
     * After getName — append custom badge to product name in listing.
     * This is safe: other modules can also have afterGetName plugins.
     */
    public function afterGetName(
        Product $subject,
        string $result
    ): string {
        $badge = (string) $subject->getData('custom_badge');

        if ($badge !== '') {
            return sprintf('%s [%s]', $result, $badge);
        }

        return $result;
    }

    /**
     * Before save — normalize custom badge before persisting.
     */
    public function beforeSave(Product $subject): void
    {
        $badge = (string) $subject->getData('custom_badge');
        $subject->setData('custom_badge', trim(strtolower($badge)));
    }
}

Around-Plugins bieten die mächtigste Kontrolle: Sie umschließen den originalen Methodenaufruf vollständig und können sowohl vor als auch nach dem Aufruf Code ausführen, und sogar den originalen Aufruf ganz überspringen. Das sollte aber mit Bedacht eingesetzt werden, weil Around-Plugins die Performance belasten und schwer debugbar sein können. Für einfache Vor- oder Nachbearbeitungen sind Before- und After-Plugins deutlich vorzuziehen.

Preference-Konflikte zwischen zwei Modulen

Das größte Problem mit Klassen-Override-Preferences ist der Konflikt, der entsteht wenn zwei Module dieselbe Klasse überschreiben. In diesem Fall wird die Preference des Moduls verwendet, das zuletzt geladen wird — bestimmt durch die sequence-Direktive in der module.xml. Die Preference des anderen Moduls wird stillschweigend ignoriert: Der Code läuft nicht, es gibt keine Fehlermeldung und keine Warnung. Das ist eine der tückischsten Fehlerquellen in Multi-Modul-Magento-2-Projekten.

Ein typisches Szenario: Modul A eines Drittanbieters überschreibt Magento\Catalog\Model\Product mit einer eigenen Klasse. Ein zweites Modul B eines anderen Anbieters überschreibt dieselbe Klasse. Das Projekt verwendet beide Module. Je nach Ladereihenfolge funktioniert entweder A oder B — aber nie beide. Wenn Modul B zuletzt geladen wird, wird die Erweiterung von Modul A nicht ausgeführt, obwohl das Modul aktiv ist.

Die professionelle Lösung für dieses Problem ist eine Kombination aus Chaining und Plugins. Modul B sollte nicht von der Core-Klasse erben, sondern von Modul A's Klasse: class Product extends \ModulA\Catalog\Model\Product. Damit erbt Modul B automatisch die Erweiterungen von Modul A. Beide Module funktionieren, und die Ladereihenfolge spielt keine Rolle mehr — solange Modul B nach Modul A geladen wird. Die ideale Lösung ist jedoch, von vornherein Plugins statt Preferences zu verwenden, wenn Methoden-Modifikation das Ziel ist.

Magento 2.4.8 bietet mit dem Befehl bin/magento dev:di:info ein Diagnose-Tool, das aktive Preferences und Plugins für eine Klasse anzeigt. Dieser Befehl ist unverzichtbar bei der Analyse von Preference-Konflikten: Er zeigt welche Preference aktuell aktiv ist und welche Plugins auf der Klasse registriert sind. In einem Projekt mit vielen Drittanbieter-Modulen sollte dieser Befehl bei der Integration jedes neuen Moduls ausgeführt werden.

Virtual Type als Alternative zu Preferences

Virtual Types sind in bestimmten Szenarien eine elegante Alternative zu Klassen-Override-Preferences. Ein Virtual Type erstellt eine neue "Klasse" in der DI-Konfiguration, die eine bestehende Klasse mit anderen Constructor-Argumenten konfiguriert. Im Gegensatz zu einer Preference-basierten eigenen Klasse gibt es keine PHP-Erbschaftsbeziehung — es ist dieselbe PHP-Klasse, nur mit einer anderen DI-Konfiguration.

Virtual Types können als Preferences für Interfaces dienen. Das bedeutet: Man kann ein Interface nicht mit der originalen Klasse, sondern mit einem Virtual Type verknüpfen. Der Virtual Type hat dann dieselben Methoden wie die originale Klasse (weil er dieselbe PHP-Klasse ist), aber mit einer anderen Konfiguration (anderen Constructor-Argumenten). Das ist besonders nützlich wenn man dasselbe Interface in verschiedenen Kontexten unterschiedlich implementiert haben möchte.

<?xml version="1.0"?>
<!-- app/code/Mironsoft/Blog/etc/di.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">

    <!-- Virtual Type as a configured variant of a service -->
    <virtualType name="Mironsoft\Blog\Model\CachedPostRepository"
                 type="Mironsoft\Blog\Model\PostRepository">
        <arguments>
            <!-- Use cache-enabled resource model variant -->
            <argument name="cache" xsi:type="object">Magento\Framework\App\Cache\Type\Block</argument>
            <argument name="cacheTtl" xsi:type="number">3600</argument>
        </arguments>
    </virtualType>

    <!-- Use the cached variant for frontend, original for admin -->
    <!-- In etc/frontend/di.xml: -->
    <!-- <preference for="Mironsoft\Blog\Api\PostRepositoryInterface"
                    type="Mironsoft\Blog\Model\CachedPostRepository"/> -->

    <!-- In etc/di.xml (global — includes admin): -->
    <preference for="Mironsoft\Blog\Api\PostRepositoryInterface"
                type="Mironsoft\Blog\Model\PostRepository"/>

    <!-- Standard interface bindings -->
    <preference for="Mironsoft\Blog\Api\Data\PostInterface"
                type="Mironsoft\Blog\Model\Post"/>

    <preference for="Mironsoft\Blog\Api\Data\PostSearchResultsInterface"
                type="Mironsoft\Blog\Model\PostSearchResults"/>

</config>

Virtual Types haben keine Auswirkung auf andere Module, die dieselbe Klasse per Plugin erweitern. Die Plugins bleiben aktiv, weil der Virtual Type dieselbe PHP-Klasse ist. Das ist ein Vorteil gegenüber einer Preference-basierten Klasse, bei der Plugins aus anderen Modulen möglicherweise nicht greifen, wenn die Klassen-Hierarchie verändert wurde. Virtual Types sind in dieser Hinsicht kooperativer im Multi-Modul-Umfeld.

Bereichsspezifische Preferences

Magento 2 ermöglicht es, Preferences für verschiedene Anwendungsbereiche (Areas) unterschiedlich zu konfigurieren. Das ermöglicht es, für den Frontend-Bereich eine andere Implementierung eines Interfaces zu verwenden als für den Admin-Bereich oder die REST-API. Diese Flexibilität ist in bestimmten Szenarien sehr nützlich: etwa wenn eine gecachte Implementierung für den Frontend-Bereich und eine nicht gecachte für den Admin-Bereich gewünscht ist.

Die Konfiguration erfolgt in bereichsspezifischen di.xml-Dateien: etc/frontend/di.xml gilt nur für Frontend-Requests, etc/adminhtml/di.xml nur für Admin-Requests, etc/webapi_rest/di.xml nur für REST-API-Requests und etc/webapi_soap/di.xml für SOAP-API-Requests. Die globale etc/di.xml gilt für alle Bereiche, wird aber durch bereichsspezifische Konfigurationen überschrieben, wenn der Request in einem bestimmten Bereich ausgeführt wird.

Bereichsspezifische Preferences sind besonders relevant für Performance-Optimierungen. Caching-Layer, die im Frontend sinnvoll sind, können im Admin störend sein, weil Admin-Operationen immer aktuelle Daten benötigen. Eine bereichsspezifische Preference für das Frontend auf eine gecachte Repository-Implementierung, während die globale Preference auf die nicht gecachte Implementierung zeigt, löst dieses Problem elegant und ohne komplexe Laufzeit-Bedingungen im Code.

Nach jeder Preference-Änderung — ob global oder bereichsspezifisch — muss der DI-Container neu kompiliert werden. bin/magento setup:di:compile liest alle di.xml-Dateien, löst Preferences auf, generiert Interceptoren für Plugins und schreibt die resultierende Konfiguration in den generated/-Ordner. Ohne diesen Schritt ist eine Preference-Änderung in der Produktionsumgebung nicht aktiv. Im Developer-Mode kompiliert Magento automatisch on-demand, aber für die Produktionsumgebung ist der explizite Compile-Schritt unerlässlich.

Zusammenfassung: Preferences in Magento 2

Preferences sind für Interface-Bindungen unverzichtbar. Für Klassen-Overrides nur wenn neue Methoden hinzugefügt werden müssen. Für Methoden-Modifikation immer Plugins bevorzugen — sie sind konfliktfrei kombinierbar. Preference-Konflikte zwischen zwei Modulen sind die gefährlichste Fehlerquelle. Virtual Types können Preferences ergänzen oder ersetzen wenn kein PHP-Code-Duplikat benötigt wird.

Interface Binding

Immer notwendig: for="...Interface" type="...Implementation". Interfaces können nicht instanziiert werden — Preference ist Pflicht.

Klassen-Override

Nur wenn neue Methoden hinzugefügt werden. Konflikte bei 2 Modulen. Neue Klasse muss Original erweitern und vererben.

Plugin bevorzugen

Für Methoden-Modifikation immer Plugin. Mehrere Plugins kombinierbar — Preferences überschreiben sich gegenseitig. Upgrade-sicher.

Debugging

bin/magento dev:di:info ClassName zeigt aktive Preference. Nach Änderung: setup:di:compile + cache:flush.

Mironsoft

Preferences und Plugins korrekt einsetzen?

Wir entwickeln Magento 2 Module mit korrektem Einsatz von Preferences für Interface-Bindungen und Plugins für Erweiterungen — upgrade-sicher, konfliktfrei und mit vollständigem Code-Review.

DI-Analyse

Preference-Konflikte erkennen und durch Plugins ersetzen

Service Contracts

Interface-Architektur mit korrekten Preference-Bindungen

Plugin-Entwicklung

Before-, Around- und After-Plugins für Core-Erweiterungen

FAQ: Preferences in Magento 2

1 Was ist eine Preference in Magento 2?

Teilt dem DI-Container mit, welche konkrete Klasse für ein Interface oder eine andere Klasse verwendet werden soll. Zwei Szenarien: Interface Binding (immer notwendig wenn Interfaces injiziert werden) und Class Override (neue Methoden hinzufügen, mit Vorsicht).

2 Wann muss ich eine Preference definieren?

Immer wenn ein Interface per Constructor Injection verwendet wird. Ohne Preference: DI-Exception. Für eigene Module immer: RepositoryInterface → Repository, DataInterface → Model, SearchResultsInterface → SearchResults.

3 Preference oder Plugin — was wählen?

Preference: neue Methoden hinzufügen oder Interface binden. Plugin: vorhandene Methoden modifizieren. Plugins sind konfliktfrei kombinierbar, mehrere Module können dieselbe Methode per Plugin erweitern. Preferences überschreiben sich.

4 Was ist Preference-Konflikt zwischen zwei Modulen?

Zwei Module setzen Preference für dieselbe Klasse. Je nach sequence in module.xml gewinnt eine Preference. Die andere wird still ignoriert. Lösung: Modul B erbt von Modul A's Klasse, oder beide nutzen Plugins.

5 Was ist Interface-first Design in Magento 2?

Zuerst Interface definieren, dann Implementierung. Andere Module kennen nur das Interface. Implementierung über Preference austauschbar ohne Breaking Changes. Api/Data/ für Data Interfaces, Api/ für Repository-Interfaces.

6 Wie debugge ich die aktive Preference?

bin/magento dev:di:info 'Klasse' zeigt aktive Preference und Plugins. Xdebug-Breakpoint in verdächtiger Klasse — Stack Trace zeigt geladene Klasse. Im generated/code/ nach Compile inspizieren.

7 Kann Virtual Type eine Preference ersetzen?

Virtual Types können als Preference für Interfaces dienen: <preference for="Interface" type="VirtualTypeName"/>. Sie können Klassen-Overrides nicht ersetzen wenn neue PHP-Methoden benötigt werden. Für Konfigurations-Varianten einer Klasse sind Virtual Types besser als Preference.

8 Wie definiert man bereichsspezifische Preferences?

etc/adminhtml/di.xml für Admin. etc/frontend/di.xml für Frontend. etc/webapi_rest/di.xml für REST. Bereichsspezifisch hat Vorrang vor globalem etc/di.xml. Ideal für gecachte vs. nicht gecachte Implementierungen.

9 Kann Preference eine finale Klasse überschreiben?

Nein. final class kann nicht erweitert werden. Magento markiert einige Core-Klassen als final um unkontrolliertes Überschreiben zu verhindern. Für finale Klassen: Around-Plugin oder Komposition statt Vererbung.

10 Was muss man nach Preference-Änderung tun?

bin/magento setup:di:compile + cache:flush. In Dev-Mode reicht oft cache:flush. In Production immer explizit di:compile vor Go-Live. Ohne Compile ist die Preference-Änderung in Production nicht aktiv.