Virtual Types in Magento 2 di.xml: Clone Classes Without Code | Mironsoft
AI generated
VTYPE
XML
Deep Dive · Magento 2 di.xml

Virtual Types in di.xml:
Clone Classes Without Code

How to specialize classes through pure XML configuration, create multiple loggers with different handlers, and build decorator chains without any PHP code.

11 min read
Magento 2.4.8 · di.xml
Magento DI configuration

Virtual Types are one of the most powerful and most frequently misunderstood features of Magento's DI system. They let you use a single class multiple times with different configurations, without writing a single line of PHP code. Instead of creating a new class, you create a virtual name for a configured variant of the same class.

1. What are Virtual Types?

Imagine you have a Logger class that expects a handler as a parameter. You want three loggers: one for orders, one for API calls, one for security events, all using the same logger class but with different handlers.

Without Virtual Types you would have to create three PHP classes (OrderLogger, ApiLogger, SecurityLogger), identical except for the handler configuration. With Virtual Types you solve this purely in XML:


WITHOUT Virtual Types:                 WITH Virtual Types:
───────────────────                    ─────────────────
OrderLogger.php                        di.xml:
  extends Logger                         virtualType name="OrderLogger"
  constructor(OrderHandler)               type="Monolog\Logger"
                                          argument: OrderHandler
ApiLogger.php
  extends Logger                         virtualType name="ApiLogger"
  constructor(ApiHandler)                 type="Monolog\Logger"
                                          argument: ApiHandler
SecurityLogger.php
  extends Logger                         virtualType name="SecurityLogger"
  constructor(SecurityHandler)            type="Monolog\Logger"
                                          argument: SecurityHandler

3 PHP files, 3x boilerplate                0 PHP files!
    

2. Syntax and basic structure


<!-- app/code/Mironsoft/Module/etc/di.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">

    <!-- Virtual Type: a new "name" for a configured version of a class -->
    <virtualType
        name="Mironsoft\Module\Model\MyVirtualClass"    <!-- New name (arbitrary) -->
        type="Magento\Framework\Logger\Monolog"          <!-- Real PHP class -->
    >
        <arguments>
            <!-- Argument override for this virtual variant -->
            <argument name="name" xsi:type="string">mironsoft-order</argument>
            <argument name="handlers" xsi:type="array">
                <item name="system" xsi:type="object">
                    Mironsoft\Module\Model\Handler\OrderHandler
                </item>
            </argument>
        </arguments>
    </virtualType>

    <!-- Inject the Virtual Type as a dependency -->
    <type name="Mironsoft\Order\Service\OrderService">
        <arguments>
            <argument name="logger" xsi:type="object">
                Mironsoft\Module\Model\MyVirtualClass    <!-- Virtual name -->
            </argument>
        </arguments>
    </type>

</config>
    

Important rules for Virtual Types:

  • The name can be any arbitrary string, it does not have to be an existing PHP class
  • The type must be an existing PHP class
  • Virtual Types are only known inside the DI container, no instanceof check via the virtual name is possible
  • Virtual Types can inherit from other Virtual Types as their type

3. Classic example: multiple loggers

The most common use case: different loggers for different modules/contexts:


