Dependency Injection in Magento 2
AI generated
Magento 2 · Design Patterns

Dependency Injection
in Magento 2

The foundation of all Magento architecture: constructor injection, di.xml, preferences, virtual types, shared objects and clean testing with mocks, explained in full.

⏱ 15 min read PHP 8.4 Magento 2.4.8

1. The core principle: Inversion of Control

DI is a concrete implementation of the Inversion of Control (IoC) principle: instead of a class controlling its own dependencies (creating, configuring them), a higher-level container takes over that control and "injects" the dependencies from the outside.

  • Testability: in a test, you can inject a mock implementation without changing the class itself.
  • Interchangeability: the implementation behind an interface can be swapped through configuration.
  • Loose coupling: classes only know the interface, not the concrete implementation.
  • Declarative configuration: which class applies to which interface is defined in XML, clear and centralized.

2. Constructor injection and property promotion

Magento 2 exclusively supports constructor injection. Property injection and setter injection are not used. The constructor is the only place where all of a class's dependencies are visible. Since PHP 8.1, constructor property promotion combines declaration and assignment in a single line:


<?php
declare(strict_types=1);

namespace Mironsoft\Blog\Model;

use Psr\Log\LoggerInterface;
use Mironsoft\Blog\Api\PostRepositoryInterface;
use Mironsoft\Blog\Api\Data\PostInterfaceFactory;
use Magento\Framework\Stdlib\DateTime\TimezoneInterface;

/**
 * Blog post management service.
 */
class PostManager
{
    /**
     * PHP 8.1+ Constructor Property Promotion with readonly.
     */
    public function __construct(
        private readonly PostRepositoryInterface $postRepository,
        private readonly PostInterfaceFactory    $postFactory,
        private readonly TimezoneInterface       $timezone,
        private readonly LoggerInterface         $logger
    ) {}

    public function publish(int $postId): void
    {
        $post = $this->postRepository->getById($postId);
        $post->setStatus('published');
        $post->setPublishedAt($this->timezone->date()->format('Y-m-d H:i:s'));
        $this->postRepository->save($post);
        $this->logger->info('Post published', ['post_id' => $postId]);
    }
}

3. di.xml: the DI container's configuration language

The di.xml file is the central configuration file for the ObjectManager. It describes which concrete classes are used for interfaces, how arguments are passed and which plugins are active:


<?xml version="1.0"?>
<!-- app/code/Mironsoft/Blog/etc/di.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">

    <!-- 1. Preference: bind an interface to an implementation -->
    <preference for="Mironsoft\Blog\Api\PostRepositoryInterface"
                type="Mironsoft\Blog\Model\PostRepository"/>

    <!-- 2. Arguments: specific arguments for a class -->
    <type name="Mironsoft\Blog\Model\PostManager">
        <arguments>
            <argument name="logger" xsi:type="object">Mironsoft\Blog\Logger\BlogLogger</argument>
        </arguments>
    </type>

    <!-- 3. Plugin: add behavior to a method -->
    <type name="Mironsoft\Blog\Model\PostRepository">
        <plugin name="mironsoft_blog_cache_invalidator"
                type="Mironsoft\Blog\Plugin\PostCacheInvalidator"
                sortOrder="20"/>
    </type>

</config>

4. Preferences: binding interfaces to implementations

A <preference> element binds an interface to a concrete implementation. When the ObjectManager needs to instantiate a class with this interface as a dependency, it uses the configured preference:


<!-- Always bind your own interfaces, this is required! -->
<preference for="Mironsoft\Blog\Api\PostRepositoryInterface"
            type="Mironsoft\Blog\Model\PostRepository"/>

<preference for="Mironsoft\Blog\Api\Data\PostInterface"
            type="Mironsoft\Blog\Model\Post"/>

<!-- Overriding core classes: CAUTION, only when necessary! -->
<!-- Use a plugin instead! -->
<preference for="Magento\Customer\Api\CustomerRepositoryInterface"
            type="Mironsoft\Blog\Model\CustomerRepositoryExtended"/>

5. Arguments: injecting configuration

With <arguments> you can pass specific instances or configuration values to certain classes:


<type name="Mironsoft\Blog\Model\Notifier">
    <arguments>
        <!-- Inject a string value -->
        <argument name="senderEmail" xsi:type="string">blog@mironsoft.de</argument>

        <!-- Inject a boolean -->
        <argument name="enableNotifications" xsi:type="boolean">true</argument>

        <!-- Inject a number -->
        <argument name="maxRetries" xsi:type="number">3</argument>

        <!-- Inject a specific object -->
        <argument name="logger" xsi:type="object">Mironsoft\Blog\Logger\BlogLogger</argument>

        <!-- Inject an array -->
        <argument name="allowedStatuses" xsi:type="array">
            <item name="draft"     xsi:type="string">draft</item>
            <item name="published" xsi:type="string">published</item>
        </argument>
    </arguments>
