VTYPE
XML
Deep Dive · Magento 2 di.xml

Virtual Types in di.xml:
Klassen ohne Code klonen

Wie du Klassen durch reine XML-Konfiguration spezialisierst, mehrere Logger mit verschiedenen Handlers erstellst und Decorator-Chains ohne PHP-Code aufbaust.

11 min Lesezeit
Magento 2.4.8 · di.xml
Magento DI-Konfiguration

Virtual Types sind eines der mächtigsten und am häufigsten missverstandenen Features von Magento's DI-System. Sie erlauben es, eine Klasse mehrfach mit verschiedenen Konfigurationen zu verwenden — ohne auch nur eine Zeile PHP-Code zu schreiben. Statt einer neuen Klasse erstellst du einen virtuellen Namen für eine Konfigurationsvariante derselben Klasse.

1. Was sind Virtual Types?

Stell dir vor, du hast eine Logger-Klasse die einen Handler als Parameter erwartet. Du willst drei Logger: einen für Bestellungen, einen für API-Calls, einen für Security-Events — alle nutzen dieselbe Logger-Klasse, aber mit verschiedenen Handlers.

Ohne Virtual Types müsstest du drei PHP-Klassen erstellen (OrderLogger, ApiLogger, SecurityLogger) — identisch bis auf die Handler-Konfiguration. Mit Virtual Types löst du das rein in XML:


OHNE Virtual Types:                    MIT 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-Dateien, 3×Boilerplate               0 PHP-Dateien!
    

2. Syntax und Grundstruktur


<!-- 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: Ein neuer "Name" für eine konfigurierte Version einer Klasse -->
    <virtualType
        name="Mironsoft\Module\Model\MyVirtualClass"    <!-- Neuer Name (beliebig) -->
        type="Magento\Framework\Logger\Monolog"          <!-- Echte PHP-Klasse -->
    >
        <arguments>
            <!-- Argument-Überschreibung für diese virtuelle Variante -->
            <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>

    <!-- Den Virtual Type als Dependency injizieren -->
    <type name="Mironsoft\Order\Service\OrderService">
        <arguments>
            <argument name="logger" xsi:type="object">
                Mironsoft\Module\Model\MyVirtualClass    <!-- Virtueller Name -->
            </argument>
        </arguments>
    </type>

</config>
    

Wichtige Regeln für Virtual Types:

  • Der name kann ein beliebiger String sein — er muss keine existierende PHP-Klasse sein
  • Der type muss eine existierende PHP-Klasse sein
  • Virtual Types sind nur im DI-Container bekannt — kein instanceof per Virtual-Name möglich
  • Virtual Types können andere Virtual Types als type erben

3. Klassisches Beispiel: Verschiedene Logger

Das häufigste Anwendungsfeld: Verschiedene Logger für verschiedene Module/Contexts:


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

    <!-- Handler 1: Schreibt in 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: Nutzt 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: Schreibt in 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: Nutzt 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 bekommt OrderLogger -->
    <type name="Mironsoft\Order\Service\OrderService">
        <arguments>
            <argument name="logger" xsi:type="object">
                Mironsoft\Order\Logger\OrderLogger
            </argument>
        </arguments>
    </type>

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

</config>
    

Der PHP-Code bleibt komplett unberührt — nur LoggerInterface injizieren:


<?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, // ← Bekommt OrderLogger via di.xml
    ) {}

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

4. Magento Core: Virtual Types überall

Magento selbst nutzt Virtual Types extensiv. Ein Blick in den Core zeigt die Patterns:


<!-- vendor/magento/module-catalog/etc/di.xml — vereinfacht -->
<config>

    <!-- Virtual Type für Catalog-spezifischen 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 für Product-Collection-Factory -->
    <virtualType name="Magento\Catalog\Model\ResourceModel\Product\CollectionFactory"
                 type="Magento\Framework\ObjectManager\Factory\Dynamic\Developer">
        <!-- ... -->
    </virtualType>

    <!-- Shipping-Carrier Virtual Types (viele Carrier, eine Basis-Klasse) -->
    <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>
    

Suche nach Virtual Types im Core:


# Alle virtualType-Definitionen im Core finden
grep -rn "<virtualType" vendor/magento/ | wc -l
# → Mehrere Hundert Virtual Types!

# Spezifisch nach Logger-Virtual-Types suchen
grep -rn "<virtualType" vendor/magento/ | grep -i "logger"

# Alle Virtual Types in deinem eigenen Code finden
grep -rn "<virtualType" src/app/code/
    

5. Strategy-Auswahl per Virtual Type

Virtual Types können genutzt werden, um Strategy-Pattern-Implementierungen zu konfigurieren ohne 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: Verschiedene PriceCalculator-Varianten per Virtual Type -->
<config>

    <!-- B2C: Standardmäßige Berechnung mit MwSt und Mengenrabatt -->
    <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: Netto-Preise ohne MwSt, Volumenrabatt -->
    <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>

    <!-- CustomerGroup-basierte Injektion -->
    <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 ohne PHP-Code

Virtual Types ermöglichen elegante Decorator-Chains rein per 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);
    }

    // ... weitere CacheInterface-Methoden
}
    

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

    <!-- Innerster Layer: In-Memory Cache -->
    <virtualType name="Mironsoft\Cache\Model\MemoryCache"
                 type="Mironsoft\Cache\Model\ArrayCache">
        <!-- keine weiteren Argumente -->
    </virtualType>

    <!-- Mittlerer Layer: Redis über 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>

    <!-- Äußerster Layer: Logging über 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>

    <!-- Produktservice bekommt die vollständige 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 als Factory-Konfiguration

Virtual Types können auch Factories konfigurieren — sehr nützlich für generische Factories:


<!-- Generische Factory für verschiedene Dokumententypen -->
<config>

    <!-- Virtual Type für 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 für CreditMemo-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 bekommt beide 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. Virtual Types debuggen

Da Virtual Types keine PHP-Klassen sind, kann das Debugging schwieriger sein:


# DI-Konfiguration kompilieren und validieren
bin/magento setup:di:compile

# Kompilierte DI-Konfiguration prüfen (enthält aufgelöste Virtual Types)
# Die compiled di.xml ist in generated/metadata/
ls generated/metadata/

# Einen spezifischen Virtual Type in der compilierten Konfiguration finden
grep -r "OrderLogger" generated/metadata/

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

Debugging via Xdebug: Wenn du einen Virtual Type debuggst, siehst du die echte Klasse (den type), nicht den Virtual-Type-Namen:


<?php

// Im Debugger siehst du:
// $this->logger → Objekt von Klasse Magento\Framework\Logger\Monolog
// NICHT "Mironsoft\Order\Logger\OrderLogger"

// Prüfe die Konfiguration zur Laufzeit:
/** @var \Magento\Framework\ObjectManager\ConfigInterface $diConfig */
$diConfig = $objectManager->get(\Magento\Framework\ObjectManager\ConfigInterface::class);
$instanceType = $diConfig->getInstanceType('Mironsoft\Order\Logger\OrderLogger');
// → gibt 'Magento\Framework\Logger\Monolog' zurück
    

9. Grenzen von Virtual Types

Virtual Types haben wichtige Einschränkungen die du kennen musst:

Einschränkung Detail Alternative
Kein instanceof $obj instanceof 'VirtualTypeName' funktioniert nicht Interface oder echte Basisklasse verwenden
Keine Plugins direkt Plugin auf Virtual-Type-Name nicht möglich Plugin auf echte Klasse registrieren
Nicht reflektierbar new ReflectionClass('VirtualType') schlägt fehl DI-Config-API verwenden
Nur Injectable Virtual Types müssen Injectable sein (nicht Models) Factory mit Virtual Type kombinieren

<?php

// Was NICHT funktioniert mit Virtual Types:

// ✗ instanceof mit Virtual-Type-Name
$logger = $objectManager->get('Mironsoft\Order\Logger\OrderLogger');
$logger instanceof 'Mironsoft\Order\Logger\OrderLogger'; // FALSCH → false
$logger instanceof \Magento\Framework\Logger\Monolog;    // RICHTIG → true

// ✗ Direkte Plugin-Registrierung auf Virtual Type
// <type name="Mironsoft\Order\Logger\OrderLogger">
//   <plugin .../> ← funktioniert NICHT

// ✓ Stattdessen: Plugin auf echte Klasse
// <type name="Magento\Framework\Logger\Monolog">
//   <plugin .../> ← greift für ALLE Monolog-Instanzen inkl. Virtual Types
    

10. Fazit: Wann Virtual Types einsetzen?

✓ Virtual Types nutzen für

  • Mehrere Logger mit verschiedenen Handlers
  • Strategy-Varianten einer Klasse (B2C/B2B)
  • Decorator-Chains ohne PHP-Boilerplate
  • Generische Factories für verschiedene Typen
  • Konfigurationsunterschiede, kein Logik-Unterschied

✗ Keine Virtual Types wenn

  • Die Klassen wirklich unterschiedliche Logik haben
  • instanceof-Prüfungen nötig sind
  • Plugins direkt auf dem virtuellen Namen nötig sind
  • Non-Injectable Objects konfiguriert werden sollen
  • Die Konfiguration zu komplex wird (→ eigene Klasse besser)

Zusammenfassung

Konzept
Virtual Types sind Konfigurationsvarianten einer echten Klasse — kein PHP-Code nötig, nur di.xml
Hauptanwendung
Logger mit verschiedenen Handlers — das klassische Magento-Beispiel für Virtual Types in der Praxis
Vorteil
Kein Boilerplate-Code, reine Konfiguration, einfach änderbar ohne Deployment neuer PHP-Dateien
Limit
Kein instanceof, keine direkten Plugins, keine Reflection — der virtuelle Name existiert nur im DI-Container

DI-Architektur optimieren

Logger-System aufbauen, Strategy-Patterns konfigurieren, di.xml-Architektur reviewen.

????
Logger-System
Modulspezifische Logger via Virtual Types aufbauen
????
di.xml-Review
Bestehende DI-Konfiguration analysieren und optimieren
Strategy-Pattern
Verschiedene Algorithmus-Varianten per Virtual Type konfigurieren

Häufige Fragen zu Virtual Types in Magento

Was sind Virtual Types in Magento 2? +
Konfigurationsvarianten einer existierenden PHP-Klasse, rein über di.xml definiert — ohne neuen PHP-Code. Ein Virtual Type gibt einer Klasse einen neuen Namen mit geänderten Konstruktor-Argumenten.
Was ist der häufigste Anwendungsfall? +
Modulspezifische Logger: Statt für jeden Logger eine PHP-Klasse zu erstellen, werden Virtual Types auf Basis von Monolog erstellt — jeder mit einem anderen Handler der in eine andere Log-Datei schreibt.
Wie unterscheidet sich ein Virtual Type von einer Preference? +
Eine Preference ersetzt eine Klasse durch eine andere. Ein Virtual Type erstellt einen neuen Namen für eine konfigurierte Version ohne die Originalklasse zu ändern. Virtual Types sind additiv, Preferences ersetzend.
Kann ich instanceof mit einem Virtual Type prüfen? +
Nein. instanceof prüft gegen PHP-Klassen, aber Virtual-Type-Namen sind keine PHP-Klassen. Gegen die echte Basisklasse (den 'type') prüfen.
Kann ich ein Plugin auf einen Virtual Type registrieren? +
Nein, direkte Plugins auf Virtual-Type-Namen funktionieren nicht. Das Plugin muss auf die echte Klasse (den 'type') registriert werden.
Können Virtual Types andere Virtual Types als Basis haben? +
Ja. Ein Virtual Type kann im 'type'-Attribut einen anderen Virtual Type referenzieren und dessen Konfiguration erben — für hierarchische Konfiguration.
Wie finde ich alle Virtual Types in einer Installation? +
grep -rn '<virtualType' vendor/magento/ für Core. grep -rn '<virtualType' src/app/code/ für eigene Module. Nach setup:di:compile in generated/metadata/ sichtbar.
Wann eigene PHP-Klasse statt Virtual Type? +
Eigene Klasse wenn: unterschiedliche Logik, instanceof nötig, Reflection verwendet wird, oder DI-Konfiguration zu komplex. Virtual Types nur für reine Konfigurationsvarianten.
Wie nutze ich Virtual Types für Decorator-Chains? +
Jeder Virtual Type referenziert den nächsten in der Chain als 'inner'-Argument. LoggingDecorator(inner=RedisCache(inner=MemoryCache)) — ohne eine neue PHP-Klasse.
Werden Virtual Types beim Kompilieren aufgelöst? +
Ja. setup:di:compile löst alle Virtual Types auf und generiert optimierte Konfigurationsdateien in generated/metadata/. Im Production-Mode bei Startup aufgelöst und gecacht.