<!-- app/code/Mironsoft/Order/etc/di.xml -->
<config>

    <!-- Handler 1: writes to var/log/mironsoft-order.log -->
    <virtualType name="Mironsoft\Order\Logger\Handler\OrderHandler"
                 type="Magento\Framework\Logger\Handler\Base">
        <arguments>
            <argument name="fileName" xsi:type="string">/var/log/mironsoft-order.log</argument>
        </arguments>
    </virtualType>

    <!-- Logger 1: uses OrderHandler -->
    <virtualType name="Mironsoft\Order\Logger\OrderLogger"
                 type="Magento\Framework\Logger\Monolog">
        <arguments>
            <argument name="name" xsi:type="string">mironsoft-order</argument>
            <argument name="handlers" xsi:type="array">
                <item name="system" xsi:type="object">
                    Mironsoft\Order\Logger\Handler\OrderHandler
                </item>
            </argument>
        </arguments>
    </virtualType>

    <!-- Handler 2: writes to var/log/mironsoft-api.log -->
    <virtualType name="Mironsoft\Order\Logger\Handler\ApiHandler"
                 type="Magento\Framework\Logger\Handler\Base">
        <arguments>
            <argument name="fileName" xsi:type="string">/var/log/mironsoft-api.log</argument>
        </arguments>
    </virtualType>

    <!-- Logger 2: uses ApiHandler -->
    <virtualType name="Mironsoft\Order\Logger\ApiLogger"
                 type="Magento\Framework\Logger\Monolog">
        <arguments>
            <argument name="name" xsi:type="string">mironsoft-api</argument>
            <argument name="handlers" xsi:type="array">
                <item name="system" xsi:type="object">
                    Mironsoft\Order\Logger\Handler\ApiHandler
                </item>
            </argument>
        </arguments>
    </virtualType>

    <!-- OrderService gets OrderLogger -->
    <type name="Mironsoft\Order\Service\OrderService">
        <arguments>
            <argument name="logger" xsi:type="object">
                Mironsoft\Order\Logger\OrderLogger
            </argument>
        </arguments>
    </type>

    <!-- ApiClient gets ApiLogger -->
    <type name="Mironsoft\Order\Http\ApiClient">
        <arguments>
            <argument name="logger" xsi:type="object">
                Mironsoft\Order\Logger\ApiLogger
            </argument>
        </arguments>
    </type>

</config>
    

The PHP code stays completely untouched, just inject LoggerInterface:


<?php

declare(strict_types=1);

namespace Mironsoft\Order\Service;

use Psr\Log\LoggerInterface;

/**
 * OrderService receives the order-specific logger via DI.
 * No reference to "OrderLogger", just LoggerInterface.
 */
final class OrderService
{
    public function __construct(
        private readonly LoggerInterface $logger, // ← Gets OrderLogger via di.xml
    ) {}

    public function process(int $orderId): void
    {
        $this->logger->info('Processing order', ['order_id' => $orderId]);
        // Log goes to var/log/mironsoft-order.log
    }
}
    

4. Magento core: Virtual Types everywhere

Magento itself uses Virtual Types extensively. A look into the core reveals the patterns:


<!-- vendor/magento/module-catalog/etc/di.xml (simplified) -->
<config>

    <!-- Virtual Type for a catalog-specific logger -->
    <virtualType name="Magento\Catalog\Model\Logger"
                 type="Magento\Framework\Logger\Monolog">
        <arguments>
            <argument name="name" xsi:type="string">Magento_Catalog</argument>
        </arguments>
    </virtualType>

    <!-- Virtual Type for the product collection factory -->
    <virtualType name="Magento\Catalog\Model\ResourceModel\Product\CollectionFactory"
                 type="Magento\Framework\ObjectManager\Factory\Dynamic\Developer">
        <!-- ... -->
    </virtualType>

    <!-- Shipping carrier Virtual Types (many carriers, one base class) -->
    <virtualType name="Magento\Shipping\Model\Rate\Result\ErrorFactory"
                 type="Magento\Framework\ObjectManager\Factory\Dynamic\Developer">
        <arguments>
            <argument name="instanceName" xsi:type="string">
                Magento\Shipping\Model\Rate\Result\Error
            </argument>
        </arguments>
    </virtualType>

</config>
    

Searching for Virtual Types in the core:


# Find all virtualType definitions in the core
grep -rn "<virtualType" vendor/magento/ | wc -l
# → Several hundred Virtual Types!

# Search specifically for logger Virtual Types
grep -rn "<virtualType" vendor/magento/ | grep -i "logger"

# Find all Virtual Types in your own code
grep -rn "<virtualType" src/app/code/
    

5. Strategy selection via Virtual Type

Virtual Types can be used to configure strategy pattern implementations without any PHP code:


<?php