</type>

6. Virtual types: configuring classes without subclasses

A virtual type is a "virtual" class with no PHP file of its own: it configures an existing class with different constructor arguments. Ideal for loggers, API clients and configurable services:


<!-- Virtual type: a custom logger without a new PHP class -->
<virtualType name="Mironsoft\Blog\Logger\BlogHandler"
             type="Magento\Framework\Logger\Handler\Base">
    <arguments>
        <argument name="fileName" xsi:type="string">/var/log/mironsoft_blog.log</argument>
    </arguments>
</virtualType>

<virtualType name="Mironsoft\Blog\Logger\BlogLogger"
             type="Magento\Framework\Logger\Monolog">
    <arguments>
        <argument name="name" xsi:type="string">mironsoft_blog</argument>
        <argument name="handlers" xsi:type="array">
            <item name="system" xsi:type="object">Mironsoft\Blog\Logger\BlogHandler</item>
        </argument>
    </arguments>
</virtualType>

<!-- Inject the virtual type just like a real class -->
<type name="Mironsoft\Blog\Model\PostManager">
    <arguments>
        <argument name="logger" xsi:type="object">Mironsoft\Blog\Logger\BlogLogger</argument>
    </arguments>
</type>

7. Shared vs. non-shared objects

By default, the ObjectManager creates only a single instance per class (shared). Ideal for services and repositories since they hold no state. Entities and DTOs need a new instance on every call, which is what factories are for:


<?php
declare(strict_types=1);

namespace Mironsoft\Blog\Model;

use Mironsoft\Blog\Api\Data\PostInterface;
use Mironsoft\Blog\Api\Data\PostInterfaceFactory;

/**
 * Uses Factory for new Post instances instead of direct injection.
 */
class PostService
{
    public function __construct(
        private readonly PostInterfaceFactory $postFactory
    ) {}

    public function createDraft(string $title): PostInterface
    {
        // create() always returns a NEW instance
        $post = $this->postFactory->create();
        $post->setTitle($title);
        $post->setStatus('draft');
        return $post;
    }
}

8. DI scopes: global, frontend, adminhtml

di.xml files are separated by area (scope). Different scopes allow different implementations for frontend and backend:


app/code/Mironsoft/Blog/etc/
├── di.xml              ← applies globally (all scopes)
├── frontend/
│   └── di.xml          ← applies only in the frontend
├── adminhtml/
│   └── di.xml          ← applies only in the backend
└── webapi_rest/
    └── di.xml          ← applies only to REST API requests

9. Testing: DI enables clean unit tests

The biggest practical benefit of DI: mock objects can be injected as a substitute for real implementations. No database access, no filesystem operations, just clean, fast unit tests:


<?php
declare(strict_types=1);

namespace Mironsoft\Blog\Test\Unit\Model;

use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\MockObject\MockObject;
use Mironsoft\Blog\Model\PostManager;
use Mironsoft\Blog\Api\PostRepositoryInterface;
use Mironsoft\Blog\Api\Data\PostInterface;
use Mironsoft\Blog\Api\Data\PostInterfaceFactory;
use Magento\Framework\Stdlib\DateTime\TimezoneInterface;
use Psr\Log\LoggerInterface;

class PostManagerTest extends TestCase
{
    private PostManager $postManager;
    private PostRepositoryInterface&MockObject $postRepositoryMock;
    private LoggerInterface&MockObject $loggerMock;

    protected function setUp(): void
    {
        $this->postRepositoryMock = $this->createMock(PostRepositoryInterface::class);
        $this->loggerMock = $this->createMock(LoggerInterface::class);

        $timezoneMock = $this->createMock(TimezoneInterface::class);
        $timezoneMock->method('date')->willReturn(new \DateTime('2026-05-04 12:00:00'));

        $postMock = $this->createMock(PostInterface::class);
        $this->postRepositoryMock->method('getById')->willReturn($postMock);

        $this->postManager = new PostManager(
            $this->postRepositoryMock,
            $this->createMock(PostInterfaceFactory::class),
            $timezoneMock,
            $this->loggerMock
        );
    }

