What is Magento di.xml ?
di.xml is Magento 2’s dependency-injection configuration file. Every class gets its dependencies through __construct(Dep1 $a, Dep2 $b), and di.xml is where the framework is told which classes to wire in. It lives at etc/di.xml (global), etc/frontend/di.xml (storefront), or etc/adminhtml/di.xml (admin). Five canonical uses: preferences (swap an interface implementation), plugins (wrap a method), type arguments (inject constructor params), virtualTypes (clone a class under a new name), and structured data injection (pass an array of config into a service).
Five steps from XML node to running interceptor
di.xml is not magic — it is parsed once at boot, merged across every enabled module, and compiled into concrete proxy classes by setup:di:compile. Here is the wiring, end to end.
-
01
Pick the dependency wiring you need
Five canonical jobs. Replace an interface implementation entirely —
<preference for="..." type="..."/>. Wrap a method on an existing class — register a plugin inside<type><plugin/></type>. Pass specific arguments to a class’s constructor —<type><arguments><argument name="..."/></arguments></type>. Reuse a generic class under a new name with a different config —<virtualType>. Inject a structured array of config data (filters, mappings, allowed types) —<argument xsi:type="array">. Choose by intent before you touch XML. -
02
Write the di.xml
Root element is
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">. Children are<preference>,<type>, and<virtualType>. Scope is decided by file location, not by attribute:etc/di.xmlapplies everywhere,etc/frontend/di.xmlonly on the storefront,etc/adminhtml/di.xmlonly in admin. Same node in two area files is allowed — one wires the storefront, the other wires admin, and they cannot collide. -
03
Run
setup:upgrade— Magento merges every module’s di.xmlOn boot Magento loads the di.xml from every enabled module, merges all of them into a single in-memory
<config>, and caches the result. Load order is taken fromapp/etc/config.php’s modules array — which in turn is influenced by your module.xml<sequence>declarations. When two modules both prefer the same interface, the module loaded last wins; that’s why<sequence>matters in any module that overrides core or third-party classes. -
04
Compile in production —
setup:di:compilebin/magento setup:di:compilewalks the merged di.xml and generates three classes of artefacts undergenerated/code/. Interceptors for every class that has a plugin (Foo\Bar\InterceptorextendsFoo\Barand wraps the plugin’d method). Factories for every\Foo\BarFactoryreference. Proxies for every\Foo\Bar\Proxylazy-load reference. In default / developer mode Magento generates these on first request instead, which is slow — production mode pre-compiles them once and serves them flat. -
05
Resolved at runtime by the ObjectManager
When code asks for a class — constructor injection,
ObjectManagerInterface::create(),ObjectManagerInterface::get()— the ObjectManager consults the merged di.xml. Preferences are honoured first (the replacement class is instantiated). Plugins wrap the result. Type arguments supply constructor params. Virtual types resolve to their concrete type with the cloned argument map. The class’s own constructor never sees this machinery — from inside the class it’s just__construct(Dep1 $a, Dep2 $b).
Four scenarios — pick the right di.xml primitive for the job
Most di.xml mistakes come from reaching for the wrong primitive. Each of these four jobs has one obviously-correct answer.
-
Replacing a Magento or third-party class entirely
Use a
<preference>. The canonical example: “for the interfaceMagento\Catalog\Api\ProductRepositoryInterface, useVendor\Module\Model\MyProductRepository”. Magento’s own di.xml is full of preferences mapping every public interface to its default concrete implementation — your module reroutes that mapping to your class. The replacement should extend the original or implement the same interface so plugins on the original still apply where possible. -
Wrapping a method without replacing the class
Use a plugin declared in di.xml:
<type name="Vendor\Target"><plugin name="my_plugin" type="Vendor\Module\Plugin\MyPlugin"/></type>. Plugins runbefore/around/aftera public method without subclassing it — they coexist with every other plugin and preference on the same class. This is the standard answer 90% of the time you want to “change Magento’s behaviour” — preferences are heavier and more disruptive. -
Reusing a generic class with different configuration
Use a
<virtualType>. Magento’s collection processors, filter chains, and configurable factories are riddled with virtual types — e.g. two filter managers both extendingMagento\Framework\Filter\FilterManagerbut registered asMyCatalogFiltersandMyCustomerFilterswith different argument arrays. You get a fresh DI alias without writing a new PHP class. Reference the virtual type by name anywhere a normal class name would go. -
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 scalars, nested arrays, orxsi:type="object"references to other DI-managed classes. Used everywhere a service needs a list of strategies, validators, formatters, or feature flags wired by configuration rather than code — the canonical “extensible by other modules” pattern in Magento.
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. <code>etc/di.xml</code> applies globally (every area). <code>etc/frontend/di.xml</code> applies only when Magento is running in the frontend area. <code>etc/adminhtml/di.xml</code> applies only in admin. <code>etc/webapi_rest/di.xml</code> and <code>etc/webapi_soap/di.xml</code> apply 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 <code><preference></code> for the same interface and loads after yours, so its preference wins. Open <code>app/etc/config.php</code> and look at the <code>modules</code> array order; the further down a module sits, the later it loads. To fix, add a <code><sequence></code> block to your module’s <code>etc/module.xml</code> listing 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 in <code>etc/adminhtml/di.xml</code> but the code runs on the storefront), or your replacement class has a syntax error and Magento silently falls back. Check <code>var/log/exception.log</code>. -
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 <code><if store="..."></code> primitive, and adding one would defeat the purpose of pre-compilation. The standard answer is to put the conditional logic <em>inside</em> the class itself: inject <code>Magento\Framework\App\Config\ScopeConfigInterface</code> via 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 generic <code>FilterManager</code> that 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 <code><virtualType name="MyCatalogFilters" type="Vendor\FilterManager"><arguments>...</arguments></virtualType></code> 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 <em>frontend-only</em> di.xml (<code>etc/frontend/di.xml</code>) targeting Luma block classes (e.g. <code>Magento\Catalog\Block\Product\View</code>) 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.