Service Locator & ObjectManager in Magento 2
AI generated
mironsoft.deBlogDesign Patterns
Magento 2 · Design Patterns
Service Locator &
ObjectManager in Magento 2

The ObjectManager is the Service Locator pattern in Magento 2, and at the same time the platform's best known anti-pattern. This article explains why using it directly in production code is forbidden, which exceptions are legitimate, and what correct Dependency Injection looks like.

13 min read PHP 8.4 Magento 2.4.8

What is the Service Locator pattern?

The Service Locator pattern is a design pattern for resolving dependencies at runtime. A central service locator object knows every registered service and returns it on request. Instead of declaring dependencies in the constructor, the class calls the locator inside its methods: $service = ServiceLocator::getInstance()->get('mailer'). The class receives the service without instantiating it itself.

At first glance this looks practical: you avoid maintaining long constructor parameter lists and can swap services flexibly. In practice, though, the downsides massively outweigh the benefits. The first and most serious one: anyone looking at a class from the outside, say during code review or while writing tests, cannot see which dependencies the class needs. They do not appear in the constructor. You have to read the entire method body to find every service locator call.

The Service Locator pattern shows up in many forms: as a global registry object, as a static utility class, or as a dependency injection container that is called directly. In Magento 2, the best known manifestation is calling ObjectManager::getInstance() directly. That call is the Magento-specific service locator, and it is strictly forbidden in production code.

It is important to distinguish the Service Locator pattern from a Dependency Injection container. Both are containers that manage dependencies. The crucial difference lies in how they are used: a DI container proactively injects dependencies into the constructor, the class does not have to do anything. A service locator provides services only on explicit request, the class has to actively ask for them and thereby takes on a dependency to the locator itself. Magento's DI container is a valid DI container; calling the ObjectManager directly turns it into a service locator.

The role of the ObjectManager in Magento 2

The Magento\Framework\ObjectManager is the heart of the Magento 2 dependency injection system. It is responsible for instantiating every object in the system, resolves constructor dependencies automatically, manages shared instances (the object pool pattern), and processes the DI configuration from di.xml files. Internally, the ObjectManager is the foundation the entire framework is built on.

Magento itself makes heavy internal use of the ObjectManager. The bootstrap process, request routing, the plugin system and factory creation all run through the ObjectManager. When Magento runs setup:di:compile, it analyzes every class, resolves dependency chains and generates optimized PHP code that largely bypasses the ObjectManager during regular operation, which is why Magento runs faster in production mode than in developer mode.

The public class Magento\Framework\App\ObjectManager exposes a global access point through getInstance(). That global access point is the problem. It gives external code the ability to misuse the DI container as a service locator, and this misuse is widespread in the Magento community, especially in older code and quickly written extensions.

Interestingly, it is precisely the power of the ObjectManager that makes using it directly so tempting. With a single line of code you get access to any service in the system without having to touch a single constructor. In Magento 1, the global Mage::getModel() function was the equivalent, and many Magento 1 developers carried this pattern over into Magento 2, where it counts as an anti-pattern.

<?php
// FORBIDDEN: Service Locator pattern via ObjectManager (anti-pattern)
use Magento\Framework\App\ObjectManager;

class BadCheckoutHelper
{
    public function getCartTotal(): float
    {
        // Dependencies are hidden here: neither in the constructor nor visible
        $om = ObjectManager::getInstance();
        $cart = $om->get(\Magento\Checkout\Model\Cart::class);
        $session = $om->get(\Magento\Checkout\Model\Session::class);
        $logger = $om->get(\Psr\Log\LoggerInterface::class);

        // This class cannot be tested without a running ObjectManager.
        // PHPStan ECG rules flag these lines as errors.
        $quote = $session->getQuote();
        return (float) $quote->getGrandTotal();
    }
}

Why ObjectManager::getInstance() is an anti-pattern

The first and most critical objection to using the ObjectManager directly is the invisibility of dependencies. In a correctly implemented class, the constructor describes every dependency, it is a contract: "This class needs exactly these services to function." Anyone who hides an ObjectManager call inside a method breaks that contract. The class has a hidden dependency that is never communicated outward.

