Registry Pattern: Legacy-Anti-Pattern oder noch nützlich?

· Lesezeit: ca. 12 Minuten · Kategorie: Magento 2 · Design Patterns

REG
global
Magento 2 · Vergleich · Design Patterns

Registry Pattern:
Legacy oder noch nützlich?

Das Registry Pattern war in Magento 1 allgegenwärtig und ist in Magento 2 deprecated. Wann war es sinnvoll, welche Probleme es verursacht und was du stattdessen verwenden sollst.

⏱ 12 Min. Vergleich Design Patterns PHP 8.4

Ein Pattern das seine Zeit hatte

Wer alten Magento-Code liest, findet es überall: Mage::registry('current_product') in Magento 1, $this->registry->register('current_category', $category) in frühen Magento-2-Modulen. Das Registry Pattern war jahrelang der Standard für das Weitergeben von Objekten zwischen Controllern und Blöcken.

Seit Magento 2.3 ist Magento\Framework\Registry offiziell deprecated. Die Migration läuft langsam – viele Third-Party-Module nutzen die Registry noch. Aber in neuem Code hat sie nichts verloren.

Dieser Artikel erklärt: Was das Registry Pattern leistet, warum es problematisch ist, und welche modernen Alternativen für jeden Use Case existieren.

1. Was ist das Registry Pattern?

Das Registry Pattern (auch bekannt als Service Registry) ist ein globaler Schlüssel-Wert-Speicher der zur Laufzeit Objekte bereitstellt. Es ist das bewusst einfachere Gegenstück zu einem DI-Container: kein Typsystem, keine Konfiguration – einfach set und get mit einem String-Key.


Registry Pattern – Kernkonzept:

┌─────────────────────────────────────────────┐
│              Registry (global)               │
│                                             │
│  'current_product'  →  Product $product     │
│  'current_category' →  Category $category   │
│  'cms_page'         →  Page $cmsPage        │
└─────────────────────────────────────────────┘
       ↑                         ↓
  Controller schreibt       Block liest
  (register)                (registry)

Problem: Implizite Kopplung über String-Keys.
Weder Schreiber noch Leser wissen voneinander.

In klassischen Frameworks wie Symfony oder Laravel gibt es dieses Pattern nicht – die Datenweitergabe erfolgt über explizite Controller-Parameter. In Magento war es die pragmatische Lösung für ein spezifisches Architekturproblem: Block-Klassen kennen ihren Controller nicht und können keine Daten direkt empfangen.

2. Magento Registry: Die Implementierung

Die Implementierung in Magento 2 ist simpel:


<?php
// vendor/magento/framework/Registry.php (vereinfacht)
// @deprecated since 2.3.0 in favor of data providers.
namespace Magento\Framework;

class Registry
{
    private array $registry = [];

    /**
     * Store a value in the registry by key.
     */
    public function register(string $key, mixed $value, bool $graceful = false): void
    {
        if (isset($this->registry[$key])) {
            if ($graceful) {
                return;
            }
            throw new \RuntimeException('Registry key "' . $key . '" already exists.');
        }
        $this->registry[$key] = $value;
    }

    /**
     * Retrieve a value by key.
     */
    public function registry(string $key): mixed
    {
        return $this->registry[$key] ?? null;
    }

    /**
     * Remove a value from the registry.
     */
    public function unregister(string $key): void
    {
        unset($this->registry[$key]);
    }
}

Der vollständige Code sind etwa 60 Zeilen – keine Typsicherheit, keine Abhängigkeitsverwaltung, keine Lebenszyklussteuerung. Ein globales Array mit String-Keys.

3. Wo Magento Core die Registry verwendet

Trotz Deprecation ist die Registry noch in weiten Teilen des Magento-Cores aktiv. Die häufigsten Stellen:


Aktive Registry-Nutzung in Magento Core (Stand 2.4.8):

'current_product'
  → Gesetzt von: Catalog\Controller\Product\View (nach Repository-Laden)
  → Gelesen von: Catalog\Block\Product\View, Pricing-Blöcken, Review-Blöcken
  → Warum noch aktiv: Sehr viele Third-Party-Module abhängig davon

'current_category'
  → Gesetzt von: Catalog\Controller\Category\View
  → Gelesen von: Catalog\Block\Category\View, Layered Navigation

'current_cms_page'
  → Gesetzt von: Cms\Controller\Page\View
  → Gelesen von: Cms\Block\Page

