Inhalt
- 1. Was sind Virtual Types?
- 2. Syntax und Grundstruktur
- 3. Klassisches Beispiel: Verschiedene Logger
- 4. Magento Core: Virtual Types überall
- 5. Strategy-Auswahl per Virtual Type
- 6. Decorator-Chain ohne PHP-Code
- 7. Virtual Type als Factory-Konfiguration
- 8. Virtual Types debuggen
- 9. Grenzen von Virtual Types
- 10. Fazit: Wann Virtual Types einsetzen?
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
namekann ein beliebiger String sein — er muss keine existierende PHP-Klasse sein - Der
typemuss eine existierende PHP-Klasse sein - Virtual Types sind nur im DI-Container bekannt — kein
instanceofper Virtual-Name möglich - Virtual Types können andere Virtual Types als
typeerben
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
DI-Architektur optimieren
Logger-System aufbauen, Strategy-Patterns konfigurieren, di.xml-Architektur reviewen.