mironsoft.de › Blog › Design Patterns
Magento 2 · Design Patterns
Injectable & Virtual Types
in Magento 2

Warum kann ein Product-Objekt nicht direkt per Constructor injiziert werden — aber ein ProductRepository schon? Die Unterscheidung zwischen Injectable und Non-Injectable Objects ist fundamental für Magento 2 DI. Virtual Types lösen dazu ein häufiges di.xml-Problem elegant.

15 Min. Lesezeit PHP 8.4 Magento 2.4.8

Was sind Injectable Objects?

Injectable Objects sind das Fundament der Magento 2 Dependency Injection. Der Begriff beschreibt Klassen, die sicher per Constructor Injection in andere Klassen eingebunden werden können, weil sie stateless oder zumindest request-scoped stateless sind. Stateless bedeutet: Die Klasse hält keinen variablen Zustand zwischen einzelnen Methodenaufrufen. Jeder Aufruf einer Injectable-Klasse ist unabhängig von vorherigen Aufrufen. Das macht sie sicher für das Sharing — eine einzige Instanz kann von vielen Stellen im Code genutzt werden, ohne dass sich die Aufrufer gegenseitig beeinflussen.

Typische Injectable Objects in Magento 2 sind Services (Klassen, die Geschäftslogik kapseln), Repositories (die den Datenbankzugriff über ein Interface abstrahieren), Logger (die Lognachrichten in Dateien oder andere Ziele schreiben), Factories (die neue Instanzen von Non-Injectable Objects erstellen) und Helpers (die Hilfsfunktionen bereitstellen). All diese Klassen haben gemeinsam, dass sie für sich selbst keinen veränderlichen Zustand halten und damit sicher als Shared Instance im DI-Container verwaltet werden können.

Der Magento 2 DI-Container verwaltet Injectable Objects standardmäßig als Shared Instances. Das bedeutet: Wenn zwei verschiedene Klassen denselben PostRepositoryInterface injiziert bekommen, erhalten sie dieselbe Repository-Instanz — der Container erstellt das Repository nur einmal. Dieses Verhalten ist identisch mit dem Singleton-Pattern, aber sauber durch den DI-Container gemanagt, nicht durch eine statische getInstance()-Methode. In PHP 8.4 mit readonly Properties und Constructor Property Promotion lassen sich Injectable Objects besonders sauber und unveränderlich implementieren.

Ein ViewModel in Magento 2 (Klasse die ArgumentInterface implementiert) ist ein klassisches Beispiel für ein Injectable Object. Er hält keine Request-spezifischen Daten in seinem State, sondern berechnet und liefert diese bei jedem Methodenaufruf neu aus seinen injizierten Abhängigkeiten. Damit ist er sicher für das Sharing über mehrere Block-Instanzen hinweg, auch wenn diese denselben ViewModel injiziert bekommen.

Was sind Non-Injectable Objects?

Non-Injectable Objects sind das Gegenstück: Klassen, die stateful sind und einen variablen Zustand speichern. Das prominenteste Beispiel ist das Magento 2 Model, das eine konkrete Datenbankzeile repräsentiert. Ein Product-Objekt hat eine SKU, einen Preis, einen Status und viele andere Attribute — diese Daten sind der Zustand des Objekts und unterscheiden sich bei jedem Produkt. Es macht keinen Sinn, ein und dasselbe Product-Objekt für alle Produkte zu teilen.

Collections sind ebenfalls Non-Injectable Objects. Eine ProductCollection enthält eine bestimmte Menge an Produkten, gefiltert nach bestimmten Kriterien. Diese Filterung und der resultierende Inhalt sind der Zustand der Collection — dieser Zustand unterscheidet sich je nach Kontext, in dem die Collection verwendet wird. Würde man die Collection direkt per Constructor injizieren, bekämen alle Injektionspunkte dieselbe leere (oder vorher befüllte) Collection, was zu falschen Ergebnissen führen würde.

Value Objects wie eine Price- oder Address-Klasse sind ebenfalls non-injectable, weil sie spezifische Werte speichern, die sich je nach Kontext unterscheiden. Ein Preis-Objekt mit dem Wert 29.99 EUR ist nicht dasselbe wie ein Preis-Objekt mit 59.99 CHF. Jede Verwendung benötigt eine eigene Instanz mit eigenen Werten.

