Chat on WhatsApp
Magento glossary

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).

How it works

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.

  1. 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.

  2. 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.xml applies everywhere, etc/frontend/di.xml only on the storefront, etc/adminhtml/di.xml only in admin. Same node in two area files is allowed — one wires the storefront, the other wires admin, and they cannot collide.

  3. 03

    Run setup:upgrade — Magento merges every module’s di.xml

    On 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 from app/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.

  4. 04

    Compile in production — setup:di:compile

    bin/magento setup:di:compile walks the merged di.xml and generates three classes of artefacts under generated/code/. Interceptors for every class that has a plugin (Foo\Bar\Interceptor extends Foo\Bar and wraps the plugin’d method). Factories for every \Foo\BarFactory reference. Proxies for every \Foo\Bar\Proxy lazy-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.

  5. 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).

When to use

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 interface Magento\Catalog\Api\ProductRepositoryInterface, use Vendor\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 run before / around / after a 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 extending Magento\Framework\Filter\FilterManager but registered as MyCatalogFilters and MyCustomerFilters with 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, or xsi: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.

Common mistakes

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 — meaning Vendor_A beats Vendor_Z… until a third module ships and re-shuffles everything. Always declare <sequence> in your module’s etc/module.xml listing every module whose classes you intend to override. Make load order explicit, not accidental.

  • Using <preference> when a plugin would do

    Preferences 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:compile after a di.xml edit in production

    Production 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 is bin/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.

FAQ

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.
di.xml audit

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.