ARTICLE

Magento 2 Virtual Types in di.xml: Reusing Classes and Replacing Arguments Without Writing New PHP

Magento 2 Virtual Types in di.xml: Reusing Classes and Replacing Arguments Without Writing New PHP

A Magento 2 virtual type is a configured variant of an existing class, declared entirely in di.xml with no PHP file. You give it different constructor arguments, then inject it into one consumer, so you customize behavior for that consumer without a subclass and without changing the original class for everyone else.

Virtual types are one of the most useful and most misunderstood features of the Magento dependency injection system. Teams reach for a preference or a full subclass when all they needed was the same class with different arguments. The result is extra PHP files, a wider change than intended, and a customization that is harder to upgrade. This guide covers what a virtual type actually is, the exact di.xml syntax, a real example, and the decision of when to use one instead of a preference or a plugin on Magento and Adobe Commerce.

What a virtual type really is

Magento’s object manager builds objects by reading their constructor signature and injecting the dependencies declared in di.xml. A virtual type takes advantage of that: it defines a new named type that is based on a real class but ships a different set of constructor arguments. There is no corresponding .php file. The name exists only in configuration, and the object manager treats it as if it were a distinct class.

Think of it as a subclass you declare in XML instead of in code. You are not changing the behavior of the parent class. You are creating a separately configured instance of it that only the consumers you point at it will receive. Adobe’s dependency injection component documentation describes how the object manager resolves these configured types, and the object manager reference explains the broader resolution model that makes virtual types possible.

The single most important rule: a virtual type does nothing until something injects it. Declaring one has no effect on its own. You must reference it by name in another class’s <type> arguments, or as an item inside another argument.

The di.xml syntax

A virtual type is declared with the <virtualType> element. It takes a name (the new, invented type name) and a type (the real class it is based on). Inside it, you set only the arguments you want to change:

<virtualType name="Vendor\Module\Model\CsvReader"
             type="Magento\Framework\File\Csv">
    <arguments>
        <argument name="delimiter" xsi:type="string">;</argument>
    </arguments>
</virtualType>

That alone does nothing yet. You then inject the virtual type into a real consumer with a standard <type> declaration:

<type name="Vendor\Module\Service\PriceImport">
    <arguments>
        <argument name="csvReader" xsi:type="object">Vendor\Module\Model\CsvReader</argument>
    </arguments>
</type>

Now PriceImport receives a Magento\Framework\File\Csv instance configured with a semicolon delimiter, while every other consumer of Magento\Framework\File\Csv across the store keeps the default comma. That narrow scope is the whole point.

The argument types you can set

Inside a virtual type, each <argument> declares an xsi:type that tells the object manager how to interpret the value. The ones you will use most are string for text, boolean for flags, number for numeric values, object to inject another class or virtual type, and array to build a collection. Arrays are where virtual types earn their keep. You assemble a pool of handlers, processors, or readers by listing <item> entries, each one an object reference by name.

One behavior worth knowing: Magento merges array arguments across di.xml files by item name. That means you can add an item to a core pool without redefining the whole pool, and you can remove an inherited item by setting it to false. A mismatched xsi:type, such as declaring an object as a string, is a frequent cause of a virtual type that compiles cleanly but injects the wrong value, so confirm the type matches what the constructor expects before you recompile.

A real example: a dedicated log file

The classic use of virtual types is giving a module its own log file. Magento’s logger is a shared Monolog instance resolved through a global preference, so adding a handler to it would mix your module’s logs into the main log for every class. Virtual types let you build a private logger instead.

First, a virtual handler that writes to a specific file:

<virtualType name="Vendor\Module\Logger\Handler\ImportFile"
             type="Magento\Framework\Logger\Handler\Base">
    <arguments>
        <argument name="fileName" xsi:type="string">/var/log/vendor_import.log</argument>
    </arguments>
</virtualType>

Then a virtual logger that uses that handler:

<virtualType name="Vendor\Module\Logger\Virtual"
             type="Magento\Framework\Logger\Monolog">
    <arguments>
        <argument name="name" xsi:type="string">vendorImport</argument>
        <argument name="handlers" xsi:type="array">
            <item name="import" xsi:type="object">Vendor\Module\Logger\Handler\ImportFile</item>
        </argument>
    </arguments>
</virtualType>

Finally, inject the virtual logger into the class that should use it:

<type name="Vendor\Module\Service\PriceImport">
    <arguments>
        <argument name="logger" xsi:type="object">Vendor\Module\Logger\Virtual</argument>
    </arguments>
</type>