<?php
declare(strict_types=1);

namespace Mironsoft\Catalog\Service;

use Magento\Catalog\Model\ProductFactory;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Psr\Log\LoggerInterface;

/**
 * Product service — correct DI usage for injectable and non-injectable objects.
 */
class ProductService
{
    public function __construct(
        // CORRECT: ProductRepository is injectable — stateless service
        private readonly ProductRepositoryInterface $productRepository,
        // CORRECT: ProductFactory is injectable — stateless factory
        private readonly ProductFactory $productFactory,
        // CORRECT: Logger is injectable — stateless logging service
        private readonly LoggerInterface $logger
        // WRONG would be: private readonly Product $product
        // Product is non-injectable — it holds entity state
    ) {}

    /**
     * Create a new product with the given data.
     */
    public function createNewProduct(string $sku, float $price): \Magento\Catalog\Api\Data\ProductInterface
    {
        // Non-injectable: use factory to create a fresh instance
        $product = $this->productFactory->create();
        $product->setSku($sku);
        $product->setPrice($price);
        $product->setAttributeSetId(4);
        $product->setTypeId('simple');
        $product->setVisibility(4);
        $product->setStatus(1);

        $this->logger->info('Creating new product', ['sku' => $sku]);

        return $this->productRepository->save($product);
    }
}

Factories: Der richtige Weg für Non-Injectable Objects

Factories sind das Bindeglied zwischen dem DI-System und den Non-Injectable Objects. Eine Factory ist selbst ein Injectable Object (stateless) und stellt über die create()-Methode neue Instanzen des zugehörigen Non-Injectable Objects bereit. Magento 2 generiert Factory-Klassen automatisch während des Compile-Prozesses (setup:di:compile). Die Naming-Convention ist denkbar einfach: Klassenname + Factory. Für die Klasse Mironsoft\Blog\Model\Post wird automatisch Mironsoft\Blog\Model\PostFactory generiert.

Die generierte Factory-Klasse liegt im generated/code/-Verzeichnis und enthält eine einzige create()-Methode, die intern den ObjectManager verwendet, um eine neue Instanz der Zielklasse zu erzeugen. Dadurch werden auch alle Constructor-Argumente der Zielklasse per Dependency Injection aufgelöst. Man muss also keine Arguments an create() übergeben — alle Abhängigkeiten werden automatisch injiziert. Optional kann man ein Array mit überschriebenen Argumenten übergeben.

In PHP 8.4 mit Constructor Property Promotion und readonly Properties können sowohl die Factory als auch das Model, das sie erstellt, sehr sauber implementiert werden. Die Factory selbst nutzt Constructor Property Promotion für den ObjectManager. Das erstellte Model-Objekt kann Properties readonly deklarieren, die nach der Erstellung nicht mehr geändert werden sollen — was allerdings im Widerspruch zur typischen Magento 2 setData/getData-Architektur steht und nur für wirklich unveränderliche Value Objects empfohlen wird.

Ein häufiger Fehler ist die Verwechslung von Factory und Repository. Die Factory erstellt neue leere Instanzen — sie lädt keine Daten aus der Datenbank. Das Repository lädt bestehende Entities, erstellt intern über die Factory neue Instanzen und befüllt sie mit Datenbankdaten. Factory und Repository arbeiten zusammen: Das Repository nutzt intern die Factory, um frische Model-Instanzen zu erstellen, die dann mit Daten aus dem ResourceModel befüllt werden.

Shared vs. Non-Shared Instances

Der Magento 2 DI-Container unterscheidet zwischen Shared und Non-Shared Instances. Shared Instances werden einmal erstellt und wiederverwendet — das entspricht dem Singleton-Pattern auf Anforderung. Non-Shared Instances werden bei jedem Aufruf neu erstellt. Standardmäßig sind alle Injectable Objects Shared Instances. Non-Injectable Objects, die über Factories erstellt werden, sind immer Non-Shared Instances.

Das Shared-Verhalten kann in der di.xml explizit konfiguriert werden. Mit shared="false" auf einem type-Element wird der DI-Container angewiesen, bei jeder Injection eine neue Instanz zu erstellen. Das ist selten notwendig, kann aber bei Klassen sinnvoll sein, die einen Request-spezifischen Zustand aufbauen müssen, der sich zwischen Injektionspunkten unterscheiden soll.

