mironsoft.deBlogDesign Patterns
Magento 2 · Design Patterns
Singleton & Registry
Pattern in Magento 2

Das Singleton Pattern garantiert genau eine Objektinstanz. Magento 2 implementiert das über Shared Instances im DI-Container — elegant, testbar und ohne globale Kopplung. Die Registry-Klasse als globaler Datentopf ist seit 2.3 deprecated. Dieser Artikel erklärt warum und wie man es besser macht.

12 Min. Lesezeit PHP 8.4 Magento 2.4.8

Das GoF Singleton Pattern — Konzept und PHP-Implementierung

Das Singleton Pattern gehört zu den bekanntesten Entwurfsmustern aus dem 1994 erschienenen Buch "Design Patterns: Elements of Reusable Object-Oriented Software" der Gang of Four. Es löst ein spezifisches Problem: Es soll garantiert werden, dass von einer bestimmten Klasse genau eine Instanz existiert, und es soll einen globalen Zugriffspunkt auf diese Instanz geben. Typische Kandidaten für Singletons sind Logger, Konfigurationsobjekte, Verbindungspools und Registry-artige Datenbehälter.

Die klassische PHP-Implementierung des Singleton Patterns nutzt drei Kernelemente: Erstens einen privaten Konstruktor, der verhindert, dass externe Klassen eine neue Instanz erstellen. Zweitens eine statische Property $instance, die die einzige existierende Instanz speichert. Drittens eine öffentliche statische Methode getInstance(), die beim ersten Aufruf die Instanz erstellt und bei allen weiteren dieselbe zurückgibt.

Zusätzlich werden in einer korrekten PHP-Singleton-Implementierung auch __clone() und __wakeup() als privat oder geschützt deklariert, um das Klonen und das Deserialisieren der Instanz zu verhindern — beides würde die Singleton-Garantie unterlaufen. In PHP 8.0+ kann man readonly und andere moderne Features nutzen, das grundlegende Muster bleibt jedoch gleich.

Das GoF-Singleton war zur Zeit seiner Erfindung ein elegantes Muster für Sprachen ohne echtes Dependency Injection. Heute, in einer Welt moderner DI-Container, ist das klassische Singleton weitgehend überholt — nicht weil das Konzept falsch wäre, sondern weil die Implementierung spezifische Probleme mit sich bringt, die moderne Frameworks eleganter lösen.

<?php
declare(strict_types=1);

// Klassisches PHP Singleton — konzeptionell korrekt, aber in Magento 2 NICHT verwenden
class LegacyConfigRegistry
{
    private static ?self $instance = null;
    private array $data = [];

    // Privater Konstruktor: keine externe Instanziierung möglich
    private function __construct() {}

    // Klonen verboten — würde zweite Instanz erzeugen
    private function __clone(): void {}

    // Deserialisieren verboten — würde zweite Instanz erzeugen
    public function __wakeup(): void
    {
        throw new \RuntimeException('Singletons cannot be deserialized.');
    }