The second problem, especially painful in everyday development, is that classes with direct ObjectManager calls cannot be tested in isolation with unit tests. Unit tests require mocking dependencies, replacing real implementations with test doubles. That is trivial with Constructor Injection. With ObjectManager calls inside method bodies, you have to manipulate global state, which makes tests flaky, slow and dependent on execution order.

The third problem concerns the build system: bin/magento setup:di:compile is the most powerful error-checking tool in Magento 2. It analyzes every class, checks constructor dependencies against registered types and interface preferences, and generates factory classes. This analysis catches typos in class names, missing preferences and circular dependencies, but only in constructors. ObjectManager calls inside methods are invisible to this mechanism. Errors only surface at runtime, often in production environments.

The fourth problem is the violation of the Open/Closed Principle: classes with ObjectManager calls are hard to extend because plugins and preferences only act on constructor injections. When a dependency is called directly through the ObjectManager, no alternative implementation can be swapped in through preference entries in di.xml without changing the call itself.

SOLID principles and how ObjectManager violates them

The Dependency Inversion Principle (DIP), the "D" in SOLID, states that high-level modules should not depend on low-level modules, but both should depend on abstractions. In practice that means classes should be programmed against interfaces, not against concrete implementations. The DI container helps with this by automatically injecting the correct implementation for an interface.

A direct ObjectManager::get(ConcreteClass::class) call violates DIP in two ways: first, it references the concrete implementation instead of an interface. Second, the class itself is now directly dependent on ObjectManager, a concrete implementation of a framework service. The Single Responsibility Principle (SRP) is violated too, because the class now has two responsibilities: its actual job and managing its own dependencies.

The Interface Segregation Principle suffers as well: since dependencies are not declared in the constructor, there is no incentive to define lean, specific interfaces. Developers tend to pull broad, universal classes through the ObjectManager instead of exposing only the needed methods through narrow interfaces.

The Liskov Substitution Principle is violated when concrete classes are retrieved instead of interfaces: extending such a class through a subclass or a preference is technically possible but not cleanly enforced, because the ObjectManager call references the concrete class directly. In tests you cannot replace the dependency with a test double, because the ObjectManager call sits in the middle of the method body, inseparable from the rest of the code.

Legitimate exceptions: when the ObjectManager is allowed

The rule "no ObjectManager in production code" has a small number of well-defined exceptions. These exceptions are not loopholes, they are specific scenarios where no DI context exists yet, or where the ObjectManager is explicitly needed as a bootstrapping tool. Every exception should be commented and justified in the code.

The first legitimate location is bootstrap entry points: pub/index.php, pub/cron.php and the bin/magento CLI script. These files initialize the ObjectManager themselves, they have to, because there is no other way to start the DI container. After that, they hand control over to the framework, which uses Constructor Injection.

The second legitimate location is integration tests: Magento\TestFramework\Helper\Bootstrap::getObjectManager() allows direct ObjectManager access in integration test fixtures. This is a deliberate design decision: integration tests run in a full Magento context and need to instantiate classes without having to write full class constructors. Unit tests, on the other hand, should have no ObjectManager dependency whatsoever.

The third special case concerns factory classes that are not themselves registered as injectable. A rare but valid scenario: a static helper method called outside the DI context. Here the ObjectManager can serve as a bridge, but even this should be avoided wherever possible and replaced with clean DI design. In Magento 2.4.8 there is a cleaner path for nearly every one of these cases.

<?php
declare(strict_types=1);

// LEGITIMATE: Bootstrap entry point, pub/index.php
// The ObjectManager is initialized here, not misused
$bootstrap = \Magento\Framework\App\Bootstrap::create(BP, $_SERVER);
$app = $bootstrap->createApplication(\Magento\Framework\App\Http::class);
$bootstrap->run($app);

// LEGITIMATE: Magento integration test fixture
namespace Mironsoft\Blog\Test\Integration;

use Magento\TestFramework\Helper\Bootstrap;