'isSecureArea'
  → Gesetzt von: Admin-Controllern für sensitive Operationen
  → Gelesen von: Model-Klassen die unterschiedliches Verhalten in secure areas haben

'use_page_cache_plugin'
  → Gesetzt von: Full Page Cache Mechanismus
  → Gelesen von: Cache-Entscheidungslogik

Wichtig: Nur weil Magento Core die Registry noch nutzt, heißt das nicht, dass du es in neuem Code nutzen sollst. Der Core befindet sich in schrittweiser Migration. Neue Module sollen Registry nicht mehr verwenden.

4. Die Probleme des Registry Patterns


<?php
// Problem 1: Keine Typsicherheit — PHPStan kann nicht prüfen
$product = $this->registry->registry('current_product');
// Typ ist 'mixed' — kein IDE-Support, keine statische Analyse möglich
// Könnte null sein, könnte falsches Objekt sein, könnte String sein

// Problem 2: Implizite Abhängigkeit zwischen Controller und Block
// Controller:
$this->registry->register('current_product', $product);
// Block (woanders im Code):
$product = $this->registry->registry('current_product');
// Reihenfolge muss stimmen — wenn Controller nicht gelaufen ist: null!
// Kein Compiler prüft das.

// Problem 3: String-Key-Konflikte
// Modul A registriert 'current_product' mit einem anderen Objekt-Typ
// Modul B liest 'current_product' und erwartet ProductInterface
// → ClassCastException oder null-Fehler zur Laufzeit

// Problem 4: Tests sind komplex
class ProductBlockTest extends \PHPUnit\Framework\TestCase
{
    public function testGetProduct(): void
    {
        $registry = new \Magento\Framework\Registry();
        $product = $this->createMock(\Magento\Catalog\Api\Data\ProductInterface::class);
        $registry->register('current_product', $product);

        $block = new ProductBlock($context, $registry);
        // Test muss Registry-State manuell setzen — fehleranfällig
        // Was wenn ein anderer Test 'current_product' bereits registriert hat?
    }
}

5. Alternative 1: ViewModel Pattern

Für die häufigste Registry-Nutzung (Objekt aus Controller an Block weitergeben) ist das ViewModel Pattern die beste Alternative. Der ViewModel holt die Daten selbst aus dem Repository:


<?php
declare(strict_types=1);

namespace Mironsoft\Catalog\ViewModel;

use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Framework\App\RequestInterface;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Framework\View\Element\Block\ArgumentInterface;

/**
 * ViewModel replaces Registry for current product access.
 * No global state — data fetched explicitly from repository.
 */
class CurrentProduct implements ArgumentInterface
{
    private ?ProductInterface $product = null;

    public function __construct(
        private readonly ProductRepositoryInterface $productRepository,
        private readonly RequestInterface $request
    ) {}

    /**
     * Returns the current product based on request parameter.
     * Lazy-loaded and internally cached for the request lifecycle.
     */
    public function getProduct(): ?ProductInterface
    {
        if ($this->product !== null) {
            return $this->product;
        }

        $productId = (int) $this->request->getParam('id');
        if ($productId === 0) {
            return null;
        }

        try {
            $this->product = $this->productRepository->getById($productId);
        } catch (NoSuchEntityException) {
            return null;
        }

        return $this->product;
    }
}

<!-- Layout-XML: ViewModel statt Registry -->
<!-- app/code/Mironsoft/Catalog/view/frontend/layout/catalog_product_view.xml -->
<page>
    <body>
        <referenceBlock name="product.info">
            <arguments>
                <argument name="view_model" xsi:type="object">
                    Mironsoft\Catalog\ViewModel\CurrentProduct
                </argument>
            </arguments>
        </referenceBlock>
    </body>
</page>

<?php
// Template: $viewModel statt $this->registry->registry('current_product')
/** @var \Mironsoft\Catalog\ViewModel\CurrentProduct $viewModel */
$product = $viewModel->getProduct();
if ($product === null) {
    return;
}
?>
<h1><?= $block->escapeHtml($product->getName()) ?></h1>

6. Alternative 2: RequestInterface

Wenn nur einfache Werte (IDs, Strings) zwischen Controller und Klassen weitergegeben werden müssen, ist RequestInterface die direkteste Alternative:


<?php
// Statt:
// Controller: $this->registry->register('current_post_id', $postId);
// Block:      $postId = $this->registry->registry('current_post_id');