    /**
     * Get the singleton instance — created on first call.
     */
    public static function getInstance(): self
    {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    public function get(string $key): mixed
    {
        return $this->data[$key] ?? null;
    }

    public function set(string $key, mixed $value): void
    {
        $this->data[$key] = $value;
    }
}

// Verwendung — Anti-Pattern in Magento 2:
$config = LegacyConfigRegistry::getInstance();
$config->set('base_url', 'https://example.com');

Warum das klassische Singleton in modernem PHP problematisch ist

Das größte Problem des klassischen Singleton Patterns ist seine statische Natur. getInstance() ist eine statische Methode, die auf globalen Zustand zugreift. Statische Aufrufe können in PHP-Unit-Tests nicht ohne weiteres überschrieben oder gemockt werden. Das bedeutet: Jede Klasse, die ein Singleton verwendet, hat eine versteckte Abhängigkeit auf globalen State, die in isolierten Unit Tests nicht entfernt werden kann. Tests werden zu Integrationstests ohne Isolation.

Das zweite Problem ist die Test-Kontamination zwischen Test-Cases. Da das Singleton eine statische Instanz hält, überlebt dessen State von einem Test zum nächsten — innerhalb desselben PHP-Prozesses. Wenn Test A den Singleton-State verändert, sieht Test B diesen veränderten State. Das führt zu zufälligen Testfehlern, die von der Ausführungsreihenfolge abhängen — eine der schlimmsten Klassen von Bugs in automatisierten Testsystemen.

Das dritte Problem betrifft die Austauschbarkeit. Ein klassisches Singleton ist für alle Aufrufer dieselbe Klasse. Es gibt keine Möglichkeit, für einen bestimmten Kontext eine andere Implementierung zu verwenden — etwa eine spezialisierte Implementierung für bestimmte Store Views oder eine leichtgewichtige Implementierung für CLI-Kontexte. DI-Container lösen das elegant über Preferences und Virtual Types.

In Magento 2 sind diese Probleme bekannt und der Framework-Kern verwendet das klassische statische Singleton bewusst nicht. Stattdessen wird das Singleton-Konzept über den DI-Container implementiert — mit allen Vorteilen des Konzepts (eine Instanz pro Request, gemeinsamer Zustand) ohne dessen Nachteile (statische Kopplung, keine Testbarkeit). Das Ergebnis nennt sich Shared Instance.

Shared Instances: Magento's elegante Singleton-Alternative

Magento 2 implementiert das Singleton-Konzept über den DI-Container. Wenn der ObjectManager zum ersten Mal eine Injectable Klasse auflöst, erstellt er die Instanz und speichert sie in einem internen $sharedInstances-Array. Alle nachfolgenden Anfragen nach dieser Klasse — ob über Constructor Injection oder über ObjectManager::get() — erhalten dieselbe Instanz zurück. Das ist Singleton-Verhalten, ohne die problematischen Eigenschaften des klassischen Patterns.

Der entscheidende Unterschied zum klassischen Singleton: Die Shared Instance ist nicht auf Klassenebene erzwungen, sondern vom DI-Container gesteuert. In Unit Tests erstellt man einfach eine neue Instanz der Klasse mit new — es gibt keinen privaten Konstruktor der das verhindert. Der Konstruktor ist öffentlich, alle Abhängigkeiten können als Mock-Objekte übergeben werden. Der DI-Container sorgt im laufenden System für die Singleton-Semantik, ohne sie in den Test-Kontext zu erzwingen.

Ein weiterer Vorteil: Shared Instances können über die preference-Konfiguration in di.xml ausgetauscht werden. Wenn ein Modul eine andere Implementierung eines Interfaces verwenden soll, reicht ein Eintrag in di.xml — alle Klassen, die das Interface injiziert bekommen, erhalten automatisch die neue Implementierung. Das klassische Singleton bietet keinen vergleichbaren Mechanismus.

In Magento 2.4.8 sind alle Services (Repositories, Logger, SearchCriteriaBuilder, Session-Objekte, EventManager) standardmäßig Shared Instances. Das bedeutet: Egal wie viele Klassen einen bestimmten Service injizieren — es existiert nur eine Instanz pro Request. Das spart Speicher, vermeidet redundante Initialisierungen und stellt sicher, dass alle Teile des Systems denselben Zustand eines Services sehen — ohne einen globalen Singleton-Zugriffspunkt zu benötigen.

<?php
declare(strict_types=1);

namespace Mironsoft\Blog\ViewModel;

use Magento\Framework\View\Element\Block\ArgumentInterface;
use Mironsoft\Blog\Api\PostRepositoryInterface;
use Magento\Framework\Api\SearchCriteriaBuilder;

/**
 * Blog post list ViewModel.
 * PostRepositoryInterface is a shared instance — one instance per request,
 * injected into every class that depends on it. No Singleton boilerplate needed.
 */
class PostList implements ArgumentInterface
{
    public function __construct(
        // Shared instance: the DI container ensures only one exists per request
        private readonly PostRepositoryInterface $postRepository,
        private readonly SearchCriteriaBuilder $searchCriteriaBuilder
    ) {}