In der Entwicklung mit PHP 8.4 kann man das Shared-Verhalten mit readonly-Properties kombinieren. Ein Injectable Object, das seinen Zustand nach der Initialisierung nie ändert, kann alle Properties als readonly deklarieren. Der DI-Container erstellt die Instanz einmal, initialisiert alle readonly Properties im Constructor, und gibt dieselbe unveränderliche Instanz an alle Abhängigen weiter. Das entspricht dem Konzept eines echten Singletons, ist aber durch den DI-Container verwaltet und in Unit Tests vollständig durch Mocks ersetzbar.

Im Praxisalltag ist das Shared-Konzept bei Proxies besonders relevant. Ein Proxy ist ein Lazy-Loading-Wrapper für eine Injectable Klasse: Der Proxy selbst ist leichtgewichtig und wird sofort erstellt. Die eigentliche Klasse (z.B. ein schwerer Service, der Datenbankverbindungen aufbaut) wird erst beim ersten echten Methodenaufruf instanziiert. Das verbessert die Performance beim Bootstrap, weil schwere Services nicht immer benötigt werden. Proxies werden durch das Anhängen von \Proxy an den Klassennamen im Constructor konfiguriert.

Virtual Types in di.xml — Konzept und Syntax

Virtual Types sind eine der elegantesten Funktionen des Magento 2 DI-Systems. Ein Virtual Type ist eine "Klasse ohne PHP-Datei" — er wird ausschließlich in der di.xml definiert und konfiguriert eine existierende PHP-Klasse mit anderen Constructor-Argumenten. Der DI-Container behandelt den Virtual Type wie eine eigenständige Klasse mit einem eindeutigen Namen. Es wird jedoch kein PHP-Code generiert; intern verweist der Virtual Type auf die originale PHP-Klasse mit einer spezifischen DI-Konfiguration.

Das Kernproblem, das Virtual Types lösen, ist Code-Duplikation durch DI-Konfiguration. Stellen Sie sich vor, Sie brauchen dieselbe generische Klasse in zwei verschiedenen Konfigurationen — zum Beispiel einen HTTP-Client mit zwei verschiedenen Base-URLs. Ohne Virtual Types müssten Sie entweder zwei PHP-Klassen schreiben (Duplikat!) oder die Konfiguration zur Laufzeit übergeben (was die Testbarkeit erschwert). Mit Virtual Types definieren Sie zwei "virtuelle" Klassen in der di.xml, die denselben PHP-Code verwenden, aber verschiedene Constructor-Argumente erhalten.

Die Syntax eines Virtual Types in der di.xml ist intuitiv: Man verwendet das <virtualType>-Element mit einem name-Attribut (das ist der eindeutige Klassenname des Virtual Types) und einem type-Attribut (das ist die originale PHP-Klasse). Innerhalb des <virtualType>-Elements können Constructor-Argumente über <arguments> konfiguriert werden. Diese Argumente überschreiben die Default-Werte der originalen Klasse.

Virtual Types können andere Virtual Types als Typ-Argument verwenden. Das ermöglicht eine hierarchische Konfiguration, wie sie beim Logger-Setup in Magento 2 typisch ist: Ein Handler-Virtual-Type konfiguriert einen Monolog Handler mit einem bestimmten Dateinamen. Ein Logger-Virtual-Type konfiguriert einen Monolog Logger mit diesem Handler. Die Klassen bleiben dabei dieselben PHP-Klassen aus dem Magento Framework — nur die Konfiguration unterscheidet sich.

Praxisbeispiel: Eigener Modul-Logger per Virtual Type

Das häufigste und bekannteste Anwendungsbeispiel für Virtual Types in Magento 2 ist die Konfiguration eines eigenen Modul-Loggers. Das Ziel: Log-Meldungen des eigenen Moduls sollen nicht in der allgemeinen system.log landen, sondern in einer eigenen Datei wie var/log/mironsoft_blog.log. Ohne Virtual Types müsste man dafür eigene PHP-Klassen für den Handler und den Logger schreiben. Mit Virtual Types reicht eine di.xml-Konfiguration.

