Skip to content

Magento Dependency Injection — di.xml, Preferences and Type Config

DodaTech Updated 2026-06-27 11 min read

In this tutorial, you'll learn how Magento's dependency injection container works with di.xml configuration, preferences, type arguments, virtual types, and constructor injection.

What You'll Learn

  • How Magento's ObjectManager and DI container work
  • How to map interfaces to implementations using preferences
  • How to configure constructor arguments in di.xml
  • How virtual types let you reuse class configurations
  • How plugins are declared in di.xml

Why It Matters

Dependency injection is the architectural foundation of Magento 2. Unlike Magento 1, where classes instantiated dependencies directly using new or Mage::getModel(), Magento 2 uses a DI container that automatically resolves and injects dependencies. Understanding DI is essential because every module you write uses it. When you write a constructor that accepts interfaces, Magento's DI container supplies the concrete implementations. When you need to replace a core class with your own, you use a preference in di.xml. Without this knowledge, you will fight the framework instead of working with it.

Real-World Use

A logistics company needs to integrate their shipping calculator with Magento's rate request system. The core Magento\Shipping\Model\Carrier\AbstractCarrier handles rate requests, but the company's custom rate logic is in Vendor\Shipping\Model\Carrier\CustomCarrier. Using a preference in di.xml, Magento's checkout uses the custom carrier instead of the default. The preference maps the shipping carrier interface to the custom implementation. No core code is changed. The integration ships in hours instead of days.

Learning Path