    /**
     * Get the latest published blog posts.
     *
     * @return \Mironsoft\Blog\Api\Data\PostInterface[]
     */
    public function getLatestPosts(int $limit = 5): array
    {
        $searchCriteria = $this->searchCriteriaBuilder
            ->addFilter('status', 'published')
            ->setPageSize($limit)
            ->setCurrentPage(1)
            ->create();

        return $this->postRepository->getList($searchCriteria)->getItems();
    }
}

shared=true vs. shared=false in di.xml — wann was?

Der shared-Parameter in di.xml steuert, ob der DI-Container für einen Typ Singleton-Verhalten aktiviert oder deaktiviert. Der Standard für alle Injectable Classes ist shared="true" — diese Klassen werden als Shared Instances verwaltet. Man muss diesen Wert also nur explizit setzen, wenn man vom Standard abweichen möchte.

shared="false" wird für Non-Injectable Objects gesetzt — also für Klassen, die ihren eigenen, nicht teilbaren Zustand halten. Das betrifft in Magento vor allem Entities (Product, Quote, Order, Customer, Address), Collections und Value Objects. Diese Klassen dürfen nicht als Shared Instance behandelt werden, weil sie Datenbankdaten repräsentieren, die für jeden Kontext unterschiedlich sind.

In der Praxis werden Non-Shared-Klassen nicht direkt injiziert, sondern über generierte Factory-Klassen instanziiert. Wenn man in einer Klasse ProductFactory injiziert und $this->productFactory->create() aufruft, erstellt die Factory intern jedes Mal eine neue Product-Instanz. Die Factory selbst ist eine Shared Instance — das Product nicht. Dieses Muster ist die korrekte Magento-Umsetzung des Factory-Patterns für Non-Injectable Objects.

Ein häufiges Missverständnis: Auch Virtual Types in di.xml können mit shared konfiguriert werden. Wenn man mehrere Varianten desselben Services mit unterschiedlichen Konstruktor-Argumenten benötigt (etwa zwei Logger mit verschiedenen Kanälen), erstellt man Virtual Types — diese erben automatisch das shared-Verhalten der Elternklasse, können es aber überschreiben.

<!-- di.xml: shared-Konfiguration explizit steuern -->
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">

    <!-- SHARED (Standard, kann weggelassen werden): eine Instanz pro Request -->
    <type name="Mironsoft\Blog\Model\PostRepository" shared="true"/>

    <!-- NON-SHARED: jede Injection bekommt neue Instanz — selten nötig, da Factories genutzt werden -->
    <type name="Mironsoft\Blog\Model\Post" shared="false"/>

    <!-- Virtual Type: verschiedene Logger-Instanzen mit unterschiedlichen Kanälen -->
    <virtualType name="MironsoftBlogLogger" type="Magento\Framework\Logger\Monolog">
        <arguments>
            <argument name="name" xsi:type="string">MironsoftBlog</argument>
            <argument name="handlers" xsi:type="array">
                <item name="system" xsi:type="object">Magento\Framework\Logger\Handler\System</item>
            </argument>
        </arguments>
    </virtualType>