Das Muster ist immer zweistufig: Zuerst definiert man einen Handler-Virtual-Type, der die Monolog-Handler-Klasse mit dem gewünschten Dateinamen konfiguriert. Dann definiert man einen Logger-Virtual-Type, der die Monolog-Logger-Klasse konfiguriert und dabei den Handler-Virtual-Type als Handler einträgt. Schließlich injiziert man den Logger-Virtual-Type in die eigenen Klassen über eine type-Konfiguration.

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

    <!-- Step 1: Handler Virtual Type — configures log file destination -->
    <virtualType name="Mironsoft\Blog\Logger\Handler"
                 type="Magento\Framework\Logger\Handler\Base">
        <arguments>
            <argument name="fileName" xsi:type="string">/var/log/mironsoft_blog.log</argument>
        </arguments>
    </virtualType>

    <!-- Step 2: Logger Virtual Type — uses our custom handler -->
    <virtualType name="Mironsoft\Blog\Logger\Logger"
                 type="Magento\Framework\Logger\Monolog">
        <arguments>
            <argument name="name" xsi:type="string">mironsoft_blog</argument>
            <argument name="handlers" xsi:type="array">
                <item name="system" xsi:type="object">Mironsoft\Blog\Logger\Handler</item>
            </argument>
        </arguments>
    </virtualType>

    <!-- Step 3: Inject the custom logger into our classes -->
    <type name="Mironsoft\Blog\Model\PostRepository">
        <arguments>
            <argument name="logger" xsi:type="object">Mironsoft\Blog\Logger\Logger</argument>
        </arguments>
    </type>

    <type name="Mironsoft\Blog\Service\PostImportService">
        <arguments>
            <argument name="logger" xsi:type="object">Mironsoft\Blog\Logger\Logger</argument>
        </arguments>
    </type>

</config>

Die Klassen, die den Logger injiziert bekommen, müssen dafür nichts wissen. Sie injizieren einfach LoggerInterface aus PSR-3, und der DI-Container sorgt dafür, dass der passende Virtual-Type-Logger verwendet wird. Das ist saubere Dependency Injection: Die Klasse kennt nur das Interface, nicht die konkrete Implementierung oder Konfiguration.

Ein praktischer Nebeneffekt des modul-eigenen Loggers: Wenn man in Produktionssystemen die Logdatei eines bestimmten Moduls analysieren möchte, findet man alle relevanten Einträge gebündelt in einer einzigen Datei. Das erleichtert das Troubleshooting erheblich gegenüber dem Durchsuchen der allgemeinen system.log nach modulspezifischen Einträgen. In größeren Teams, bei denen verschiedene Entwickler für verschiedene Module zuständig sind, ist das ein wesentlicher Vorteil für die tägliche Arbeit.

Zusätzlich kann man den Log-Level des Virtual-Type-Loggers anpassen. In der Entwicklungsumgebung möchte man vielleicht DEBUG-Level-Logs, in der Produktionsumgebung nur WARNING und darüber. Das lässt sich über den level-Argument am Handler konfigurieren. Monolog unterstützt alle PSR-3 Log-Level: DEBUG, INFO, NOTICE, WARNING, ERROR, CRITICAL, ALERT, EMERGENCY. Durch die Virtual-Type-Konfiguration können Entwicklungs- und Produktionskonfigurationen in getrennten di.xml-Dateien für verschiedene Magento-Modi gepflegt werden.

Praxisbeispiel: API-Clients mit Virtual Types

Ein weiterer typischer Anwendungsfall für Virtual Types ist die Konfiguration mehrerer API-Clients mit unterschiedlichen Endpunkten. Angenommen, ein Modul muss sowohl mit einem Payment-API als auch mit einem Shipping-API kommunizieren. Beide APIs haben denselben grundlegenden Request-Mechanismus (Authentifizierung, Timeout, Error-Handling), unterscheiden sich aber in der Base-URL und eventuell im Timeout.

