Contents
- 1. What are Virtual Types?
- 2. Syntax and basic structure
- 3. Classic example: multiple loggers
- 4. Magento core: Virtual Types everywhere
- 5. Strategy selection via Virtual Type
- 6. Decorator chain without PHP code
- 7. Virtual Type as factory configuration
- 8. Debugging Virtual Types
- 9. Limits of Virtual Types
- 10. Conclusion: when to use Virtual Types?
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
namecan be any arbitrary string, it does not have to be an existing PHP class - The
typemust be an existing PHP class - Virtual Types are only known inside the DI container, no
instanceofcheck 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
Optimize your DI architecture
Build a logger system, configure strategy patterns, review your di.xml architecture.