    <!-- Inject the virtual type (shared by default) -->
    <type name="Mironsoft\Blog\Model\PostService">
        <arguments>
            <argument name="logger" xsi:type="object">MironsoftBlogLogger</argument>
        </arguments>
    </type>

</config>

Das Registry Pattern: globaler Datentopf und seine Geschichte

Das Registry Pattern implementiert einen zentralen, global zugänglichen Datenspeicher — einen Key-Value-Store auf Anwendungsebene. Objekte können unter einem Schlüssel registriert und von jeder anderen Komponente unter demselben Schlüssel abgerufen werden. Das Pattern löst ein reales Problem: Wie übergibt man Kontext-Daten — etwa das aktuell angezeigte Produkt oder die aktuelle Kategorie — durch mehrere Schichten der Anwendung, ohne sie als Parameter durch alle Funktionsaufrufe schleifen zu müssen?

In Magento 2.0 bis 2.2 war Magento\Framework\Registry die Standardlösung für dieses Problem. Der Controller initialisierte das aktuelle Produkt und registrierte es unter dem Schlüssel current_product. Blocks, Observers und Helpers lasen es aus dem Registry. Dasselbe Muster galt für current_category, current_order und viele weitere Kontextobjekte.

Das Muster war in Magento 1 weit verbreitet — dort hieß der globale Datentopf Mage::registry() und Mage::register(). Beim Übergang zu Magento 2 wurde die globale Funktion durch eine Injectable-Klasse ersetzt — Magento\Framework\Registry — aber das fundamentale Problem des globalen States blieb bestehen.

Die Klasse Registry bot zwei Kernmethoden: register(string $key, mixed $value) zum Speichern und registry(string $key) zum Abrufen. Die Methode unregister() ermöglichte das Entfernen von Einträgen. Da die Registry selbst eine Shared Instance war, war ihr Zustand global für den gesamten Request sichtbar — was das Problem des globalen States nicht löste, sondern lediglich hinter einer Injectable-Klasse versteckte.

Warum Magento\Framework\Registry deprecated ist

Ab Magento 2.3.x wurde Magento\Framework\Registry offiziell als deprecated markiert. Die Gründe sind vielfältig und fundamental. Das erste und schwerwiegendste Problem: Wer $this->registry->registry('current_product') aufruft, deklariert keine Abhängigkeit auf das Produkt — nur auf die Registry. Welche Daten die Registry im konkreten Moment enthält, ist aus der Klasse nicht ersichtlich. Der eigentliche Vertrag der Klasse — "ich brauche ein Produkt" — ist versteckt.

Das zweite Problem ist die fehlende Typsicherheit. Registry::registry() gibt mixed zurück. IDEs können keine Typ-Hinweise geben, PHPStan kann keine Typ-Fehler erkennen, und Entwickler müssen aus dem Kontext erraten, welchen Typ der Registry-Wert hat. In PHP 8.4 mit strikter Typisierung ist das ein erheblicher Rückschritt — man verlässt die Sicherheit des Typsystems genau dort, wo Typen am wichtigsten sind.

Das dritte Problem ist die bereits erwähnte Test-Kontamination. Da die Registry eine Shared Instance ist und ihr State global im Speicher verbleibt, kontaminiert ein Registry-register()-Aufruf in einem Test alle folgenden Tests im selben PHP-Prozess. In der Magento-Testsuite muss deshalb nach jedem Test der Registry-State manuell bereinigt werden — das ist fehleranfällig und verlangsamt die Testentwicklung erheblich.

Das vierte Problem betrifft die Erweiterbarkeit und die Wartbarkeit von Magento-Code. Wenn viele Module denselben Registry-Schlüssel current_product lesen, gibt es eine implizite Kopplung zwischen dem Schreiber (dem Controller) und allen Lesern. Wenn der Controller das Produkt nicht setzt oder unter einem anderen Schlüssel registriert, brechen alle abhängigen Klassen — aber ohne einen klaren Fehler. Der Schlüssel ist ein Magic String, der nicht von Typ-Checking-Tools überprüft werden kann.

Registry-Alternativen: Context-Objekte, Session und ViewModel-Injection

Der Ersatz für Registry-Nutzungen hängt davon ab, welche Art von Daten gespeichert und in welchem Kontext sie gelesen werden. Die häufigste und sauberste Alternative für Request-Kontextdaten ist eine typisierte Context-Klasse als Shared Instance. Diese Klasse ist eine einfache DTO-ähnliche Klasse mit typierten set()- und get()-Methoden. Da sie eine Shared Instance ist, sehen alle Klassen, die sie injizieren, denselben Zustand — genau wie bei der Registry, aber typsicher und deklariert.

Für Session-Daten, die über mehrere Requests erhalten bleiben müssen, sind Magento's Session-Objekte die richtige Wahl: Magento\Checkout\Model\Session, Magento\Customer\Model\Session oder ein eigenes Session-Objekt. Diese werden per Constructor Injection deklariert und sind vollständig typsicher. In der Hyvä-Welt werden viele dieser Daten über Alpine.js-State und API-Calls geladen, was Session-Abhängigkeiten im PHP-Layer reduziert.

Für Produktseiten und Kategoriepages, bei denen das aktuelle Objekt im Controller geladen wird, gibt es in Magento 2.4.8 oft die Möglichkeit, das Objekt über den Request-Kontext zu laden. Eine Context-Klasse, die im Observer oder Controller-Plugin gesetzt wird, ist die sauberste Lösung. Alternativ kann ein ViewModel das Objekt direkt über einen Repository-Aufruf laden, wenn die Produktseite ohnehin einen Request-Parameter enthält.

In manchen Fällen ist die Registry-Nutzung ein Zeichen dafür, dass Daten zu früh im Request geladen und zu lange durch das System getragen werden. Eine Alternative ist Lazy Loading: Der ViewModel lädt das Produkt selbst, wenn er es braucht, anstatt auf einen extern gesetzten Zustand zu warten. In Magento 2.4.8 mit vollständigem Page Cache ist das oft die bessere Strategie — die Produktdaten werden für jede Seite ohnehin gecacht.

<?php
declare(strict_types=1);

namespace Mironsoft\Catalog\Model\Context;

use Magento\Catalog\Api\Data\ProductInterface;

/**
 * Typed context holder for the current product in the request scope.
 * Registered as shared="true" (default) in di.xml.
 * Replaces: $registry->registry('current_product')
 */
class CurrentProduct
{
    private ?ProductInterface $product = null;