The PriceImport service now logs to var/log/vendor_import.log without a single new PHP class, and nothing else in the store is affected. This is the pattern you will see most often in well-built modules.

Virtual type vs preference vs plugin

Virtual types are one of three ways to change how a class behaves. Choosing the wrong one is the usual source of over-broad or fragile customizations.

Mechanism What it changes Scope Writes new PHP?
Virtual type Constructor arguments of an existing class Only the consumers you inject it into No
Preference The implementation behind a class or interface Global, for every consumer Yes, a replacement class
Plugin (interceptor) Behavior of public methods, before/after/around The intercepted method everywhere Yes, a plugin class

A preference swaps the implementation for everyone, which is the right tool when you genuinely need to replace a class store-wide. A plugin wraps a public method to change its input or output. A virtual type does neither: it reconfigures an existing class for a chosen set of consumers. If all you need is the same logic with different arguments, a virtual type is the lightest and safest option. Our guide to plugins versus observers versus preferences covers the behavior-changing tools in depth, and virtual types sit alongside them as the configuration-only choice.

When to use one, and when not to

Reach for a virtual type when:

  • You need the same class with different constructor arguments for a specific consumer (a different file path, delimiter, pool of handlers, or collection of processors).
  • You want to assemble a configured object, such as a logger, a reader, or a composite, out of existing building blocks.
  • You want to keep the change in XML so it is easy to read in a code review and does not add classes to maintain.

Avoid a virtual type when:

  • You need to change method logic. That is a job for a plugin or, rarely, a preference.
  • You need the replacement everywhere. A global swap is a preference.
  • The configured object needs new methods or properties. At that point you need a real class.

A practical warning: virtual types are compiled. After adding or changing one, you must regenerate the DI configuration, or the change will not take effect in production mode. Run the code compiler as described in Adobe’s code compiler reference:

bin/magento setup:di:compile

This is the same discipline that keeps other structural changes safe. See our pieces on writing idempotent data patches and declarative schema with db_schema.xml for the broader pattern of making changes Magento can apply predictably.

How virtual types fit a clean module

Virtual types pay off most on modules that integrate external systems, where you often need several configured variants of the same reader, writer, or logger. A well-structured module declares its virtual types in the right area file (etc/di.xml for all areas, or etc/frontend/di.xml and etc/adminhtml/di.xml for area-specific wiring), keeps names descriptive, and injects each one exactly where it belongs. That discipline is part of what makes a build auditable and upgrade-safe, which is the standard Bemeir applies to every Magento and Hyva engagement, as well as the work we do on Shopify, BigCommerce, and Shopware.

FAQ

Do virtual types create a PHP file?

No. A virtual type exists only in di.xml. The object manager instantiates the real class named in the type attribute, applies the virtual type’s arguments, and hands the result to whatever injected it. There is no generated class file you edit.

Why is my virtual type being ignored?

Almost always one of two reasons. Either nothing injects it (a virtual type has no effect until it is referenced by name in a consumer’s arguments), or you did not recompile. Run bin/magento setup:di:compile after adding or changing a virtual type in production mode, and confirm the consumer actually points at the virtual type name.

What is the difference between a virtual type and a preference?

A preference replaces the implementation of a class or interface for every consumer in the application. A virtual type creates a separately configured instance of an existing class and affects only the consumers you inject it into. Use a preference for a global swap and a virtual type for a scoped, configuration-only variant.

Can I base a virtual type on another virtual type?

Yes. The type attribute can point at another virtual type, which lets you layer configuration. The custom-logger pattern does exactly this: the virtual logger’s handler argument references a virtual handler. Keep the chain shallow and the names clear so the wiring stays readable.

Do virtual types work in both frontend and adminhtml?

Yes. Declare them in etc/di.xml to apply to all areas, or in an area-specific file such as etc/adminhtml/di.xml to scope them. The same load order that applies to normal <type> configuration applies to virtual types.

The lightest tool that does the job

Virtual types let you customize an existing class by changing its arguments, scoped to exactly the consumers you choose, with no new PHP and no global side effects. When the change you need is configuration rather than new logic, they are the cleanest option Magento offers, and they keep your module smaller and your upgrades simpler. If you want a team that builds Adobe Commerce and Hyva modules this way, learn more about Bemeir and the technology partners we integrate across the stack.

Let us help you get started on a project with Magento 2 Virtual Types in di.xml: Reusing Classes and Replacing Arguments Without Writing New PHP and leverage our partnership to your fullest advantage. Fill out the contact form below to get started.

more articles about ecommerce

Read on the latest with Shopify, Magento, eCommerce topics and more.