Service Locator Pattern: Why ObjectManager Is Evil
· Reading time: approx. 13 minutes · Category: Magento 2 · Design Patterns
Service Locator:
Why ObjectManager Is Evil
ObjectManager::getInstance()->get() hides dependencies, makes testing impossible, and blocks static analysis. Everything about the Service Locator anti-pattern, and the correct alternative.
The Problem in One Line
There is one line of code in Magento projects that should immediately trigger a code review:
$service = \Magento\Framework\App\ObjectManager::getInstance()->get(SomeService::class);
This one line is the Service Locator Pattern, and it is an anti-pattern in almost all modern PHP projects. Magento explicitly documents it as forbidden for custom code. Still, it shows up regularly in third-party modules, older code, and Stack Overflow answers.
This deep dive explains: what the Service Locator Pattern is, why ObjectManager is the textbook example of it, which concrete problems it causes, and how Constructor Injection replaces it completely.
- 1. What Is the Service Locator Pattern?
- 2. ObjectManager: Magento's Service Locator
- 3. Problem 1: Hidden Dependencies
- 4. Problem 2: Not Testable
- 5. Problem 3: No Static Analysis
- 6. Problem 4: Security Risks
- 7. Constructor Injection: The Correct Alternative
- 8. Legitimate Exceptions: When ObjectManager Is Allowed
- 9. Detecting and Fixing Service Locator Anti-Patterns
- 10. Summary
- 11. FAQ
1. What Is the Service Locator Pattern?
The Service Locator Pattern describes a central registry where code actively fetches its own dependencies instead of having them injected. It is the exact opposite of Dependency Injection:
Dependency Injection (correct):
Container → inject → Service
The dependency is provided from outside
The class declares its requirements
Service Locator (anti-pattern):
Service → locate → Container → return Dependency
The class fetches what it needs itself
Dependencies are hidden
Analogy:
DI = the restaurant brings the food to the table
Service Locator = the guest walks into the kitchen and looks for the food themselves
The pattern was described by Martin Fowler as "Service Locator vs. Dependency Injection". Fowler emphasizes that both solve the problem of dependency management, but Service Locator hides the dependencies.
2. ObjectManager: Magento's Service Locator
Magento's ObjectManager is a fully fledged DI container. As such, it is necessary and legitimate, it is used internally by the framework to resolve all DI dependencies. The problem arises when code calls the ObjectManager directly:
<?php
// The ObjectManager interface is deliberately minimal:
interface ObjectManagerInterface
{
public function get(string $type): object; // Shared object (singleton pool)
public function create(string $type, array $arguments = []): object; // New instance
}
// Direct call = Service Locator anti-pattern:
$om = \Magento\Framework\App\ObjectManager::getInstance();
// All of the following calls are anti-patterns:
$config = $om->get(\Magento\Framework\App\Config\ScopeConfigInterface::class);
$product = $om->create(\Magento\Catalog\Model\Product::class);
$repository = $om->get(\Magento\Catalog\Api\ProductRepositoryInterface::class);
// Why? Because the class that does this no longer makes it clear
// from its constructor signature what it actually needs.
<?php
// Example: a service that looks like a black box
class OrderProcessor
{
// No constructor, no visible dependencies!
public function processOrder(int $orderId): bool
{
$om = \Magento\Framework\App\ObjectManager::getInstance();
// These dependencies are INVISIBLE to the caller:
$order = $om->get(OrderRepositoryInterface::class)->get($orderId);
$emailSender = $om->get(OrderSender::class);
$logger = $om->get(LoggerInterface::class);
$config = $om->get(ScopeConfigInterface::class);
$event = $om->get(ManagerInterface::class);
// ... processing code
return true;
}
}
// Callers of OrderProcessor have no idea:
// - What OrderProcessor needs
// - What side effects it has
// - How to mock it
3. Problem 1: Hidden Dependencies
The most serious problem: classes that use Service Locator have hidden dependencies. No code review, no IDE, and no static analysis tool can determine what the class truly needs:
<?php
// With Service Locator: what does this class actually need?
class InventoryChecker
{
public function isInStock(int $productId): bool
{
// Buried somewhere deep in the code...
$stockItem = \Magento\Framework\App\ObjectManager::getInstance()
->get(\Magento\CatalogInventory\Api\StockRegistryInterface::class)
->getStockItem($productId);
return $stockItem->getIsInStock();
}
}
// Answer: you don't know without reading the entire method body.
// In larger classes: impossible without a full code analysis.
// With Constructor Injection: instant clarity
class InventoryChecker
{
public function __construct(
private readonly \Magento\CatalogInventory\Api\StockRegistryInterface $stockRegistry
) {}
// ↑ Visible: this class needs StockRegistry. That's all.
public function isInStock(int $productId): bool
{
return $this->stockRegistry->getStockItem($productId)->getIsInStock();
}
}
// Dependency in the constructor: visible, documented, enforced.
4. Problem 2: Not Testable
This is the most serious practical drawback. Code with direct ObjectManager access cannot be tested without a Magento bootstrap:
<?php
// Service Locator: a PHPUnit test is impossible without a bootstrap
class InventoryCheckerTest extends \PHPUnit\Framework\TestCase
{
public function testIsInStock(): void
{
$checker = new InventoryChecker();
// PROBLEM: InventoryChecker calls ObjectManager::getInstance()
// ObjectManager is not initialized in this context
// → Fatal Error: ObjectManager not initialized
// → Only solvable with a full Magento bootstrap (slow, complex)
$result = $checker->isInStock(42);
$this->assertTrue($result);
}
}
// Constructor Injection: the test just works
class InventoryCheckerTest extends \PHPUnit\Framework\TestCase
{
public function testIsInStock(): void
{
$stockItem = $this->createMock(\Magento\CatalogInventory\Api\Data\StockItemInterface::class);
$stockItem->method('getIsInStock')->willReturn(true);
$stockRegistry = $this->createMock(\Magento\CatalogInventory\Api\StockRegistryInterface::class);
$stockRegistry->method('getStockItem')->with(42)->willReturn($stockItem);
$checker = new InventoryChecker($stockRegistry); // Inject the mock!
$this->assertTrue($checker->isInStock(42));
}
}
// No bootstrap. No database. No Magento installation.
// The test runs in milliseconds.
5. Problem 3: No Static Analysis
<?php
// Service Locator: PHPStan/Psalm cannot analyze the dependencies
class BadService
{
public function doSomething(): void
{
$repo = ObjectManager::getInstance()->get('Mironsoft\Blog\Api\PostRepositoryInterface');
// PHPStan: the return type is 'object', no type checking possible
// $repo->getById() → PHPStan has no idea whether the method exists!
$post = $repo->getById(1);
}
}
// Constructor Injection: PHPStan analyzes it completely
class GoodService
{
public function __construct(
private readonly PostRepositoryInterface $postRepository // Type known!
) {}
public function doSomething(): void
{
$post = $this->postRepository->getById(1);
// PHPStan: method exists ✓, return type is PostInterface ✓
// Typo in the method name? PHPStan reports it immediately.
}
}
// PHPStan rule for Magento (phpstan-magento):
// Automatically reports ObjectManager::getInstance() calls as errors.
6. Problem 4: Security Risks
Service Locator, combined with dynamic class names, can lead to security problems:
<?php
// DANGEROUS: dynamic class names from user input
class DangerousFactory
{
public function create(string $type): object
{
// type comes from user input or a database!
return \Magento\Framework\App\ObjectManager::getInstance()->get($type);
// An attacker could instantiate arbitrary classes
// → Potential remote code execution if classes have side effects
}
}
// Constructor Injection + allowlist: no such risk
class SafeFactory
{
private const ALLOWED_TYPES = [
'product' => ProductModel::class,
'order' => OrderModel::class,
];
public function __construct(
private readonly ProductFactory $productFactory,
private readonly OrderFactory $orderFactory
) {}
public function create(string $type): mixed
{
return match ($type) {
'product' => $this->productFactory->create(),
'order' => $this->orderFactory->create(),
default => throw new \InvalidArgumentException('Unknown type: ' . $type),
};
}
}
7. Constructor Injection: The Correct Alternative
Constructor Injection is the complete solution. All dependencies are declared in the constructor, and the DI container resolves them automatically:
<?php
declare(strict_types=1);
namespace Mironsoft\Blog\Model;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\Event\ManagerInterface as EventManager;
use Mironsoft\Blog\Api\PostRepositoryInterface;
use Psr\Log\LoggerInterface;
/**
* Order processor with explicit, injectable dependencies.
* All dependencies visible in constructor, no hidden state.
*/
class PostPublisher
{
public function __construct(
private readonly PostRepositoryInterface $postRepository,
private readonly ScopeConfigInterface $scopeConfig,
private readonly EventManager $eventManager,
private readonly LoggerInterface $logger
) {
// Constructor Property Promotion (PHP 8.0+)
// All dependencies are visible, type-safe, mockable
}
/**
* Publish a blog post.
* All dependencies explicitly declared, testable without ObjectManager.
*/
public function publish(int $postId): bool
{
try {
$post = $this->postRepository->getById($postId);
$post->setStatus(PostInterface::STATUS_PUBLISHED);
$this->postRepository->save($post);
$this->eventManager->dispatch('mironsoft_blog_post_published', ['post' => $post]);
$this->logger->info('Post published', ['id' => $postId]);
return true;
} catch (\Exception $e) {
$this->logger->error('Publish failed', ['id' => $postId, 'error' => $e->getMessage()]);
return false;
}
}
}
8. Legitimate Exceptions: When ObjectManager Is Allowed
Magento defines explicit exceptions where direct ObjectManager access is permitted:
Legitimate direct ObjectManager access:
1. Generated factory classes (generated/code/):
ObjectManager::create() inside generated *Factory classes
→ These are generated automatically by Magento, not written by hand
2. Proxy classes (generated/code/):
ObjectManager::get() inside generated *Proxy classes
→ Automatically generated for lazy loading
3. Setup scripts (Setup/Patch):
In InstallData, UpgradeData, DataPatchInterface
→ The constructor has a different interface, normal DI is not available
4. Test bootstrap:
Integration test setup (Magento\TestFramework\Helper\Bootstrap)
→ Explicit test context, documented as an exception
5. pub/index.php and bootstrap:
Initialization of the container itself
→ Necessary bootstrapping code
ObjectManager::getInstance() is NOT allowed in any other case!
9. Detecting and Fixing Service Locator Anti-Patterns
<?php
// Common anti-patterns and their fixes:
// Anti-pattern 1: ObjectManager::getInstance()
// Wrong:
$config = \Magento\Framework\App\ObjectManager::getInstance()->get(ScopeConfigInterface::class);
// Correct:
class MyService {
public function __construct(private readonly ScopeConfigInterface $scopeConfig) {}
}
// Anti-pattern 2: ObjectManager as a constructor dependency
// Wrong:
class BadBlock extends Template {
public function __construct(
Context $context,
private readonly ObjectManagerInterface $objectManager // FORBIDDEN!
) { parent::__construct($context); }
public function getService(): MyService {
return $this->objectManager->get(MyService::class);
}
}
// Correct: inject MyService directly
// Anti-pattern 3: static helper method with ObjectManager
// Wrong:
class Helper {
public static function getConfig(string $path): string {
return \Magento\Framework\App\ObjectManager::getInstance()
->get(ScopeConfigInterface::class)->getValue($path);
}
}
// Correct: turn the helper into an injectable service with Constructor Injection
// Anti-pattern 4: ObjectManager in an event observer
// Wrong:
class MyObserver implements ObserverInterface {
public function execute(Observer $observer): void {
$product = \Magento\Framework\App\ObjectManager::getInstance()
->create(\Magento\Catalog\Model\Product::class);
}
}
// Correct: inject ProductFactory via the constructor
# Find Service Locator anti-patterns in the code:
grep -r "ObjectManager::getInstance" app/code/
grep -r "ObjectManagerInterface" app/code/ | grep -v "use Magento"
# PHPStan with the phpstan-magento extension:
vendor/bin/phpstan analyse app/code/Mironsoft --level=6
# Automatically reports direct ObjectManager access as an error
Mironsoft
Magento 2 Code Quality & Refactoring
Remove ObjectManager Anti-Patterns From Your Code?
We find every Service Locator anti-pattern in your Magento modules and refactor them to Constructor Injection, with PHPStan integration and full test coverage.
10. Summary
The Service Locator Pattern with ObjectManager::getInstance() is the most common anti-pattern in Magento projects. It hides dependencies, makes code untestable without a Magento bootstrap, blocks static analysis, and can create security risks. Constructor Injection solves all of these problems, and it is the only correct way to handle dependencies in Magento classes.
Service Locator vs. Constructor Injection
Service Locator (Anti-Pattern)
Hidden dependencies. Not testable without a bootstrap. No static analysis. Violates Single Responsibility. Forbidden in custom code.
Constructor Injection (Correct)
Explicit dependencies. Fully testable with mocks. Analyzable by PHPStan/Psalm. Follows the Dependency Inversion Principle. Standard practice in Magento 2.
Legitimate Exceptions
Generated factories, proxies, setup scripts, test bootstrap, pub/index.php. Not allowed in any other context.
Detecting and Fixing
grep -r "ObjectManager::getInstance" plus PHPStan with phpstan-magento. Refactoring: move all dependencies into the constructor, let the DI container handle the rest.
11. FAQ: Service Locator Pattern in Magento 2
1 Why is ObjectManager::getInstance() forbidden?
2 get() vs. create() in ObjectManager?
get(): shared instance (singleton pool). create(): always a new instance (non-shared). Both are forbidden in custom code, use Constructor Injection or Factory::create() instead.3 How do I migrate legacy ObjectManager code?
grep -r 'ObjectManager::getInstance' app/code/. 2. For each occurrence: add the class as a constructor parameter. 3. Replace the direct call. 4. Write a test that runs without a bootstrap. 5. Remove the old code.4 Can I inject ObjectManagerInterface?
5 What if I have too many constructor dependencies?
6 PHPStan setup for detecting ObjectManager calls?
composer require --dev bitExpert/phpstan-magento then configure it in phpstan.neon, set level 6 or higher, and it automatically reports ObjectManager::getInstance() as an error.7 Are third-party modules with ObjectManager safe?
grep -r 'ObjectManager::getInstance' vendor/VendorName/.