flowchart LR
  A["25: Module Structure"] --> B["26: Dependency Injection
You are here"]:::current B --> C["27: Plugins"] C --> D["28: Observers and Events"] classDef current fill:#38bdf8,color:#0f172a,stroke-width:2px

What Is Dependency Injection

Dependency injection is a design pattern where a class receives its dependencies from an external source rather than creating them internally. Instead of writing:

// BAD: tight coupling, hard to test
class Example
{
    private $database;

    public function __construct()
    {
        $this->database = new Database('localhost', 'user', 'pass');
    }
}

You write:

// GOOD: dependency injected, loose coupling
class Example
{
    private $database;

    public function __construct(Database $database)
    {
        $this->database = $database;
    }
}

The first approach is hard to test (you cannot mock the database) and hard to change (different environments need different connection details). The second approach lets the DI container supply the correct database instance. In testing, you supply a mock.

Magento's DI Container: ObjectManager

Magento's DI container is called the ObjectManager. When a class is instantiated, the ObjectManager reads the constructor's type hints, looks up the concrete classes for each interface argument, resolves the full dependency tree recursively, and creates the object.

What the ObjectManager Does

// This is what happens internally when you inject a class
$objectManager = \Magento\Framework\App\ObjectManager::getInstance();

// It reads constructor signatures, resolves dependencies,
// and creates the object with all dependencies injected
$example = $objectManager->create(\Vendor\Module\Model\Example::class);

You should never call ObjectManager::getInstance() directly in your code. The ObjectManager is an internal implementation detail. Always use constructor injection.

Auto-Generated Factories

Magento automatically generates factory classes for any class you reference with a Factory suffix in a constructor:

use Vendor\Module\Model\ExampleFactory;

class MyClass
{
    private $exampleFactory;

    public function __construct(ExampleFactory $exampleFactory)
    {
        $this->exampleFactory = $exampleFactory;
    }

    public function createExample()
    {
        return $this->exampleFactory->create();
    }
}

You do not need to write the ExampleFactory class. Magento's code generator creates it automatically based on the Example class.

di.xml Location

di.xml files can exist in multiple locations within a module:

Location Scope
etc/di.xml Global (all areas)
etc/frontend/di.xml Storefront only
etc/adminhtml/di.xml Admin panel only
etc/webapi_rest/di.xml REST API only
etc/graphql/di.xml GraphQL only

Place area-specific configurations in the area directory. Global configurations go in etc/di.xml.

Preferences

Preferences map an interface (or class) to a concrete implementation. This is the primary mechanism for replacing core classes with custom ones.

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <preference for="Magento\Catalog\Api\ProductRepositoryInterface"
                type="Vendor\Module\Model\ProductRepository"/>
</preference>
</config>

The for attribute is the interface or class that Magento injects in constructors. The type attribute is the concrete implementation to use.

Preference Resolution Order

When multiple modules declare a preference for the same interface, the last module to load wins. The loading order is determined by the <sequence> declarations in module.xml. If module B depends on module A, module B's preferences override module A's.

Class-to-Class Preferences

You can also preference a class with another class:

<preference for="Magento\Catalog\Model\Product"
            type="Vendor\Module\Model\Product"/>

This is useful when you need to extend a core model with additional functionality. The custom class extends the original:

<?php
namespace Vendor\Module\Model;

class Product extends \Magento\Catalog\Model\Product
{
    public function getName(): string
    {
        $name = parent::getName();
        return $name . ' - ' . $this->getSku();
    }
}

Type Configuration

The <type> element configures constructor arguments for a specific class. This is how you pass configuration values, enable/disable features, or inject specific objects.

<type name="Vendor\Module\Model\Shipping">
    <arguments>
        <argument name="apiEndpoint" xsi:type="string">https://api.example.com/v1</argument>
        <argument name="timeout" xsi:type="number">30</argument>
        <argument name="debugMode" xsi:type="boolean">true</argument>
        <argument name="config" xsi:type="object">Vendor\Module\Model\Config</argument>
        <argument name="options" xsi:type="array">
            <item name="retry_count" xsi:type="number">3</item>
            <item name="log_responses" xsi:type="boolean">true</item>
        </argument>
        <argument name="apiKey" xsi:type="const">Vendor\Module\Model\Config::DEFAULT_API_KEY</argument>
    </arguments>
</type>

Argument Types

xsi:type PHP Equivalent Example
string string "Hello"
number int/float 42, 3.14
boolean bool true, false
object object instance ClassName::class
null null (no value)
array array Nested <item> elements
const class constant Class::CONSTANT

Array Arguments

Arrays use nested <item> elements:

<argument name="shipping_methods" xsi:type="array">
    <item name="flatrate" xsi:type="array">
        <item name="title" xsi:type="string">Flat Rate</item>
        <item name="price" xsi:type="number">5.99</item>
        <item name="enabled" xsi:type="boolean">true</item>
    </item>
    <item name="freeshipping" xsi:type="array">
        <item name="title" xsi:type="string">Free Shipping</item>
        <item name="price" xsi:type="number">0</item>
        <item name="enabled" xsi:type="boolean">true</item>
    </item>
</argument>

Virtual Types

Virtual types let you create a new type based on an existing class with different constructor arguments. This is useful when you need multiple instances of the same class with different configurations.

<virtualType name="Vendor\Module\Model\Virtual\TypeName"
             type="Magento\Framework\View\Element\Template">
    <arguments>
        <argument name="template" xsi:type="string">Vendor_Module::different-template.phtml</argument>
        <argument name="data" xsi:type="array">
            <item name="title" xsi:type="string">Virtual Type Title</item>
        </argument>
    </arguments>
</virtualType>

The name is the virtual type's identifier. The type is the base class. The virtual type behaves exactly like the base class but with the specified constructor arguments.

Why Virtual Types Exist

Consider a scenario where you need two different product listing blocks on the same page. Each block needs different template and data:

<virtualType name="Vendor\Module\Block\FeaturedProducts"
             type="Magento\Catalog\Block\Product\ListProduct">
    <arguments>
        <argument name="template" xsi:type="string">Vendor_Module::featured.phtml</argument>
    </arguments>
</virtualType>

<virtualType name="Vendor\Module\Block\BestSellers"
             type="Magento\Catalog\Block\Product\ListProduct">
    <arguments>
        <argument name="template" xsi:type="string">Vendor_Module::bestsellers.phtml</argument>
    </arguments>
</virtualType>

Now you can use Vendor\Module\Block\FeaturedProducts and Vendor\Module\Block\BestSellers in layout XML as if they were real classes.

Plugin Declarations in di.xml

Plugins (also called interceptors) are declared in di.xml. They let you modify any public method's behavior without changing the original class.

<type name="Magento\Catalog\Model\Product">
    <plugin name="vendor_module_product_plugin"
            type="Vendor\Module\Plugin\ProductPlugin"
            sortOrder="10"
            disabled="false"/>
</type>
  • name — unique plugin identifier
  • type — the plugin class
  • sortOrder — execution order (lower runs first for before, higher runs first for after)
  • disabled — set to true to temporarily disable

Plugin classes implement before, after, and around methods:

<?php
namespace Vendor\Module\Plugin;

use Magento\Catalog\Model\Product;

class ProductPlugin
{
    public function afterGetName(Product $subject, $result)
    {
        return $result . ' - Customized';
    }

    public function beforeSetPrice(Product $subject, $price)
    {
        return [min($price, 100)];
    }

    public function aroundGetPrice(Product $subject, callable $proceed)
    {
        $originalPrice = $proceed();
        return $originalPrice * 1.1;
    }
}

Shared vs Non-Shared Instances

By default, the DI container returns the same instance every time a class is requested (Singleton Patternton" >}} pattern). You can change this with the shared attribute:

<type name="Vendor\Module\Model\Session">
    <arguments>
        <argument name="sessionId" xsi:type="string">abc123</argument>
    </arguments>
</type>

<type name="Vendor\Module\Model\RequestData">
    <arguments>
        <argument name="requestId" xsi:type="number">42</argument>
    </arguments>
</type>

To make a type non-shared (new instance every time):

<type name="Vendor\Module\Model\NonSharedModel" shared="false"/>

Constructor Injection vs ObjectManager

Always use constructor injection. Never use ObjectManager::getInstance() directly.

// CORRECT: constructor injection
class CorrectExample
{
    private $productRepository;

    public function __construct(
        \Magento\Catalog\Api\ProductRepositoryInterface $productRepository
    ) {
        $this->productRepository = $productRepository;
    }
}

// WRONG: direct ObjectManager usage
class WrongExample
{
    public function getProduct($id)
    {
        $objectManager = \Magento\Framework\App\ObjectManager::getInstance();
        return $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class);
    }
}

The ObjectManager approach breaks testability, hides dependencies, and makes the code harder to maintain. It is considered a code smell in Magento 2.

Common Mistakes

  1. Using ObjectManager directly. The most common beginner mistake. Calling ObjectManager::getInstance() is a shortcut that creates untestable code. Always inject dependencies through constructors. The only exception is in factory classes or proxy classes where the ObjectManager is part of the pattern.

  2. Forgetting to declare preferences in the correct area. A preference declared in etc/adminhtml/di.xml only applies to the admin panel. If you want it globally, put it in etc/di.xml. If your preference does not seem to take effect, check that it is in the right file.

  3. Class name collisions with virtual types. Virtual types share the same namespace as real classes. If you create a virtual type named Vendor\Module\Block\FeaturedList, you cannot also have a real PHP class at that path. Magento throws an error if a virtual type name conflicts with an existing class.

  4. Wrong xsi:type for arguments. Using xsi:type="string" for a boolean value passes the string "true" instead of the boolean true. The wrong type causes subtle bugs where conditions always evaluate as truthy. Always match the xsi:type to the PHP type expected by the constructor.

  5. Circular dependencies. If class A depends on class B, and class B depends on class A, the DI container cannot resolve either. This causes a fatal error. Break the cycle by using a factory, proxy, or by Refactoring the design to remove the circular dependency.

Practice Questions

  1. What is the difference between a preference and a virtual type? Answer: A preference replaces a class or interface with another implementation. Any code that injects the original class receives the preferred class instead. A virtual type creates a new configuration of an existing class with different constructor arguments, but does not replace the original class. Preferences are for substitution; virtual types are for reuse.

  2. What does the <arguments> element inside a <type> declaration do? Answer: The <arguments> element configures constructor arguments for the specified class. Each <argument> child maps to a constructor parameter by name. The DI container passes these values when creating instances of the class, overriding any default values in the constructor.

  3. When should you use etc/frontend/di.xml vs etc/di.xml? Answer: Use etc/frontend/di.xml for configurations that should only apply to the storefront area. Use etc/adminhtml/di.xml for admin-only configurations. Use etc/di.xml for configurations that apply to all areas. If a preference or plugin should work everywhere, put it in the global file.

  4. Challenge: Create a module that replaces the core Magento\Catalog\Api\ProductRepositoryInterface with a custom implementation that logs every product save operation. Configure the log file path as a constructor argument in di.xml. Then create a virtual type of Magento\Catalog\Block\Product\ListProduct with a different template for featured products. Verify both the preference and virtual type work on the storefront.

FAQ

What is the ObjectManager in Magento?

The ObjectManager is Magento's DI container implementation. It reads constructor type hints, resolves dependencies, and creates object instances with all required dependencies injected. While you can call it directly with ObjectManager::getInstance(), this is discouraged. Always use constructor injection instead.

How do I override a core Magento class?

Use a <preference> in di.xml to map the core class to your custom class. Your custom class extends the original and overrides the methods you need to change. Magento's DI container then returns your class whenever the original class is injected. This is cleaner than rewriting the entire class.

What happens if two modules declare a preference for the same interface?

The preference from the module that loads last wins. Module load order is determined by the <sequence> declarations in module.xml. If module B depends on module A, module B's preference overrides module A's. This can cause unexpected behavior if you are not aware of dependency chains.

Can I use constructor injection in blocks and controllers?

Yes. Blocks, controllers, models, helpers, and any class instantiated by the DI container supports constructor injection. Simply type-hint your dependencies in the constructor, add them as constructor parameters, and the ObjectManager resolves them automatically. No additional configuration is needed for concrete classes.

{{< faq "What is the shared attribute in di.xml?" "The shared attribute controls whether the DI container returns the same instance (singleton) or a new instance every time a class is requested. By default, most classes are shared. Set shared=\"false\" on a type to get a new instance each time it is injected or created." >}}

Mini Project

Your task: Create a module that demonstrates all three di.xml configuration types.

  1. Create Vendor_Demo module structure with registration.php and module.xml.
  2. Create an interface Api/DemoInterface with a method getMessage(): string.
  3. Create a default implementation Model/Demo.php that returns "Default Message".
  4. Create a custom implementation Model/CustomDemo.php that returns "Custom Message".
  5. In etc/di.xml:
    • Declare a preference mapping DemoInterface to Model/Demo
    • Add a <type> configuration for Model/Demo with a string argument greeting
    • Create a virtual type of Magento\Framework\View\Element\Template with a custom template
  6. Create a controller that injects DemoInterface and displays the message.
  7. Test that the preference works, then switch the preference to CustomDemo and verify the message changes.
  8. Add a second preference in etc/adminhtml/di.xml and verify it only applies to the admin area.

This exercise covers the three core di.xml configurations you will use in every Magento project.

What's Next

Now that you understand dependency injection, the next step is learning how plugins let you extend any class without modifying it:

Continue to Lesson 27: Magento Plugins — Before, after, and around interceptors.

Related lessons:

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro