Virtual Types
A virtual type is a named, pre-configured variant of an existing class, declared in di.xml with no PHP file to write.
<virtualType name="{name}" type="{RealClassName}" shared="{true|false}">...</virtualType>A virtual type is a configuration-only class. You give it a name, point it at a real class with type, and supply constructor arguments. Magento can then build it wherever that name is referenced. There is no PHP file, and no subclass.
Why it exists
Some classes are generic on purpose and only become useful once configured: a log handler needs a file name, a logger needs its handlers, a generic reader needs a converter and a schema locator. Writing a subclass for each variant would mean a PHP file whose only content is a hardcoded value. The virtual type keeps the value in configuration where it belongs. The two standard cases are a module-specific log file and a filtered variant of an existing collection class.
Shorthand and longhand
A virtual type and a subclass that hardcodes the same values produce an equivalent object. The subclass is the longhand: real PHP, type-hintable, and yours to maintain. The virtual type is the short form: four lines of XML and nothing to maintain, but the name is not a class, so it can only be referenced from di.xml.
Making it take effect
Run bin/magento cache:flush. In production mode run bin/magento setup:di:compile, because virtual types are resolved into the compiled map rather than discovered at runtime.
Example
<!-- app/code/Vendor/Import/etc/di.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<virtualType name="Vendor\Import\Logger\Handler"
type="Magento\Framework\Logger\Handler\Base">
<arguments>
<argument name="filesystem" xsi:type="object">Magento\Framework\Filesystem\Driver\File</argument>
<argument name="fileName" xsi:type="string">/var/log/vendor_import.log</argument>
</arguments>
</virtualType>
<virtualType name="Vendor\Import\Logger\Logger"
type="Magento\Framework\Logger\Monolog">
<arguments>
<argument name="name" xsi:type="string">vendor_import</argument>
<argument name="handlers" xsi:type="array">
<item name="file" xsi:type="object">Vendor\Import\Logger\Handler</item>
</argument>
</arguments>
</virtualType>
<type name="Vendor\Import\Service\FeedImporter">
<arguments>
<argument name="logger" xsi:type="object">Vendor\Import\Logger\Logger</argument>
</arguments>
</type>
</config>When to use it
- Giving a module its own log file without writing a handler class.
- Configuring a second variant of a generic core class for one consumer only.
- Replacing a one-method subclass that exists purely to hardcode a constructor value.
More examples
The class that receives the virtual type
The hint stays on the real interface, Psr\Log\LoggerInterface, which the Monolog class satisfies. The virtual type is matched by argument name in di.xml, never by type hint. This is the part people get wrong: they try to type-hint Vendor\Import\Logger\Logger and get a class-not-found error.
<?php
declare(strict_types=1);
// app/code/Vendor/Import/Service/FeedImporter.php
namespace Vendor\Import\Service;
use Psr\Log\LoggerInterface;
class FeedImporter
{
public function __construct(
private readonly LoggerInterface $logger
) {
}
public function import(string $sku): void
{
// Writes to var/log/vendor_import.log, not to system.log,
// because di.xml bound the logger argument to the virtual type.
$this->logger->info('Importing', ['sku' => $sku]);
}
}Longhand: the subclass the virtual type replaces
Equivalent to the handler virtual type in the primary example: Base resolves the path against the installation root either way. The subclass is a real class you can type-hint, and it is also a PHP file, a namespace and a review, all to hold one string. It needs no di.xml entry at all, because arguments are inherited from the parent type, so the filesystem argument app/etc/di.xml already sets on Base applies to the subclass too. Choose the subclass only when it will gain actual behaviour.
<?php
declare(strict_types=1);
// app/code/Vendor/Import/Logger/HandlerClass.php
namespace Vendor\Import\Logger;
use Magento\Framework\Logger\Handler\Base;
class HandlerClass extends Base
{
/**
* @var string
*/
protected $fileName = '/var/log/vendor_import.log';
}A virtual type used by a second virtual type
Real core configuration, and the pattern at full size: one generic filesystem reader becomes a specific reader by being handed a converter and a schema locator. Note the name looks exactly like a class, which is a deliberate core convention and a trap for newcomers searching the codebase for a file that does not exist.
<!-- app/etc/di.xml, shipped with Magento 2.4.9 -->
<virtualType name="Magento\Framework\Communication\Config\Reader\XmlReader"
type="Magento\Framework\Config\Reader\Filesystem">
<arguments>
<argument name="converter" xsi:type="object">Magento\Framework\Communication\Config\Reader\XmlReader\Converter</argument>
<argument name="schemaLocator" xsi:type="object">Magento\Framework\Communication\Config\Reader\XmlReader\SchemaLocator</argument>
</arguments>
</virtualType>
Discussion