declare(strict_types=1);

namespace Mironsoft\Pricing\Model;

use Mironsoft\Pricing\Api\TaxCalculatorInterface;
use Mironsoft\Pricing\Api\DiscountStrategyInterface;

/**
 * Price calculator with injected strategies, configured via Virtual Types.
 */
final class PriceCalculator
{
    public function __construct(
        private readonly TaxCalculatorInterface $taxCalculator,
        private readonly DiscountStrategyInterface $discountStrategy,
    ) {}

    public function calculate(float $basePrice): float
    {
        $discounted = $this->discountStrategy->apply($basePrice);
        return $this->taxCalculator->calculate($discounted);
    }
}
    

<!-- di.xml: different PriceCalculator variants via Virtual Type -->
<config>

    <!-- B2C: standard calculation with VAT and quantity discount -->
    <virtualType name="Mironsoft\Pricing\Model\B2cPriceCalculator"
                 type="Mironsoft\Pricing\Model\PriceCalculator">
        <arguments>
            <argument name="taxCalculator" xsi:type="object">
                Mironsoft\Pricing\Model\Tax\GermanVatCalculator
            </argument>
            <argument name="discountStrategy" xsi:type="object">
                Mironsoft\Pricing\Model\Discount\QuantityDiscountStrategy
            </argument>
        </arguments>
    </virtualType>

    <!-- B2B: net prices without VAT, volume discount -->
    <virtualType name="Mironsoft\Pricing\Model\B2bPriceCalculator"
                 type="Mironsoft\Pricing\Model\PriceCalculator">
        <arguments>
            <argument name="taxCalculator" xsi:type="object">
                Mironsoft\Pricing\Model\Tax\NetPriceCalculator
            </argument>
            <argument name="discountStrategy" xsi:type="object">
                Mironsoft\Pricing\Model\Discount\VolumeDiscountStrategy
            </argument>
        </arguments>
    </virtualType>

    <!-- Customer-group-based injection -->
    <type name="Mironsoft\Pricing\Block\ProductPrice">
        <arguments>
            <argument name="b2cCalculator" xsi:type="object">
                Mironsoft\Pricing\Model\B2cPriceCalculator
            </argument>
            <argument name="b2bCalculator" xsi:type="object">
                Mironsoft\Pricing\Model\B2bPriceCalculator
            </argument>
        </arguments>
    </type>

</config>
    

6. Decorator chain without PHP code

Virtual Types enable elegant decorator chains purely via XML:


<?php

declare(strict_types=1);

namespace Mironsoft\Cache\Model;

use Psr\SimpleCache\CacheInterface;

/**
 * Cache decorator that adds logging to any CacheInterface implementation.
 */
final class LoggingCacheDecorator implements CacheInterface
{
    public function __construct(
        private readonly CacheInterface $inner,
        private readonly \Psr\Log\LoggerInterface $logger,
        private readonly string $decoratorName = 'cache',
    ) {}

    public function get(string $key, mixed $default = null): mixed
    {
        $result = $this->inner->get($key, $default);
        $hit = $result !== $default;
        $this->logger->debug("{$this->decoratorName} " . ($hit ? 'HIT' : 'MISS'), ['key' => $key]);
        return $result;
    }

    public function set(string $key, mixed $value, null|int|\DateInterval $ttl = null): bool
    {
        $this->logger->debug("{$this->decoratorName} SET", ['key' => $key, 'ttl' => $ttl]);
        return $this->inner->set($key, $value, $ttl);
    }

    // ... further CacheInterface methods
}
    