    /**
     * Set the current product (called from Controller or Observer).
     */
    public function set(ProductInterface $product): void
    {
        $this->product = $product;
    }

    /**
     * Get the current product — null if not set in this request scope.
     */
    public function get(): ?ProductInterface
    {
        return $this->product;
    }

    /**
     * Check whether a current product has been set.
     */
    public function has(): bool
    {
        return $this->product !== null;
    }

    /**
     * Clear the current product context.
     */
    public function clear(): void
    {
        $this->product = null;
    }
}

Migrationsstrategie: Registry-Nutzungen systematisch ablösen

Die Migration von Registry-Nutzungen zu sauberen Alternativen sollte systematisch erfolgen. Der erste Schritt ist eine vollständige Inventur aller Registry-Aufrufe im Projekt. Mit grep -rn 'registry->' src/app/code und grep -rn "use Magento\\Framework\\Registry" src/app/code findet man alle Vorkommen. PHPStan mit entsprechenden Regeln kann diese Suche automatisieren und als Teil des CI/CD-Prozesses kontinuierlich ausführen.

Der zweite Schritt ist die Kategorisierung der gefundenen Registry-Nutzungen. Handelt es sich um Produktkontext (current_product, current_category)? Dann ist eine typisierte Context-Klasse die richtige Wahl. Handelt es sich um temporäre Berechnungsergebnisse, die innerhalb eines Requests weitergegeben werden? Dann ist eine Shared-Instance-Klasse mit dem entsprechenden Typ die Alternative. Handelt es sich um Session-Daten? Dann sollte das Session-Objekt direkt verwendet werden.

Der dritte Schritt ist das tatsächliche Refactoring: eine Context-Klasse pro Registry-Schlüssel erstellen, die Registry-Injection durch die Context-Klassen-Injection ersetzen, und alle register()-Aufrufe durch set()-Aufrufe der Context-Klasse ersetzen. Der vierte Schritt ist das Schreiben von Unit Tests für alle refaktorierten Klassen — diesmal ohne Registry-State-Probleme.

Für Teams, die in Magento 2.4.8 aktiv entwickeln, empfiehlt sich eine Null-Toleranz-Policy für neue Registry-Nutzungen: In Code-Reviews wird jeder use Magento\Framework\Registry-Import in neuen Klassen sofort abgelehnt. PHPStan-Regeln in der CI/CD-Pipeline sorgen dafür, dass keine neuen Registry-Aufrufe unbemerkt eingeführt werden. Bestehende Registry-Nutzungen werden im Zuge anderer Arbeiten an den betreffenden Modulen schrittweise abgelöst.

<?php
declare(strict_types=1);

namespace Mironsoft\Catalog\Observer;

use Magento\Framework\Event\ObserverInterface;
use Magento\Framework\Event\Observer;
use Magento\Catalog\Api\Data\ProductInterface;
use Mironsoft\Catalog\Model\Context\CurrentProduct;

/**
 * Sets the current product context on catalog_controller_product_init_after.
 * Replaces: $registry->register('current_product', $product)
 */
class SetCurrentProductContext implements ObserverInterface
{
    public function __construct(
        private readonly CurrentProduct $currentProduct
    ) {}

    /**
     * Set the current product in the typed context class (shared instance).
     * All other classes that inject CurrentProduct will see the same instance.
     */
    public function execute(Observer $observer): void
    {
        $product = $observer->getEvent()->getProduct();
        if ($product instanceof ProductInterface) {
            $this->currentProduct->set($product);
        }
    }
}

// In ViewModel: typisiert, no magic strings, vollständig mockbar in Tests
class ProductDetailViewModel implements \Magento\Framework\View\Element\Block\ArgumentInterface
{
    public function __construct(
        private readonly CurrentProduct $currentProduct
    ) {}

    public function getProduct(): ?ProductInterface
    {
        // Type-safe, IDE-supported, no Registry needed
        return $this->currentProduct->get();
    }

    public function getProductName(): string
    {
        return $this->currentProduct->get()?->getName() ?? '';
    }
}

Singleton & Registry — Das Wichtigste auf einen Blick

Shared Instance = modernes Singleton

Injectable Objects sind per Default shared="true". Eine Instanz pro Request. Testbar, austauschbar via Preferences, kein statischer Zugriff.

Registry deprecated seit 2.3

Magento\Framework\Registry ist in neuen Modulen verboten. Globaler State, keine Typsicherheit, Test-Kontamination.

Context-Klassen als Ersatz

Typisierte Shared-Instance-Klasse mit set() und get(). Observer setzt, ViewModel liest. Vollständig typsicher, mockbar, isoliert testbar.

shared=false für Entities

Product, Quote, Order: shared="false". Werden über Factory-Klassen instanziiert — niemals direkt injiziert.

Mironsoft

Magento 2 Legacy-Modernisierung & DI-Architektur

Legacy-Registry-Code ablösen?

Wir analysieren Registry-Nutzung in Magento 2 Projekten und ersetzen sie durch typsichere, testbare Context-Klassen mit korrekter DI-Anbindung — upgrade-sicher für Magento 2.5+ und bereit für PHP 8.4.

Registry-Analyse

Alle Registry-Aufrufe finden, bewerten und migrationsreif dokumentieren

Context-Klassen

Typsichere Shared-Instance-Klassen als sauberer Ersatz für Registry

Unit Tests

Testabdeckung für alle migrierten Klassen — keine Registry-Kontamination

Zusammenfassung

Das Singleton Pattern ist in Magento 2 nicht verschwunden — es wurde modernisiert. Shared Instances im DI-Container liefern exakt das, was das klassische GoF-Singleton verspricht: eine einzige Instanz pro Kontext, global zugänglich. Aber ohne die problematischen Eigenschaften: statische Kopplung, fehlende Testbarkeit und Nicht-Austauschbarkeit. Die Konfiguration über shared="true" in di.xml macht das Singleton-Verhalten explizit konfigurierbar und per Preference austauschbar.

Die Registry-Klasse ist eine Fehlentscheidung aus der frühen Magento-2-Entwicklung, die aus der Magento-1-Ära übernommen wurde. Sie ist deprecated, weil sie globalen, untypisierten State in ein System einführt, das auf expliziten Abhängigkeiten und Typsicherheit aufgebaut ist. In Magento 2.4.8 gibt es für jeden Registry-Anwendungsfall eine bessere Alternative: typisierte Context-Klassen, Session-Objekte oder direkte Repository-Aufrufe im ViewModel.

Der Weg zu sauberem Magento 2 Code führt über drei Schritte: Klassisches Singleton durch Shared Instances ersetzen, Registry-Nutzungen durch typisierte Context-Klassen ablösen und shared=false in di.xml für Entities nutzen, die über Factory-Klassen instanziiert werden. Das Ergebnis ist testbarer, wartbarer und upgrade-sicherer Code — bereit für die nächsten Magento-Major-Versionen.

FAQ: Singleton & Registry in Magento 2

1 Was ist das Singleton Pattern nach GoF?
Das GoF-Singleton garantiert genau eine Instanz einer Klasse mit globalem Zugriffspunkt. Klassisch implementiert über privaten Konstruktor und statische getInstance()-Methode. In Magento 2 wird das Konzept über Shared Instances im DI-Container umgesetzt — ohne statische Kopplung, vollständig testbar und austauschbar.
2 Wie implementiert Magento 2 das Singleton-Konzept?
Über Shared Instances im DI-Container. Injectable Objects werden im $sharedInstances-Array verwaltet. Jede Constructor Injection liefert dieselbe Instanz. Keine statische Kopplung, kein getInstance(), vollständig testbar durch normales Instanziieren im Unit Test.
3 Was ist Magento\Framework\Registry und warum ist es deprecated?
Globaler Key-Value-Speicher für Request-Kontextdaten. Deprecated seit Magento 2.3.x wegen: unsichtbarer Abhängigkeiten, Test-Kontamination, fehlender Typsicherheit (gibt mixed zurück) und Verstöße gegen das Dependency Inversion Principle. In neuen Modulen verboten.
4 Was ist der Unterschied zwischen shared=true und shared=false in di.xml?
shared="true" (Standard): Alle Injections erhalten dieselbe Instanz — Singleton-Verhalten. shared="false": Jede Injection bekommt eine neue Instanz. Für Entities (Product, Quote) die eigenen Zustand halten — diese werden über Factory-Klassen instanziiert.
5 Wie ersetzt man Registry::registry('current_product')?
Mit einer typisierten Context-Klasse als Shared Instance: CurrentProduct::set(ProductInterface) im Observer, CurrentProduct::get() im ViewModel. Typsicher, mockbar, keine Test-Kontamination.
6 Kontaminiert die Registry Unit Tests?
Ja. Registry ist eine Shared Instance mit globalem State pro PHP-Prozess. Ein register() in Test A kontaminiert Test B. Context-Klassen sind per new CurrentProduct() im Test immer frisch — keine Kontamination.
7 Wann ist shared=false in di.xml sinnvoll?
Für Non-Injectable Objects (Entities, Collections) die eigenen nicht teilbaren Zustand halten: Product, Quote, Order, Customer. Diese werden über Factory::create() instanziiert — niemals direkt injiziert.
8 Gibt es noch Registry-Nutzung im Magento 2.4.8 Core?
Ja, als Legacy in älteren Core-Modulen (Catalog, Checkout). Schrittweise migriert. In eigenen Modulen und Extensions verboten. PHPStan-Regeln können Registry-Aufrufe als Warnings oder Errors markieren.
9 Wie tauscht man eine Shared Instance via di.xml aus?
Über preference in di.xml: <preference for="OriginalInterface" type="Mironsoft\Replacement"/>. DI-Container injiziert automatisch die neue Klasse überall. Für partielle Erweiterungen: Plugin statt vollständiger Preference verwenden.
10 Wie verhindert man Registry-Nutzung in neuen Modulen?
PHPStan mit magento/magento-coding-standard aktivieren. Im Code-Review: use Magento\Framework\Registry in neuen Klassen sofort ablehnen. Stattdessen Context-Klassen, Session-Objekte oder direkte Repository-Aufrufe im ViewModel.