Magento Dependency Injection — di.xml, Preferences and Type Config
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 identifiertype— the plugin classsortOrder— 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
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.Forgetting to declare preferences in the correct area. A preference declared in
etc/adminhtml/di.xmlonly applies to the admin panel. If you want it globally, put it inetc/di.xml. If your preference does not seem to take effect, check that it is in the right file.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.Wrong xsi:type for arguments. Using
xsi:type="string"for a boolean value passes the string"true"instead of the booleantrue. 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.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
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.
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.When should you use
etc/frontend/di.xmlvsetc/di.xml? Answer: Useetc/frontend/di.xmlfor configurations that should only apply to the storefront area. Useetc/adminhtml/di.xmlfor admin-only configurations. Useetc/di.xmlfor configurations that apply to all areas. If a preference or plugin should work everywhere, put it in the global file.Challenge: Create a module that replaces the core
Magento\Catalog\Api\ProductRepositoryInterfacewith 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 ofMagento\Catalog\Block\Product\ListProductwith a different template for featured products. Verify both the preference and virtual type work on the storefront.
FAQ
{{< 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.
- Create
Vendor_Demomodule structure withregistration.phpandmodule.xml. - Create an interface
Api/DemoInterfacewith a methodgetMessage(): string. - Create a default implementation
Model/Demo.phpthat returns "Default Message". - Create a custom implementation
Model/CustomDemo.phpthat returns "Custom Message". - In
etc/di.xml:- Declare a preference mapping
DemoInterfacetoModel/Demo - Add a
<type>configuration forModel/Demowith a string argumentgreeting - Create a virtual type of
Magento\Framework\View\Element\Templatewith a custom template
- Declare a preference mapping
- Create a controller that injects
DemoInterfaceand displays the message. - Test that the preference works, then switch the preference to
CustomDemoand verify the message changes. - Add a second preference in
etc/adminhtml/di.xmland 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:
- Magento Module Structure — Build the module that hosts your di.xml
- Magento Observers and Events — Alternative extension mechanism
- PHP OOP Interfaces — Understand the interface pattern behind preferences
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro