What is Magento di.xml?
Short answer
di.xml is Magento 2’s dependency injection configuration file. Every class receives its dependencies through __construct(Dep1 $a, Dep2 $b), and di.xml tells the framework which classes to pass in. It sits at etc/di.xml (global), etc/frontend/di.xml (storefront) or etc/adminhtml/di.xml (admin). I use it for five things: preferences (swap an interface implementation), plugins (wrap a method), type arguments (set constructor parameters), virtualTypes (reuse a class under a new name) and array arguments (pass a list of config into a service).
- Pick the dependency wiring you need 01
- Write the di.xml 02
- Magento merges every module’s di.xml 03
- Compile in production, setup:di:compile 04
- Resolved at runtime by the ObjectManager 05
Most common mistake Two modules preferring the same interface without <sequence>
Five steps from XML node to running interceptor
di.xml is read from every enabled module, merged and cached, and setup:di:compile turns it into generated classes and compiled config. This is the path from an XML node to a running interceptor.
-
01
Pick the dependency wiring you need
There are five jobs di.xml can do, and I decide which one I need before I write any XML. To replace an interface implementation:
<preference for="..." type="..."/>. To wrap a method on an existing class: a plugin inside<type><plugin/></type>. To pass specific constructor arguments:<type><arguments><argument name="..."/></arguments></type>. To reuse a class under a new name with different arguments:<virtualType>. To hand a service a list of things (filters, mappings, allowed types):<argument xsi:type="array">. -
02
Write the di.xml
The root element is
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">, and its children are<preference>,<type>and<virtualType>. The file’s location sets the scope.etc/di.xmlapplies everywhere,etc/frontend/di.xmlonly on the storefront,etc/adminhtml/di.xmlonly in the admin, and there are alsowebapi_rest,webapi_soap,graphqlandcrontabareas. An area file is merged on top of the global one, so I put a plugin inetc/frontend/di.xmlwhen it should only run on the storefront. -
03
Magento merges every module’s di.xml
Magento reads the di.xml of every enabled module, merges them into one configuration per area and caches the result. The merge order follows the module list in
app/etc/config.php, whichsetup:upgradesorts using each module’s<sequence>inmodule.xml. When two modules set a preference for the same interface, the one merged last wins. That is why I always add a<sequence>on the module I am overriding. -
04
Compile in production,
setup:di:compilebin/magento setup:di:compilereads the merged config and writes three kinds of classes undergenerated/code/. Interceptors for every class with a plugin (Foo\Bar\InterceptorextendsFoo\Barand routes the plugged methods through the plugin list). Factories for every\Foo\BarFactoryreference. Proxies for every\Foo\Bar\Proxyreference. In default and developer mode Magento creates these on the first request that needs them, which is slow. In production I compile them once on deploy. -
05
Resolved at runtime by the ObjectManager
When a class is requested, through constructor injection or
ObjectManagerInterface::create()/get(), the ObjectManager checks the merged config. A preference decides which class is built. If that class has plugins, its Interceptor is built instead. Type arguments fill in constructor parameters, and a virtual type resolves to its real class with its own arguments. Inside your class none of this is visible: you just write__construct(Dep1 $a, Dep2 $b).
Four scenarios, pick the right di.xml primitive for the job
Most di.xml problems I clean up come from picking the wrong tool for the job. Each of these four jobs has one clear answer.
-
Replacing a Magento or third-party class entirely
Use a
<preference>. For example: “forMagento\Catalog\Api\ProductRepositoryInterface, useVendor\Module\Model\MyProductRepository”. Magento’s own di.xml files are mostly preferences that map each public interface to its default class, and your module points that mapping at your class instead. I make the replacement extend the original or implement the same interface, and I only do this when a plugin can’t reach the logic. -
Wrapping a method without replacing the class
Use a plugin:
<type name="Vendor\Target"><plugin name="my_plugin" type="Vendor\Module\Plugin\MyPlugin"/></type>. A plugin runsbefore,aroundoraftera public method without subclassing, and it can live next to other modules’ plugins on the same class. When a client asks me to change how Magento behaves, a plugin is usually my first answer. A preference is heavier and clashes more easily with other modules. -
Reusing a generic class with different configuration
Use a
<virtualType>. Magento core uses them all over, in collection processors, UI data providers and logger handlers. For example, two filter managers based onMagento\Framework\Filter\FilterManagerbut registered asMyCatalogFiltersandMyCustomerFilters, each with different arguments. You get a new name in DI without writing a PHP class, and you can use that name anywhere di.xml expects a class name. -
Injecting a config array into a service class
Use
<type name="Vendor\Service"><arguments><argument name="rules" xsi:type="array">...</argument></arguments></type>. The array can hold strings, numbers, nested arrays orxsi:type="object"references to other classes. Magento uses this wherever a service needs a list of validators, processors or formatters. It is also how other modules add their own entry to your list later, just by adding an item in their own di.xml.
Three di.xml mistakes that break production silently
Every “my plugin isn’t firing” ticket I have ever debugged came from one of these three. Check them in this order before reaching for xdebug.
-
Two modules preferring the same interface without
<sequence>Both modules declare
<preference for="Some\Interface" type="Their\Class"/>. The last-loaded module wins, and load order with no explicit sequence falls back to alphabetical, meaningVendor_AbeatsVendor_Z… until a third module ships and re-shuffles everything. Always declare<sequence>in your module’setc/module.xmllisting every module whose classes you intend to override. Make load order explicit, not accidental. -
Using
<preference>when a plugin would doPreferences fully replace the class. Every other module’s plugins on the original class get bypassed unless your replacement extends the original. Worse, two modules can’t both prefer the same interface to different classes, one loses. Plugins compose: ten modules can all wrap the same method without conflict. The default answer for “change one method” is a plugin. Reach for a preference only when you really need to swap the entire class (different constructor signature, different inheritance) and accept that you become the new base.
-
Forgetting
setup:di:compileafter a di.xml edit in productionProduction mode serves pre-compiled interceptors from
generated/code/. Edit di.xml, push to live, restart php-fpm, nothing happens, because Magento is still serving the old interceptor classes that were generated before your plugin existed. The fix isbin/magento setup:upgrade && bin/magento setup:di:compile && bin/magento cache:flush. Bake this into the deploy script. Symptom: your plugin / preference doesn’t fire and there’s no error in the log because Magento doesn’t know your wiring exists yet.
Magento di.xml, frequently asked questions
-
di.xml vs config.xml, what’s the difference?
Different files for different jobs. di.xml is dependency-injection wiring, it tells the ObjectManager how to instantiate classes, which plugins wrap which methods, and which preferences override which interfaces. config.xml in Magento 2 is largely a legacy carry-over from Magento 1; the vast majority of default-value configuration in M2 lives in system.xml (for admin-editable settings), default_etc.xml fragments, or other area-specific files. If you’re wiring constructor dependencies or registering interceptors, you want di.xml. If you’re setting default values for store-scope config or registering an admin config field, you want system.xml. -
Can I have multiple di.xml files in one module?
Yes, and you usually should, one per area.etc/di.xmlapplies globally (every area).etc/frontend/di.xmlapplies only when Magento is running in the frontend area.etc/adminhtml/di.xmlapplies only in admin.etc/webapi_rest/di.xmlandetc/webapi_soap/di.xmlapply only to REST and SOAP API requests respectively. Magento loads the global di.xml first, then merges the area-specific one on top when bootstrapping that area. This lets you, for example, replace a class on the storefront but keep the stock behaviour in admin. -
Why is my preference being ignored?
Almost always module load order. Another module declared a<preference>for the same interface and loads after yours, so its preference wins. Openapp/etc/config.phpand look at themodulesarray order; the further down a module sits, the later it loads. To fix, add a<sequence>block to your module’setc/module.xmllisting every module whose preference you want to override, this forces Magento to load your module after them. Less commonly: the area scope is wrong (you put the preference inetc/adminhtml/di.xmlbut the code runs on the storefront), or your replacement class has a syntax error and Magento silently falls back. Checkvar/log/exception.log. -
Can I do conditional DI based on store or website?
No, di.xml is parsed once at boot and applies globally to every request. There’s no<if store="...">primitive, and adding one would defeat the purpose of pre-compilation. The standard answer is to put the conditional logic inside the class itself: injectMagento\Framework\App\Config\ScopeConfigInterfacevia the constructor, read the store-scoped flag, and branch on it at method-call time. For more elaborate strategies, inject an array of strategy objects via di.xml and have the class pick the right one based on store-scope config at runtime, same idea, cleaner separation. -
What’s a virtualType actually for?
Cloning a generic class under a unique name with specific arguments. Say you have a genericFilterManagerthat accepts an array of filter rules in its constructor. You need two configured instances, one for catalog filters, one for customer filters. Without virtualType you’d write two near-identical PHP classes. With virtualType you write<virtualType name="MyCatalogFilters" type="Vendor\FilterManager"><arguments>...</arguments></virtualType>and another for customer filters, both resolve to the same PHP class but with their own DI arg map. Reference them by their virtual name anywhere a class name is expected. Magento core uses this heavily for collection processors, factories, and configurable services. -
Does Hyvä use di.xml?
Yes, di.xml is a framework-level mechanism, not a theme-level one. Hyvä is a frontend theme replacing Luma’s heavy KnockoutJS / RequireJS stack with Tailwind + Alpine.js, but the PHP layer underneath, the ObjectManager, the merged di.xml, the interceptor pipeline, is identical to a Luma site. Plugins and preferences registered in any module’s di.xml fire the same way regardless of which theme is active. The only Hyvä-specific consideration is that frontend-only di.xml (etc/frontend/di.xml) targeting Luma block classes (e.g.Magento\Catalog\Block\Product\View) may not run on Hyvä storefronts simply because Hyvä renders the page through different block classes, but that’s a block-selection issue, not a di.xml limitation.
Want a di.xml architecture review on your Magento module?
Send your module repo, I will audit every preference, plugin, virtualType, and type-arg injection, flag the sequence collisions and preference-vs-plugin smells, then reply with a written refactor plan, fixed-price quote, and earliest start date. 24-business-hour turnaround.