class PostRepositoryTest extends \PHPUnit\Framework\TestCase
{
    private \Magento\Framework\ObjectManagerInterface $objectManager;

    protected function setUp(): void
    {
        // Allowed in integration tests, deliberately designed this way
        $this->objectManager = Bootstrap::getObjectManager();
    }

    public function testSaveAndRetrievePost(): void
    {
        $repository = $this->objectManager->get(
            \Mironsoft\Blog\Api\PostRepositoryInterface::class
        );
        // Test logic...
    }
}

The right way: Constructor Injection in PHP 8.4

Constructor Injection is the correct, Magento-supported and recommended way to manage dependencies. The idea is simple: every dependency a class needs is declared as a parameter in the constructor. Magento's DI container reads these parameters, resolves them automatically and passes in the matching instances when the class is created. The developer does not need to do anything beyond declaring the dependencies correctly.

In PHP 8.4, Constructor Injection is made considerably simpler by Constructor Property Promotion. Instead of declaring a property, accepting a parameter, and writing the assignment, everything happens in a single line in the constructor parameter list. The result is more concise, more readable code without boilerplate. Combined with readonly, it guarantees that dependencies cannot be overwritten after injection, immutability at the property level.

An important detail: Magento's DI container understands the difference between interfaces and concrete classes. When you declare an interface as a constructor parameter, the container looks up the registered preference for that interface in the di.xml configuration and injects the configured implementation. That makes the code flexibly swappable: a different implementation can be activated through a simple preference entry in di.xml, without touching the class itself.

declare(strict_types=1) is mandatory in Magento 2.4.8 for all new PHP files. It ensures that type errors during injection are caught immediately as errors instead of being hidden by implicit type conversions. Together with PHPStan and the ECG coding standard rules, this forms a robust safety net against ObjectManager misuse and other code quality problems.

<?php
declare(strict_types=1);

namespace Mironsoft\Order\Model;

use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Framework\Api\SearchCriteriaBuilder;
use Magento\Framework\Exception\NoSuchEntityException;
use Psr\Log\LoggerInterface;

/**
 * Order processor: all dependencies declared via constructor injection.
 * No ObjectManager usage: fully testable, mockable, compile-time-validated.
 */
class OrderProcessor
{
    public function __construct(
        private readonly ProductRepositoryInterface $productRepository,
        private readonly SearchCriteriaBuilder $searchCriteriaBuilder,
        private readonly LoggerInterface $logger,
        // Virtual types and custom arguments can be configured in di.xml
        private readonly string $defaultCurrency = 'EUR'
    ) {}

    /**
     * Process order for a product by ID.
     * All dependencies are available via $this: injected, mockable, typed.
     */
    public function processForProduct(int $productId): void
    {
        try {
            $product = $this->productRepository->getById($productId);
            $this->logger->info('Processing order', [
                'sku'      => $product->getSku(),
                'currency' => $this->defaultCurrency,
            ]);
        } catch (NoSuchEntityException $e) {
            $this->logger->error('Product not found', [
                'product_id' => $productId,
                'error'      => $e->getMessage(),
            ]);
        }
    }
}

Factories instead of ObjectManager::create()

Constructor Injection works great for services, repositories, loggers and other injectable objects that are managed as shared instances. For non-injectable objects, that is, entities such as Product, Quote, Order, as well as collections and value objects, you need a different approach. These objects must be freshly instantiated for every record and must not be shared as a single instance.

Magento solves this elegantly with automatically generated factory classes. When you run bin/magento setup:di:compile, Magento analyzes every constructor and generates factory classes for all non-injectable types that are used as a factory. The pattern is simple: instead of injecting MyClass into the constructor, you inject MyClassFactory. Magento recognizes the Factory suffix and generates the corresponding class automatically.

The advantage over ObjectManager::create() is significant: the factory is itself an injectable class that can be received via Constructor Injection and replaced with a mock factory in tests. The generated factory internally uses the ObjectManager, but that is legitimized because factory classes are explicitly an exception to the anti-pattern rule.