    public function testPublishLogsSuccessMessage(): void
    {
        $this->loggerMock
            ->expects($this->once())
            ->method('info')
            ->with('Post published', ['post_id' => 42]);

        $this->postManager->publish(42);
    }
}

10. Common mistakes with Dependency Injection

Mistake 1: using the ObjectManager directly


// WRONG, makes the code untestable and bypasses DI configuration
$repo = \Magento\Framework\App\ObjectManager::getInstance()->get(PostRepositoryInterface::class);

// CORRECT, constructor injection
public function __construct(private readonly PostRepositoryInterface $postRepository) {}

Mistake 2: injecting concrete classes instead of interfaces


// WRONG
use Mironsoft\Blog\Model\PostRepository;
public function __construct(PostRepository $postRepository) {}

// CORRECT
use Mironsoft\Blog\Api\PostRepositoryInterface;
public function __construct(PostRepositoryInterface $postRepository) {}

Mistake 3: injecting session objects directly


<!-- Always inject a session as a proxy, this avoids bootstrap problems -->
<type name="Mironsoft\Blog\Model\PostManager">
    <arguments>
        <argument name="customerSession" xsi:type="object">
            Magento\Customer\Model\Session\Proxy
        </argument>
    </arguments>
</type>

Mironsoft

Magento 2 architecture & code quality

Need a Magento code review or architecture consulting?

We analyze existing Magento code for DI anti-patterns, ObjectManager misuse and upgrade risks. Clear recommendations and a migration roadmap included.

DI audit

Identify ObjectManager calls, core overrides and circular dependencies

Architecture review

di.xml configuration, preference vs. plugin decisions, virtual type design

Unit tests

Introduce a testable DI architecture, build up PHPUnit mocks and test coverage

11. Summary

Dependency Injection is the foundation of all Magento 2 development. It enables loose coupling, testability and interchangeability through consistent constructor injection, interface bindings via di.xml, and automatically generated factories and proxies.

Dependency Injection: the essentials at a glance

Constructor injection

Always through the constructor. Use constructor property promotion. readonly for immutable dependencies. Never call the ObjectManager directly.

Interfaces over implementations

Always inject the interface, never the implementation directly. Bind it via <preference> in di.xml. Avoid core preferences, prefer a plugin.

Factory for new objects

Never inject data objects directly, always use a factory. Auto-generated for every class: ClassNameFactory. create() returns a new instance.

Virtual types

Configure classes without PHP code. Ideal for loggers, decorators and wrappers with different configurations. Lives only in di.xml, no PHP file needed.

12. FAQ: Dependency Injection in Magento 2

1 What is the ObjectManager?
Magento's DI container: reads type hints in the constructor and instantiates dependencies automatically. Never call it directly (except factories, bootstrap). Always use constructor injection.
2 Why does setup:di:compile fail?
Common causes: type errors in the constructor, incorrect class names in di.xml, circular dependencies. The error message shows the class. Fix: check di.xml, resolve circular dependencies with a proxy or an event.
3 Plugin vs. preference: which to choose?
Preference: replace a class entirely (conflicts possible). Plugin: add behavior before, after or around a method (combinable). Binding interfaces: preference. Changing core behavior: plugin.
4 What are virtual types?
Configured class variants without PHP code, only in di.xml. Same class with different constructor arguments. Ideal for loggers, API clients with different endpoints, no class duplication needed.
5 When to use a factory instead of direct injection?
For non-injectable objects (model, entity, collection). Direct injection always returns the same instance. Factory::create() returns a new, empty instance every time. Magento generates factories automatically.
6 Area-specific DI configuration?
etc/frontend/di.xml, etc/adminhtml/di.xml, etc/webapi_rest/di.xml. Area-specific takes precedence over global (etc/di.xml). For different implementations per scope.
7 How to inject a session correctly?
Never directly, it affects bootstrap. Use the proxy: Magento\Customer\Model\Session\Proxy in di.xml. The proxy initializes the session only on the first method call (lazy loading).
8 How do I test classes with DI?
Create PHPUnit mocks: $this->createMock(PostRepositoryInterface::class). Instantiate the class directly: new PostManager($repoMock, ...). No ObjectManager, no database access.
9 What happens without a preference for an interface?
The DI container throws an exception: "Cannot instantiate interface X". Every interface needs a preference binding. The preference belongs in the di.xml of the module that defines the interface.
10 Proxy vs. factory: what's the difference?
Factory: creates new instances of non-injectable objects via create(). Proxy: a lazy-loading wrapper for injectable objects, the instance is created only on the first method call. Both are generated automatically by Magento.