Registry Pattern: Legacy Anti-Pattern or Still Useful?
· Reading time: about 12 minutes · Category: Magento 2 · Design Patterns
Registry Pattern:
Legacy or still useful?
The Registry Pattern was omnipresent in Magento 1 and is deprecated in Magento 2. When it made sense, which problems it causes and what you should use instead.
A pattern that had its time
Anyone reading old Magento code finds it everywhere: Mage::registry('current_product') in Magento 1, $this->registry->register('current_category', $category) in early Magento 2 modules. The Registry Pattern was for years the standard way to pass objects between controllers and blocks.
Since Magento 2.3, Magento\Framework\Registry is officially deprecated. The migration is proceeding slowly, many third-party modules still use the registry. But in new code it has no business being used.
This article explains what the Registry Pattern does, why it is problematic, and which modern alternatives exist for every use case.
- 1. What is the Registry Pattern?
- 2. Magento Registry: the implementation
- 3. Where Magento core uses the registry
- 4. The problems with the Registry Pattern
- 5. Alternative 1: ViewModel Pattern
- 6. Alternative 2: RequestInterface
- 7. Alternative 3: Constructor Injection
- 8. Migration: replacing existing registry calls
- 9. When is registry still defensible?
- 10. Summary
- 11. FAQ
1. What is the Registry Pattern?
The Registry Pattern (also known as Service Registry) is a global key-value store that provides objects at runtime. It is the deliberately simpler counterpart to a DI container: no type system, no configuration, just set and get with a string key.
Registry Pattern, core concept:
┌─────────────────────────────────────────────┐
│ Registry (global) │
│ │
│ 'current_product' → Product $product │
│ 'current_category' → Category $category │
│ 'cms_page' → Page $cmsPage │
└─────────────────────────────────────────────┘
↑ ↓
Controller writes Block reads
(register) (registry)
Problem: implicit coupling via string keys.
Neither the writer nor the reader know about each other.
Classic frameworks like Symfony or Laravel do not have this pattern, data is passed through explicit controller parameters instead. In Magento it was the pragmatic solution for a specific architectural problem: block classes do not know their controller and cannot receive data directly.
2. Magento Registry: the implementation
The implementation in Magento 2 is simple:
<?php
// vendor/magento/framework/Registry.php (simplified)
// @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]);
}
}
The full code is about 60 lines, no type safety, no dependency management, no lifecycle control. A global array with string keys.
3. Where Magento core uses the registry
Despite deprecation, the registry is still active in large parts of Magento core. The most common spots:
Active registry usage in Magento core (as of 2.4.8):
'current_product'
→ Set by: Catalog\Controller\Product\View (after repository load)
→ Read by: Catalog\Block\Product\View, pricing blocks, review blocks
→ Why still active: many third-party modules depend on it
'current_category'
→ Set by: Catalog\Controller\Category\View
→ Read by: Catalog\Block\Category\View, layered navigation
'current_cms_page'
→ Set by: Cms\Controller\Page\View
→ Read by: Cms\Block\Page
'isSecureArea'
→ Set by: admin controllers for sensitive operations
→ Read by: model classes with different behavior in secure areas
'use_page_cache_plugin'
→ Set by: full page cache mechanism
→ Read by: cache decision logic
Important: Just because Magento core still uses the registry does not mean you should use it in new code. Core is undergoing a gradual migration. New modules should not use the registry anymore.
4. The problems with the Registry Pattern
<?php
// Problem 1: No type safety, PHPStan cannot check it
$product = $this->registry->registry('current_product');
// Type is 'mixed', no IDE support, no static analysis possible
// Could be null, could be the wrong object, could be a string
// Problem 2: Implicit dependency between controller and block
// Controller:
$this->registry->register('current_product', $product);
// Block (somewhere else in the code):
$product = $this->registry->registry('current_product');
// Order must be correct, if the controller has not run: null!
// No compiler checks this.
// Problem 3: String key conflicts
// Module A registers 'current_product' with a different object type
// Module B reads 'current_product' and expects ProductInterface
// → ClassCastException or null error at runtime
// Problem 4: Tests are complex
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 has to set registry state manually, error-prone
// What if another test already registered 'current_product'?
}
}
5. Alternative 1: ViewModel Pattern
For the most common registry use case (passing an object from controller to block), the ViewModel Pattern is the best alternative. The ViewModel fetches the data itself from the 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 instead of 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 instead of $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
When only simple values (IDs, strings) need to be passed between the controller and other classes, RequestInterface is the most direct alternative:
<?php
// Instead of:
// Controller: $this->registry->register('current_post_id', $postId);
// Block: $postId = $this->registry->registry('current_post_id');
// Better: RequestInterface directly
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');
// The post ID is already in the request, no global state needed!
try {
return $this->postRepository->getById($postId);
} catch (NoSuchEntityException) {
return null;
}
}
}
// The controller does not need to "pass along" the ID at all,
// it is already in the request object.
7. Alternative 3: Constructor Injection
When data needs to be shared between services (not between controller and block), Constructor Injection with a dedicated state object is the cleanest solution:
<?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 sets the 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); // Explicit, type safe
return $this->pageFactory->create();
}
}
// Service B reads the quote:
class CheckoutSummaryViewModel implements ArgumentInterface
{
public function __construct(
private readonly CheckoutContext $context
) {}
public function getQuote(): ?CartInterface
{
return $this->context->getQuote(); // Type safe, testable
}
}
8. Migration: replacing existing registry calls
Migration strategy per registry use case:
registry('current_product') → ViewModel with ProductRepository + Request
registry('current_category') → ViewModel with CategoryRepository + Request
registry('current_cms_page') → ViewModel with PageRepository + Request
registry('isSecureArea') → Explicit flag in dedicated services
registry('custom_data') → Dedicated state service (injectable, shared)
Migration steps:
1. grep -r "->registry(" app/code/Mironsoft/ (find all spots)
2. For each match: which use case is it?
3. Create ViewModel, remove the registry call
4. Adjust layout XML (add view_model argument)
5. Write tests (now possible without registry state)
6. Remove registry->register() from the controller
9. When is registry still defensible?
In new code: never. In existing legacy code there is one case where a short-term trade-off can be weighed:
- Third-party module integration: When a third-party module explicitly writes to the registry and you use its block system, reading it briefly may be unavoidable, until the module is migrated.
- Gradual migration: When a large module is being migrated, it can make sense to eliminate registry usage step by step instead of changing everything at once.
Clear line: In every new class, in new modules and in any class being refactored for other reasons anyway: eliminate the registry entirely. No new register() or registry() in new code.
Mironsoft
Magento 2 Modernization & Migration
Modernize registry code in your Magento?
We analyze your Magento project for registry dependencies and replace them with modern ViewModels, RequestInterface and injectable services, fully covered with tests.
10. Summary
The Registry Pattern was unavoidable in Magento 1 and pragmatic in early Magento 2. Since Magento 2.3 it is deprecated, for good reason: missing type safety, implicit coupling and testing problems make it an anti-pattern. Modern alternatives are the ViewModel Pattern, direct repository calls via RequestInterface, and injectable state services.
Registry vs. alternatives, comparison
Registry (deprecated)
Global key-value store. No type safety. Implicit coupling via string keys. Order dependent. Hard to test. In new code: never use it.
ViewModel (recommended)
Fetches data directly from the repository. Type safe, testable, no global state. Injected via layout XML. Standard for Hyva Theme.
RequestInterface
For IDs and parameters already contained in the request. No "passing along" needed, the ID is in the request, the ViewModel reads it directly.
Injectable state service
For complex state that spans a request: a dedicated service with setX()/getX(). Injected as a shared object, same instance throughout the entire request.
11. FAQ: Registry Pattern in Magento 2
1 Is the Magento Registry completely deprecated?
@deprecated since Magento 2.3. Core still uses it internally and is being migrated gradually. In your own code, new modules, new classes, do not use it anymore.2 How do I find all registry calls in my modules?
grep -r '->registry(' app/code/Mironsoft/ and grep -r '->register(' app/code/Mironsoft/. PHPStan with the Magento plugin reports registry usage as a deprecation warning.3 Am I allowed to use registry for my own module communication?
4 Difference between registry() and register()?
register(key, value) writes (typically in the controller). registry(key) reads (typically in the block). $graceful=true suppresses the exception when the key already exists.5 How do I migrate registry('current_product') to a ViewModel?
6 Must register() be called before registry()?
null with no error message. ViewModels solve this, they actively fetch data.7 Performance difference between Registry and ViewModel?
8 How do I test ViewModel vs. registry code?
createMock(ProductRepositoryInterface::class) + createMock(RequestInterface::class), done. Registry: create the object, populate it manually, then instantiate the block, more boilerplate, more sources of error.9 What is 'isSecureArea' in the registry?
10 Can I use the registry for temporary test data?
createMock(), fixtures with integration tests. Only in integration tests where core behavior that still depends on registry is being tested, and even then as a workaround, not good practice.