// Besser: RequestInterface direkt
class PostViewModel implements ArgumentInterface
{
    public function __construct(
        private readonly RequestInterface $request,
        private readonly PostRepositoryInterface $postRepository
    ) {}

    public function getPost(): ?PostInterface
    {
        $postId = (int) $this->request->getParam('id');
        // Der Post-ID ist bereits im Request — kein globaler State nötig!
        try {
            return $this->postRepository->getById($postId);
        } catch (NoSuchEntityException) {
            return null;
        }
    }
}
// Controller muss die ID gar nicht "weitergeben" —
// sie steckt ohnehin im Request-Objekt.

7. Alternative 3: Constructor Injection

Wenn Daten zwischen Services (nicht zwischen Controller und Block) geteilt werden müssen, ist Constructor Injection mit einem dedizierten State-Objekt die sauberste Lösung:


<?php
declare(strict_types=1);

namespace Mironsoft\Checkout\Model;

use Magento\Quote\Api\Data\CartInterface;

/**
 * Stateful service holding current checkout context.
 * Injected as shared service — same instance across request.
 * Replaces registry('current_quote') pattern.
 */
class CheckoutContext
{
    private ?CartInterface $quote = null;

    public function setQuote(CartInterface $quote): void
    {
        $this->quote = $quote;
    }

    public function getQuote(): ?CartInterface
    {
        return $this->quote;
    }
}

// Service A setzt den Quote:
class CheckoutController
{
    public function __construct(
        private readonly CheckoutContext $context,
        private readonly CartRepositoryInterface $cartRepository
    ) {}

    public function execute(): ResultInterface
    {
        $quote = $this->cartRepository->getActiveForCustomer($customerId);
        $this->context->setQuote($quote); // Explizit, typsicher
        return $this->pageFactory->create();
    }
}

// Service B liest den Quote:
class CheckoutSummaryViewModel implements ArgumentInterface
{
    public function __construct(
        private readonly CheckoutContext $context
    ) {}

    public function getQuote(): ?CartInterface
    {
        return $this->context->getQuote(); // Typsicher, testbar
    }
}

8. Migration: Bestehende Registry-Aufrufe ersetzen


Migrations-Strategie je Registry-Use-Case:

registry('current_product')   → ViewModel mit ProductRepository + Request
registry('current_category')  → ViewModel mit CategoryRepository + Request
registry('current_cms_page')  → ViewModel mit PageRepository + Request
registry('isSecureArea')      → Expliziter Flag in dedizierten Services
registry('custom_data')       → Dedizierter State-Service (injectable, shared)

Migrations-Schritte:
1. grep -r "->registry(" app/code/Mironsoft/ — alle Stellen finden
2. Für jeden Treffer: welcher Use Case liegt vor?
3. ViewModel erstellen, Registry-Aufruf entfernen
4. Layout-XML anpassen (view_model argument hinzufügen)
5. Tests schreiben — jetzt ohne Registry-State möglich
6. registry->register() im Controller entfernen

9. Wann ist Registry noch vertretbar?

In neuem Code: niemals. In bestehendem Legacy-Code gibt es einen Fall in dem kurzfristig abgewogen werden kann:

  • Third-Party-Module-Integration: Wenn ein Third-Party-Modul die Registry explizit schreibt und du dessen Block-System nutzt, kann das kurzfristige Lesen unvermeidbar sein – bis das Modul migriert ist.
  • Schrittweise Migration: Wenn ein großes Modul migriert wird, kann es sinnvoll sein, Registry-Nutzung schrittweise zu eliminieren statt alles auf einmal zu ändern.

Klare Linie: In allen neuen Klassen, in neuen Modulen und in jeder Klasse die aus anderen Gründen ohnehin refaktoriert wird: Registry komplett eliminieren. Kein neues register() oder registry() in neuem Code.

Mironsoft

Magento 2 Modernisierung & Migration

Registry-Code in deinem Magento modernisieren?

Wir analysieren dein Magento-Projekt auf Registry-Abhängigkeiten und ersetzen sie durch moderne ViewModels, RequestInterface und Injectable Services – vollständig mit Tests.

Registry-Audit
Alle Registry-Aufrufe in deinen Modulen finden, kategorisieren und Migrations-Plan erstellen.
ViewModel-Migration
Registry durch ViewModels ersetzen: sauber, typsicher, testbar und kompatibel mit Hyvä Theme.
Test-Abdeckung
PHPUnit-Tests für alle migrierten Klassen – ohne Registry-State, ohne Magento-Bootstrap.