Factories can also be called with custom arguments: $factory->create(['data' => [...]]). These arguments are passed to the constructor of the object being created after the DI container has resolved its normal dependencies. This allows request-specific data to be passed to new object instances without bypassing the DI container's mechanism.

<?php
declare(strict_types=1);

namespace Mironsoft\Blog\Model;

use Mironsoft\Blog\Api\Data\PostInterface;
use Mironsoft\Blog\Api\Data\PostInterfaceFactory;
use Mironsoft\Blog\Api\PostRepositoryInterface;
use Magento\Framework\Exception\CouldNotSaveException;

/**
 * Blog post service: uses Factory instead of ObjectManager::create().
 * The generated PostInterfaceFactory is a fully injectable, mockable class.
 */
class PostService
{
    public function __construct(
        private readonly PostInterfaceFactory $postFactory,
        private readonly PostRepositoryInterface $postRepository
    ) {}

    /**
     * Create and persist a new blog post.
     *
     * @throws CouldNotSaveException
     */
    public function createPost(string $title, string $content, int $authorId): PostInterface
    {
        // Factory::create() instead of ObjectManager::create()
        // The factory is injected: fully mockable in unit tests
        $post = $this->postFactory->create();
        $post->setTitle($title);
        $post->setContent($content);
        $post->setAuthorId($authorId);
        $post->setStatus(PostInterface::STATUS_DRAFT);

        return $this->postRepository->save($post);
    }
}

Mocking and refactoring the ObjectManager in unit tests

When you run into legacy code that uses the ObjectManager directly, refactoring is the clean solution. The first step is always writing a unit test for the existing class, even if the test fails at first. The test reveals which dependencies the ObjectManager resolves and makes the refactoring goal clear.

For legacy code that cannot be refactored yet, Magento offers ObjectManager::setInstance() as a test helper. It lets you set a mock implementation of the ObjectManager that returns the configured mocks on get() calls. It is not an elegant solution, but it allows writing tests for legacy classes as an interim measure until refactoring happens.

The refactoring itself follows a clear pattern: every $om->get() call in the class is identified. Each resolved type is added as a readonly constructor parameter. Method bodies replace $om->get(Type::class) with $this->injectedProperty. After the refactoring, every new dependency is passed as a PHPUnit mock in the unit tests.

After refactoring, you should always run bin/magento setup:di:compile. It validates every new constructor dependency, catches errors in class names and missing preferences, and gives a complete overview of all dependencies in the project. It is the most reliable tool for verifying a successful DI refactoring.

Recognizing and fixing typical code smells

The most common ObjectManager code smell is calling it directly inside a method: ObjectManager::getInstance()->get() or ObjectManager::getInstance()->create() somewhere in the method body. With grep or PHPStan, these calls can be identified quickly across an entire codebase. PHPStan with the ECG rules from the magento/magento-coding-standard package flags them automatically as errors.

A second code smell: the ObjectManager instance as a constructor parameter. Some developers recognize the problem with direct getInstance() calls and inject ObjectManagerInterface via the constructor instead. That is better than getInstance(), but it is still an anti-pattern. The ObjectManager should never be injected as a dependency, it is a framework internal tool, not an application dependency.

A third code smell shows up in observer classes: many legacy observers call the ObjectManager inside the execute() method to fetch services. Observer classes are full injectable classes and can declare every dependency via Constructor Injection. There is no technical reason to use the ObjectManager inside an observer.

PHPStan at level 8 with the ECG rules enabled is the most efficient tool for systematically detecting every ObjectManager smell. It can be integrated into CI/CD pipelines and prevents new anti-patterns from being introduced. Configuration happens through a phpstan.neon file at the project root.

Service Locator & ObjectManager: the essentials at a glance

Forbidden in production code

ObjectManager::getInstance()->get() in classes, blocks, ViewModels, plugins, observers. PHPStan ECG rules flag it as an error.

Constructor Injection = correct

Declare every dependency in the constructor. PHP 8.4 Constructor Property Promotion with readonly. The DI container resolves automatically.

Factory instead of create()

For non-injectables: generated ProductFactory via injection, not $om->create(). Mockable, testable, DI-validated.

Legitimate exceptions

Bootstrap entry points, integration tests. Always comment them. Never in regular classes.

Mironsoft

Magento 2 code quality & DI architecture

Want to replace ObjectManager code with DI?

We analyze and migrate ObjectManager anti-patterns in Magento 2 projects to clean Dependency Injection. Code review, PHPStan integration and full test coverage included.

Code audit

Find every ObjectManager call and prioritize by criticality

DI migration

Introduce Constructor Injection, the Factory pattern and service contracts

PHPStan setup

Enable ECG rules to automatically detect new anti-patterns

Summary

The Service Locator pattern and its Magento expression, calling the ObjectManager directly, is one of the most common anti-patterns in Magento 2 projects. The temptation is understandable: one line of code for access to any service. The price is high: invisible dependencies, untestable code, no compile-time validation and violations of every relevant SOLID principle.

Constructor Injection is the clean alternative. With PHP 8.4 Constructor Property Promotion and readonly, it is no more effort to write than an ObjectManager call, but it is significantly more maintainable, testable and future-proof. Generated factory classes fully solve the problem of non-injectable objects without ever having to fall back on the ObjectManager.

The few legitimate exceptions, bootstrap entry points and integration tests, are clearly defined and should always be commented. PHPStan with ECG rules and running setup:di:compile in CI/CD pipelines protect existing code from the gradual creep of new ObjectManager anti-patterns.

FAQ: Service Locator & ObjectManager in Magento 2

1 What is the Service Locator pattern?
A container that provides services on request. Instead of declaring dependencies in the constructor, the class asks the locator at runtime. It makes dependencies invisible and untestable, which is why it counts as an anti-pattern in modern application code. In Magento 2 it manifests as a direct ObjectManager call.
2 Why is ObjectManager::getInstance() an anti-pattern?
Dependencies are invisible (not in the constructor), unit tests cannot mock them, setup:di:compile finds no errors, and PHPStan/ECG rules flag it as an error. It violates Dependency Inversion, Open/Closed and Single Responsibility principles.
3 How do you replace ObjectManager::getInstance()->get()?
With Constructor Injection: declare the class or interface as a parameter in the constructor. The DI container injects it automatically. PHP 8.4 Constructor Property Promotion with readonly: private readonly ProductRepositoryInterface $repo.
4 How do you replace ObjectManager::getInstance()->create()?
With the Factory pattern: inject the generated ProductFactory via Constructor Injection, call it via $this->productFactory->create(). Magento generates factory classes automatically during di:compile.
5 When is it legitimate to use the ObjectManager in Magento 2?
In bootstrap entry points (pub/index.php), in integration test fixtures, and in custom CLI entry points outside the DI context. Never in regular classes, ViewModels, plugins, blocks or observers.
6 How do you mock the ObjectManager in unit tests?
With correctly structured classes (Constructor Injection), you need no ObjectManager mock at all, every dependency is passed directly as a PHPUnit mock. For legacy code: ObjectManager::setInstance() as an interim measure until refactoring.
7 How do you find ObjectManager calls in a Magento project?
With grep: grep -rn 'ObjectManager::getInstance'. Or PHPStan with magento/magento-coding-standard ECG rules, which flag every disallowed ObjectManager call automatically as an error, integrable into CI/CD.
8 What is the difference between ObjectManager::get() and ObjectManager::create()?
get() returns a shared instance, singleton behavior. create() always creates a new instance, factory behavior. Correct usage: shared instances via Constructor Injection, new instances via generated factory classes.
9 Does setup:di:compile detect faulty ObjectManager calls?
No. setup:di:compile validates DI configuration and constructors, but does not analyze runtime code inside method bodies. For that you need PHPStan with ECG rules.
10 How do you refactor a class that uses the ObjectManager directly?
Step 1: identify every $om->get() and create() call. Step 2: declare the types as constructor parameters. Step 3: replace the method calls with $this->property. Step 4: write unit tests. Step 5: run setup:di:compile.