<!-- Decorator chain via Virtual Types: Log → Redis → Memory -->
<config>

    <!-- Innermost layer: in-memory cache -->
    <virtualType name="Mironsoft\Cache\Model\MemoryCache"
                 type="Mironsoft\Cache\Model\ArrayCache">
        <!-- no further arguments -->
    </virtualType>

    <!-- Middle layer: Redis over memory -->
    <virtualType name="Mironsoft\Cache\Model\RedisWithMemoryCache"
                 type="Mironsoft\Cache\Model\RedisCache">
        <arguments>
            <argument name="fallback" xsi:type="object">
                Mironsoft\Cache\Model\MemoryCache
            </argument>
        </arguments>
    </virtualType>

    <!-- Outermost layer: logging over Redis -->
    <virtualType name="Mironsoft\Cache\Model\LoggedRedisCache"
                 type="Mironsoft\Cache\Model\LoggingCacheDecorator">
        <arguments>
            <argument name="inner" xsi:type="object">
                Mironsoft\Cache\Model\RedisWithMemoryCache
            </argument>
            <argument name="decoratorName" xsi:type="string">product-cache</argument>
        </arguments>
    </virtualType>

    <!-- Product service gets the complete chain -->
    <type name="Mironsoft\Catalog\Service\ProductCacheService">
        <arguments>
            <argument name="cache" xsi:type="object">
                Mironsoft\Cache\Model\LoggedRedisCache
            </argument>
        </arguments>
    </type>

</config>
    

7. Virtual Type as factory configuration

Virtual Types can also configure factories, very useful for generic factories:


<!-- Generic factory for different document types -->
<config>

    <!-- Virtual Type for the invoice factory -->
    <virtualType name="Mironsoft\Document\Model\InvoiceFactory"
                 type="Magento\Framework\ObjectManager\Factory\Dynamic\Developer">
        <arguments>
            <argument name="instanceName" xsi:type="string">
                Mironsoft\Document\Model\Invoice
            </argument>
        </arguments>
    </virtualType>

    <!-- Virtual Type for the credit memo factory -->
    <virtualType name="Mironsoft\Document\Model\CreditMemoFactory"
                 type="Magento\Framework\ObjectManager\Factory\Dynamic\Developer">
        <arguments>
            <argument name="instanceName" xsi:type="string">
                Mironsoft\Document\Model\CreditMemo
            </argument>
        </arguments>
    </virtualType>

    <!-- DocumentService gets both factories -->
    <type name="Mironsoft\Document\Service\DocumentService">
        <arguments>
            <argument name="invoiceFactory" xsi:type="object">
                Mironsoft\Document\Model\InvoiceFactory
            </argument>
            <argument name="creditMemoFactory" xsi:type="object">
                Mironsoft\Document\Model\CreditMemoFactory
            </argument>
        </arguments>
    </type>

</config>
    

8. Debugging Virtual Types

Since Virtual Types are not PHP classes, debugging can be trickier:


# Compile and validate the DI configuration
bin/magento setup:di:compile

# Inspect the compiled DI configuration (contains resolved Virtual Types)
# The compiled di.xml lives in generated/metadata/
ls generated/metadata/

# Find a specific Virtual Type in the compiled configuration
grep -r "OrderLogger" generated/metadata/

# List Virtual Types in a module
grep -rn "virtualType" src/app/code/Mironsoft/ \
    | grep "name=" \
    | sed 's/.*name="\([^"]*\)".*/\1/'
    

Debugging via Xdebug: when you debug a Virtual Type, you see the real class (the type), not the virtual type name:


<?php

// In the debugger you see:
// $this->logger → object of class Magento\Framework\Logger\Monolog
// NOT "Mironsoft\Order\Logger\OrderLogger"

// Check the configuration at runtime:
/** @var \Magento\Framework\ObjectManager\ConfigInterface $diConfig */
$diConfig = $objectManager->get(\Magento\Framework\ObjectManager\ConfigInterface::class);
$instanceType = $diConfig->getInstanceType('Mironsoft\Order\Logger\OrderLogger');
// → returns 'Magento\Framework\Logger\Monolog'
    

9. Limits of Virtual Types

Virtual Types have important limitations you need to be aware of:

Limitation Detail Alternative
No instanceof $obj instanceof 'VirtualTypeName' does not work Use an interface or the real base class
No plugins directly A plugin on a Virtual Type name is not possible Register the plugin on the real class
Not reflectable new ReflectionClass('VirtualType') fails Use the DI config API
Injectable only Virtual Types must be injectable (not models) Combine a factory with a Virtual Type