10. Zusammenfassung

Das Registry Pattern war in Magento 1 unvermeidbar und in frühem Magento 2 pragmatisch. Seit Magento 2.3 ist es deprecated – mit gutem Grund: fehlende Typsicherheit, implizite Kopplung und Testprobleme machen es zum Anti-Pattern. Moderne Alternativen sind ViewModel Pattern, direkte Repository-Aufrufe via RequestInterface und Injectable State-Services.

Registry vs. Alternativen – Vergleich

Registry (deprecated)

Globaler Key-Value-Store. Keine Typsicherheit. Implizite Kopplung über String-Keys. Reihenfolgeabhängig. Schwer testbar. In neuem Code: niemals verwenden.

ViewModel (empfohlen)

Holt Daten direkt aus Repository. Typsicher, testbar, kein globaler State. Injiziert via Layout-XML. Standard für Hyvä Theme.

RequestInterface

Für IDs und Parameter die im Request stecken. Kein "Weiterleiten" nötig — ID steht im Request, ViewModel liest sie direkt.

Injectable State-Service

Für komplexe Request-übergreifende Zustände: dedizierter Service mit setX()/getX(). Als Shared Object injiziert — gleiche Instanz im gesamten Request.

11. FAQ: Registry Pattern in Magento 2

1 Ist die Magento Registry komplett deprecated?
Seit Magento 2.3 als @deprecated markiert. Core nutzt sie intern noch, wird schrittweise migriert. In eigenem Code — neuen Modulen, neuen Klassen — nicht mehr verwenden.
2 Wie finde ich alle Registry-Aufrufe in meinen Modulen?
grep -r '->registry(' app/code/Mironsoft/ und grep -r '->register(' app/code/Mironsoft/. PHPStan mit Magento-Plugin meldet Registry-Nutzung als Deprecation-Warning.
3 Darf ich Registry für eigene Modul-Kommunikation nutzen?
Nein. Alternativen: Events/Observer für lose Kopplung, Service Contracts für direkte Kommunikation, Injectable State-Services. String-Keys kollidieren — Typen sind unbekannt.
4 Unterschied registry() vs. register()?
register(key, value) schreibt (typischerweise im Controller). registry(key) liest (typischerweise im Block). $graceful=true unterdrückt die Exception bei bereits existierendem Key.
5 Wie migriere ich registry('current_product') zu ViewModel?
1. ViewModel mit ArgumentInterface erstellen. 2. ProductRepository + Request injizieren. 3. getProduct() liest ID aus Request, lädt aus Repository. 4. Layout-XML: view_model argument hinzufügen. 5. Template anpassen. 6. register() im Controller entfernen.
6 Muss register() vor registry() aufgerufen werden?
Ja — das ist das Kernproblem. Block-Rendering passiert nach Controller::execute() — funktioniert in Praxis, aber keine technische Garantie. Fehlt der register()-Aufruf: null ohne Fehlermeldung. ViewModels lösen das — sie holen Daten aktiv.
7 Performance-Unterschied Registry vs. ViewModel?
ViewModel macht zusätzlichen Repository-Aufruf — aber Repository-Calls sind gecacht. In der Praxis kein messbarer Unterschied. Die Vorteile (Typsicherheit, Testbarkeit) überwiegen bei weitem.
8 Wie teste ich ViewModel vs. Registry-Code?
ViewModel: createMock(ProductRepositoryInterface::class) + createMock(RequestInterface::class) → fertig. Registry: Objekt erstellen, manuell befüllen, dann Block instanziieren — mehr Boilerplate, mehr Fehlerquellen.
9 Was ist 'isSecureArea' in der Registry?
Wird vom Admin-Bereich gesetzt, erlaubt sensitive Operationen (z.B. Bild-Löschen beim Produkt-Löschen). Noch aktiver Core-Code — nicht anfassen. Beispiel für Legacy-Registry-Nutzung die noch nicht migriert ist.
10 Darf ich Registry für temporäre Test-Daten nutzen?
Nein. Für Tests: Mocks via createMock(), Fixtures mit Integration-Tests. In Integrationstests nur wenn Core-Verhalten getestet wird das noch Registry-abhängig ist — als Workaround, nicht als gute Praxis.