Ohne Virtual Types wären zwei Optionen denkbar: Entweder schreibt man zwei PHP-Klassen PaymentApiClient und ShippingApiClient, die beide denselben Code enthalten und sich nur in der Base-URL unterscheiden (Code-Duplikation). Oder man übergibt die Base-URL als Methoden-Argument bei jedem API-Call (schlechtere Kapselung, schwerer zu testen). Virtual Types bieten die saubere dritte Option: eine generische ApiClient-Klasse, die über Virtual Types mit spezifischen Konfigurationen versehen wird.

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

    <!-- Payment API Client — configured via Virtual Type -->
    <virtualType name="Mironsoft\Integration\Model\PaymentApiClient"
                 type="Mironsoft\Integration\Model\ApiClient">
        <arguments>
            <argument name="baseUrl" xsi:type="string">https://payment.api.example.com/v2</argument>
            <argument name="timeout" xsi:type="number">30</argument>
            <argument name="retryCount" xsi:type="number">3</argument>
        </arguments>
    </virtualType>

    <!-- Shipping API Client — different URL and longer timeout -->
    <virtualType name="Mironsoft\Integration\Model\ShippingApiClient"
                 type="Mironsoft\Integration\Model\ApiClient">
        <arguments>
            <argument name="baseUrl" xsi:type="string">https://shipping.api.example.com/v1</argument>
            <argument name="timeout" xsi:type="number">60</argument>
            <argument name="retryCount" xsi:type="number">2</argument>
        </arguments>
    </virtualType>

    <!-- Services receive their specific client via DI -->
    <type name="Mironsoft\Integration\Service\PaymentService">
        <arguments>
            <argument name="apiClient" xsi:type="object">
                Mironsoft\Integration\Model\PaymentApiClient
            </argument>
        </arguments>
    </type>

    <type name="Mironsoft\Integration\Service\ShippingService">
        <arguments>
            <argument name="apiClient" xsi:type="object">
                Mironsoft\Integration\Model\ShippingApiClient
            </argument>
        </arguments>
    </type>

</config>

Die PHP-Klasse ApiClient enthält den gesamten HTTP-Code. Sie nimmt baseUrl, timeout und retryCount über Constructor Property Promotion entgegen. PaymentService und ShippingService injizieren ApiClient und kennen nicht die Virtual-Type-Details. Der DI-Container sorgt dafür, dass jeder Service den richtigen Client mit der richtigen Konfiguration bekommt. Kein PHP-Duplikat, keine Konfiguration zur Laufzeit.

Debugging von DI-Problemen

Probleme mit Injectable Objects, Factories und Virtual Types zeigen sich typischerweise als Exceptions beim Laden einer Seite oder beim Ausführen von CLI-Befehlen. Die häufigsten Fehlertypen sind: ReflectionException wenn eine Klasse nicht gefunden wird, ObjectManager Exception wenn ein Interface keine Preference hat, und stille Fehler wenn der falsche Virtual Type konfiguriert ist.

Der erste Schritt beim Debugging ist immer bin/magento setup:di:compile. Dieser Befehl liest alle di.xml-Dateien, generiert Factories und Proxies, und zeigt Syntaxfehler in der DI-Konfiguration an. Ein fehlerhafter Virtual Type Name, ein nicht existierender Klassenname im type-Attribut oder ein falsches Argument-Format werden hier gemeldet. Ohne diesen Schritt kann man keine DI-Probleme systematisch lösen.

Der Befehl bin/magento dev:di:info 'Mironsoft\Blog\Logger\Logger' gibt detaillierte Informationen über einen Virtual Type oder eine Klasse aus: Welche Klasse wird tatsächlich instanziiert, welche Constructor-Argumente werden verwendet, welche Plugins sind registriert, ob der Typ shared oder non-shared ist. Dieser Befehl ist unverzichtbar um zu verstehen, was der DI-Container intern tut.

Im generated/code/-Verzeichnis kann man nach dem Compile die generierten Klassen inspizieren. Factories haben eine einfache Struktur und sind leicht zu verstehen. Wenn man den Verdacht hat, dass eine Factory nicht korrekt generiert wurde, kann man sie direkt ansehen. Interceptors (für Plugins) und Proxies sind komplexer, aber ebenfalls lesbar. Ein Blick in den generierten Code hilft oft dabei zu verstehen, warum sich das System anders verhält als erwartet.

Zusammenfassung: Injectable Objects & Virtual Types

Injectable Objects sind stateless Services, die sicher per Constructor Injection geteilt werden können. Non-Injectable Objects wie Models und Collections müssen immer über Factories instanziiert werden. Virtual Types ermöglichen mehrere Konfigurationen derselben Klasse ohne PHP-Duplikation — der ideale Weg für Modul-Logger, API-Clients und konfigurierbare Services in Magento 2.4.8.

Injectable = Stateless

Services, Repositories, Logger, Factories. Eine Instanz reicht für alle Aufrufe. Shared by default im DI-Container.