<?php

// What does NOT work with Virtual Types:

// ✗ instanceof with a Virtual Type name
$logger = $objectManager->get('Mironsoft\Order\Logger\OrderLogger');
$logger instanceof 'Mironsoft\Order\Logger\OrderLogger'; // WRONG → false
$logger instanceof \Magento\Framework\Logger\Monolog;    // CORRECT → true

// ✗ Direct plugin registration on a Virtual Type
// <type name="Mironsoft\Order\Logger\OrderLogger">
//   <plugin .../> ← does NOT work

// ✓ Instead: plugin on the real class
// <type name="Magento\Framework\Logger\Monolog">
//   <plugin .../> ← applies to ALL Monolog instances including Virtual Types
    

10. Conclusion: when to use Virtual Types?

✓ Use Virtual Types for

  • Multiple loggers with different handlers
  • Strategy variants of a class (B2C/B2B)
  • Decorator chains without PHP boilerplate
  • Generic factories for different types
  • Configuration differences, not logic differences

✗ No Virtual Types when

  • The classes really have different logic
  • instanceof checks are needed
  • Plugins are needed directly on the virtual name
  • Non-injectable objects need to be configured
  • The configuration gets too complex (a dedicated class is better)

Summary

Concept
Virtual Types are configuration variants of a real class, no PHP code needed, just di.xml
Main use case
Loggers with different handlers, the classic Magento example for Virtual Types in practice
Advantage
No boilerplate code, pure configuration, easy to change without deploying new PHP files
Limit
No instanceof, no direct plugins, no reflection, the virtual name only exists inside the DI container

Optimize your DI architecture

Build a logger system, configure strategy patterns, review your di.xml architecture.

????
Logger system
Build module-specific loggers via Virtual Types
????
di.xml review
Analyze and optimize your existing DI configuration
Strategy pattern
Configure different algorithm variants via Virtual Type

Frequently asked questions about Virtual Types in Magento

What are Virtual Types in Magento 2? +
Configuration variants of an existing PHP class, defined purely via di.xml, without any new PHP code. A Virtual Type gives a class a new name with changed constructor arguments.
What is the most common use case? +
Module-specific loggers: instead of creating a PHP class for each logger, Virtual Types are created based on Monolog, each with a different handler that writes to a different log file.
How does a Virtual Type differ from a Preference? +
A Preference replaces a class with another. A Virtual Type creates a new name for a configured version without changing the original class. Virtual Types are additive, Preferences are replacing.
Can I check instanceof with a Virtual Type? +
No. instanceof checks against PHP classes, but Virtual Type names are not PHP classes. Check against the real base class (the 'type') instead.
Can I register a plugin on a Virtual Type? +
No, direct plugins on Virtual Type names do not work. The plugin must be registered on the real class (the 'type').
Can Virtual Types use other Virtual Types as their base? +
Yes. A Virtual Type can reference another Virtual Type in its 'type' attribute and inherit its configuration, enabling hierarchical configuration.
How do I find all Virtual Types in an installation? +
grep -rn '<virtualType' vendor/magento/ for the core. grep -rn '<virtualType' src/app/code/ for your own modules. After setup:di:compile they are visible in generated/metadata/.
When should I create a dedicated class instead of a Virtual Type? +
Create a dedicated class when: the logic differs, instanceof is needed, reflection is used, or the DI configuration becomes too complex. Virtual Types are only for pure configuration variants.
How do I use Virtual Types for decorator chains? +
Each Virtual Type references the next one in the chain as an 'inner' argument. LoggingDecorator(inner=RedisCache(inner=MemoryCache)), without a single new PHP class.
Are Virtual Types resolved during compilation? +
Yes. setup:di:compile resolves all Virtual Types and generates optimized configuration files in generated/metadata/. In production mode they are resolved and cached at startup.