Non-Injectable = Stateful

Models, Collections, Entities. Neue Instanz pro Verwendung über Factory::create(). Nie direkt injizieren.

Factory Auto-Generation

setup:di:compile generiert PostFactory für Post automatisch. Im generated/-Verzeichnis. Nur Klassennamen + Factory injizieren.

Virtual Type

Neue "Klasse" ohne PHP-Datei, nur DI-Konfiguration. Gleicher Code, andere Constructor-Argumente. Ideal für Logger und API-Clients.

Mironsoft

DI-Architektur für Magento 2 aufbauen?

Wir entwickeln saubere Magento 2 Module mit korrekter Injectable/Non-Injectable-Trennung, automatisch generierten Factories und Virtual Types für konfliktfreie Mehrfachkonfiguration. Upgrade-sicher und vollständig getestet.

Factory Design

Korrekte Factory-Nutzung für Non-Injectable Objects

Virtual Types

Logger, API-Clients und Services ohne Code-Duplikat

DI-Audit

DI-Konfiguration analysieren und Non-Injectable-Fehler beheben

FAQ: Injectable & Virtual Types in Magento 2

1 Was sind Injectable Objects in Magento 2?

Injectable Objects sind stateless oder request-scoped Klassen, die sicher per Constructor Injection geteilt werden können. Dazu gehören Services, Repositories, Logger, Factories und Helper. Der DI-Container verwaltet sie als Shared Instances innerhalb eines Requests.

2 Was sind Non-Injectable Objects?

Non-Injectable Objects sind stateful Klassen die einen variablen Zustand speichern: Models, Collections, Value Objects. Jede Verwendung benötigt eine eigene Instanz. Man injiziert die Factory und ruft create() auf, um neue Instanzen zu erhalten.

3 Was sind Virtual Types in Magento 2?

Virtual Types sind DI-Konfigurationen die eine neue Klasse ohne eigene PHP-Datei definieren. Gleicher PHP-Code, andere Constructor-Argumente. Ideal für Logger mit verschiedenen Dateien oder API-Clients mit verschiedenen Base-URLs. Keine Code-Duplikation.

4 Was ist der Unterschied zwischen Shared und Non-Shared Instances?

Shared Instances werden einmal erstellt und für alle Anfragen wiederverwendet (Singleton-Verhalten). Non-Shared Instances werden bei jedem Aufruf neu erstellt. Injectable Objects sind Shared by Default. Factory-erstellte Non-Injectables sind immer Non-Shared.

5 Wie erstellt Magento 2 Factories automatisch?

setup:di:compile generiert Factory-Klassen im generated/-Verzeichnis. Naming: Klassenname + Factory. PostFactory für Post, ProductFactory für Product. Einfach in den Constructor injizieren.

6 Wie konfiguriere ich einen eigenen Modul-Logger?

Zwei Virtual Types: Handler (Base Handler mit fileName) und Logger (Monolog mit name und handlers). Per type-Konfiguration in eigene Klassen injizieren. Log-Datei landet in var/log/meinmodul.log.

7 Kann ein Virtual Type ein Interface implementieren?

Virtual Types erben die Interfaces der Basisklasse automatisch. Man kann einen Virtual Type als Preference für ein Interface setzen: <preference for="Interface" type="VirtualTypeName"/>. Damit bekommt jeder Interface-Aufrufer den Virtual Type.

8 Wie erkenne ich ob eine Klasse injectable ist?

Faustregel: Stateless = injectable. Stateful Entity/Model = non-injectable, Factory verwenden. Alle Klassen die AbstractModel oder AbstractCollection erweitern sind non-injectable.

9 Was ist der Unterschied zwischen Factory und Proxy?

Factory erstellt neue Instanzen von Non-Injectable Objects per create(). Proxy ist ein Lazy-Loading-Wrapper für Injectable Objects — erstellt die Instanz erst beim ersten Methodenaufruf. Proxies für schwere Services die nicht immer benötigt werden.

10 Wie debugge ich Virtual Type Probleme?

bin/magento dev:di:info 'VirtualTypeName' zeigt die vollständige Konfiguration. setup:di:compile zeigt Syntaxfehler. Im generated/-Verzeichnis generierte Klasse inspizieren. Virtual Type Namen müssen eindeutig sein.