Türchen 19: Dependency Injection in Magento 2

Magento 2 lauert vor den Toren der Magento-Welt und wartet mit neuen Konzepten auf. Eines davon hat einen Namen, den ich ungerne laut ausspreche, weil selbst „native Speaker“ dabei klingen, als sollten sie das mit dem Englisch doch besser lassen: „Dependency Injection“. In diesem Beitrag kratze ich ein bisschen an der theoretischen Oberfläche rum, was das überhaupt ist und wie es im Groben in Magento verwendet wird. Denn nicht nur das Mage::getModel() ist dem zum Opfer gefallen, sondern auch das Überschreiben von Klassen ändert sich dadurch.

Was ist „Dependency Injection“?

Wenn man so vor sich hin entwickelt, braucht man immer mal wieder irgendeine Komponente, die man dann im Normalfall zur Laufzeit lädt. Im simpelsten Fall eine Datenbankverbindung oder eine Session. Ich schreibe also eine Klasse und entscheide, was diese so braucht, wenn sie es denn braucht. So wie man vielleicht in Magento ein Modul schreibt und irgendwann auf die Idee kommt ein Produkt zu laden und daher Mage::getModel('catalog/product') aufruft. Genau das will Dependency Injection nicht.

Die Idee hinter Dependency Injection ist die Anwendung des sogenannten „Hollywood Prinzips“, das so nett als „Don´t call us, we´ll call you!“ beschrieben wird. Das bedeutet nichts anderes, als dass die Verantwortung für die Dinge, die man so braucht, nicht bei den Klassen selbst liegt, sondern beim Framework. Das Instanzieren von Objekten wird nicht über „new“ gelöst (okay, das gab's in Magento auch vorher nicht) – aber auch nicht mehr über „Mage::getModel()“. Nix mehr mit: ich nehme wir, was ich brauche, wenn es soweit. Was man braucht ist schon längst entschieden und wird "injeziert", lang bevor darauf zugegriffen wird.

Es gibt unterschiedliche Arten der "Injektion". Interface Injection, Setter Injection und die Constructor Injection. Dies aber nur am Rande und der Vollständigkeit halber.

Das Ziel von Dependency Injection ist, Abhängigkeit zu minimieren – und auch leichter erkennbar zu machen. Damit wird alles testbar, aber auch wartbarer und flexibler. Magento wird nun also von Haus aus für den Einsatz von Unit-Tests gerüstet sein...

Was meint Abhängigkeit genau? Wenn Klasse Eins im Laufe ihres Lebens Klasse Zwei lädt - dann ist sie von ihr abhängig. Punkt. Was aber, wenn man irgendwann Änderungen an Klasse Zwei machen möchte/muss und Klasse Zwei dafür z.B. einen Parameter braucht - dann gräbt man sich durch sein Projekt und sucht diese Abhängigkeiten, um dann an zig Stellen einen Parameter zu übergeben. Doof. Wenn man nun Klasse Eins testen will, diese aber Klasse Zwei lädt, muss Klasse Zwei mitgetestet werden. Auch Doof.

Ein Beispiel. So sieht's aus ohne Dependency Injection:

class KlasseEins 
{
	protected $_klasseZwei;
	
	public function __construct() 
	{
		$this->_klasseZwei = new KlasseZwei();
	}
}

$klasseEins = new KlasseEins();

Und das Ganze mit Dependency Injection - aber ohne Framework dahinter:

class KlasseEins
{
protected $_klasseZwei;

public function __construct (KlasseZwei $klasseZwei) 
	{
		$this->_klasseZwei = $klasseZwei;
	}
}

$klasseZwei = new KlasseZwei();
$klasseEins = new KlasseEins($klasseZwei);

Die Idee dahinter ist also, dass die Abhängigkeiten klar sind - und vorhanden sind - BEVOR die KlasseEins anfängt zu arbeiten, sie werden in die Klasse „injiziert“ - ob nun über den Konstruktor, wie in diesem Beispiel oder über Setter-Methoden.

Die Vorteile sind - hoffentlich - in diesem Mini-Beispiel deutlich geworden. Die Klassen sind unabhängig von einander. Änderungen an den Klassen sind leichter durchführbar. Um herauszufinden, was eine Klasse braucht, reicht es, sich den Konstruktor anzusehen. Dadurch ist alles übersichtlicher und wartbarer. Und die Tester unter Euch würden jetzt noch das Wort "Mock" in den Mund nehmen, weil Klassen "gemockt" werden können und man nur noch testen muss, was man testen will.

Beispiel 3 wäre dann eine Dependency Injection mit Framework dahinter. Und hier sind wir dann im Grunde bei Magento 2 angekommen. Das meint, dass irgendjemand in der ganzen Applikation sich mit dem Instanzieren von Objekten auskennen muss, damit man nicht alle Klassen, die man injizieren möchte, laden muss, bevor man seine Klasse Eins aufruft. Ziel ist also, Klasse Eins aufzurufen OHNE vorab Klasse Zwei zu Instanzieren, diese aber dennoch im Konstruktor geladen werden kann. Dafür muss also irgendjemand wissen, wer eigentlich was braucht. Und das ist der DIC. Der Dependency Injection Container.

Sprich: der Aufruf von Klasse Eins soll nicht so aussehen:

$klasseZwei = new KlasseZwei();
$klasseEins = new KlasseEins($klasseZwei);

sondern so:

$klasseEins = $allesRanSchaffer->get('KlasseEins');

Dependency Injection in Magento 2

Wenn man (wie ich, als ich davon hörte, dass Magento 2 Dependency Injection nutzt) so hinter Dependency Injection hinterher surft, findet man allerlei Definitionen und schöne Beispiele. Über diese geht Magento ein klein wenig hinaus. Es geht nicht nur darum, Abhängigkeiten zu injizieren, sondern diese darüber hinaus auch in einem Container zu verwalten. Magento 2 erweitert das Konzept der Dependency Injection also um den Dependency Injection Container. DIC. Denn es geht nicht nur darum, Abhängigkeiten zu injizieren, sondern diese auch irgendwie zu verwalten.

Der ObjectManager

Aber kurz nochmals einen Schritt zurück... Dependency Injection in Magento 2 wird über die Klasse Magento\App\ObjectManager abgewickelt.

Das passiert in etwa so wie im folgenden Script, das im ersten Schritt zeigen soll, wie man via ObjektManager eine Klasse instanziert.

use Magento\App\ObjectManager;
require_once 'app/bootstrap.php';

class MeineTestKlasse {}

$objectManager = new ObjectManager();
$meineTestKlasse = $objectManager->get('MeineTestKlasse');
echo get_class($meineTestKlasse);

Im Browser steht nun: "MeineTestKlasse".

Das kann natürlich nicht alles gewesen sein, denn es geht ja um Abhängigkeiten von anderen Klassen. Daher will das folgenden Scriptchen zeigen, wie die injizierte Abhängigkeit aussieht:

use Magento\App\ObjectManager;
require_once 'app/bootstrap.php';

class DieInjezierteKlasse
{
    public function doSomething()
    {
        return 'Bald ist Weihnachten';
    }
}

class MeineTestKlasse {

    protected $_dieInjezierteKlasse;

    public function __construct(DieInjezierteKlasse $dieInjezierteKlasse)
    {
        $this->_dieInjezierteKlasse = $dieInjezierteKlasse;
    }

    public function getSomething()
    {
        return $this->_dieInjezierteKlasse->doSomething();
    }
}

$objectManager = new ObjectManager();
$meineTestKlasse = $objectManager->get('MeineTestKlasse');
echo $meineTestKlasse->getSomething();

Im Browser steht nun "Bald ist Weihnachten"...

Der Dependency Injection Container in Magento 2

Zuständig für die Container ist die Klasse Magento\ObjectManager\ObjectManager. Wer von wem nun abhängig ist, wird konfiguriert. Aber auch, wer sonst noch was braucht. Und Magento wäre nicht Magento, wenn dies nicht via XML passieren würde.... Es gibt eine neue nette xml-Datei, die in den Modulen liegt: di.xml.

Die "di.xml" liegt im Modul-Konfigurations-Ordner "etc". also - fast wie in Magento 1 - in "app/code/NameSpace/ModulName/etc/di.xml".

Und hier kommt nun auch was Neues zu Tage. Die Unterschiede in der Konfiguration zwischen den einzelnen Konfigurationsbereichen (frontend, adminhtml) wird nicht mehr über die XML-Knoten gelöst, sondern über Verzeichnisse. Es gibt also für Abweichungen in der Konfiguration zum frontend die Datei: ect/frontend/di.xml.

In der di.xml wird also geregelt, was die Klasse braucht – an anderen Klassen, aber auch an anderen Parametern wie Arrays oder Strings. Darüber hinaus kann dort festgelegt werden, ob der Übergabewert z.B. ein Integer sein muss.

Ich werfe einen Blick in die di.xml des Catalog-Moduls und will wissen, was die Produkt-Helper-Klasse so braucht.

So sieht der Konstruktor der Produkt-Helper-Klasse aus:

public function __construct(
        \Magento\App\Helper\Context $context,
        \Magento\Core\Model\StoreManagerInterface $storeManager,
        \Magento\Catalog\Model\CategoryFactory $categoryFactory,
        \Magento\Catalog\Model\ProductFactory $productFactory,
        \Magento\Catalog\Model\Session $catalogSession,
        \Magento\View\Url $viewUrl,
        \Magento\Core\Model\Registry $coreRegistry,
        \Magento\Catalog\Model\Attribute\Config $attributeConfig,
        \Magento\Core\Model\Store\Config $coreStoreConfig,
        \Magento\Core\Model\Config $coreConfig,
        $typeSwitcherLabel
    )

Ist voll geworden.... der Konstruktor (in anderen Klassen ist er noch länger....).

Und so sieht der passende Eintrag in der di.xml unter /app/code/Magento/Catalog/etc/di.xml aus:

<config>
    <preference for="Magento\Catalog\Model\ProductTypes\ConfigInterface" 
    	type="Magento\Catalog\Model\ProductTypes\Config" />
    <preference for="Magento\Catalog\Model\ProductOptions\ConfigInterface" 
    	type="Magento\Catalog\Model\ProductOptions\Config" />
...
    <type name="Magento\Catalog\Helper\Product">
        <param name="typeSwitcherLabel">
            <value>Virtual</value>
        </param>
    </type>
...
  </config>

Es beginnt mit dem config-Knoten (der muss nehme ich an nicht weiter erklärt werden...). Dann kommen "preference" und "type".

Type sagt:
Wenn die Klasse „ Magento\Catalog\Helper\Product“ geladen wird, wird folgendes gebraucht:

  • ein Parameter mit Namen "typeSwitcherLabel".
  • wenn nix anderes übergeben wird, ist der Wert "Virtual"

Man kann statt eines Values z.B. auch bestimmen, von welchem Typ die Klasse ist, die übergeben werden soll. Dann würde statt Virtual dort stehen:

<instance type="NameSpace\ModulName\Model\ClassName" />.

Dann kann man z.B. auch angeben:

<value type="int">64</value>

Auch das ist neu in Magento 2: wer jetzt echt mehr wissen will, was alles definiert werden kann, kann sich die ganzen Möglichkeiten ansehen unter:
lib/Magento/ObjectManager/etc/config.xsd

Nun noch ein letzter Blick auf den Knoten "preference" ganz oben in der Konfiguration.

<preference for="Magento\Catalog\Model\ProductTypes\ConfigInterface" 
        type="Magento\Catalog\Model\ProductTypes\Config" />

Das meint folgendes:

Wann auch immer die Klasse "Magento\Catalog\Model\ProductTypes\ConfigInterface" gefordert ist, wie hier in der Product-Helper-Klasse:

public function __construct(
        \Magento\Catalog\Model\ProductTypes\ConfigInterface $config,
        \Magento\Catalog\Model\Product\Type\Pool $productTypePool,
        \Magento\Catalog\Model\Product\Type\Price\Factory $priceFactory
    )

dann lade NICHT \Magento\Catalog\Model\ProductTypes\ConfigInterface (was auch einen Fehler gäbe, weil's ein Interface ist), SONDERN lade: Magento\Catalog\Model\ProductTypes\Config.

Das heißt: die benötigte Klasse ist abhängig von einem Interface, das bestimmt, welche Voraussetzungen (oder Methoden) die Klasse haben muss, die übergeben wird. Im Konstruktor steht das Inferface - welche Klasse aber tatsächlich geladen wird steht in der Container-Konfiguration.

Noch ein Beispiel:

Wenn man also einfach mal in irgendeiner Klasse nach einem Interface sucht, alsoooo (Suchgeräusche) z.B. im Import-Export-Model in der Import-Klasse. Da soll folgende Klasse geladen werden:

/**
     * @var \Magento\ImportExport\Model\Import\ConfigInterface
     */
    protected $_importConfig;

die über den Konstruktor injiziert wird:

public function __construct(
	...
	\Magento\ImportExport\Model\Import\ConfigInterface $importConfig,
	...
  )

Welche aber tatsächlich geladen werden soll, ist zu finden in der di.xml:

<config>
    <preference for="Magento\ImportExport\Model\Import\ConfigInterface" 
    	type="Magento\ImportExport\Model\Import\Config" />
    <preference for="Magento\ImportExport\Model\Export\ConfigInterface" 
    	type="Magento\ImportExport\Model\Export\Config" />
  </config>

Okay? Dann noch ein letztes Wort zu den....

Rewrites in Magento 2

Und zu guter letzt, ergibt sich daraus auch, wie in Magento 2 nun Klassen überschrieben werden. Dies geschieht nun auch in der di.xml:

<config>
	<preference for="NameSprace/ModulName/Model/ClassName" 
		type="NameSpraceNew/ModulNameNew/Model/ClassName" />
  </config>

Also z.B.:

<config>
	<preference for="Magento/Catalog/Model/Product" 
		type="Wegbuys/AdvancedCatalog/Model/Product" />
  </config>

Fazit

Magento (man verzeihe mir, dass ich "Magento" personifiziere, so wie man gerne "im Internet kauft") sagte mal, dass die Einarbeitungszeit für Magento 1 etwa ein halbes Jahr beträgt und für Magento 2 sich das Ganze auf drei Monate verkürzen wird.

Pffffft.....

Ich glaube, das bezieht sich eher auf die Frage: wie schnell komme ich zum Ziel, wenn ich nicht wirklich das Ganze verstanden habe. Da brauchte man in Magento 1 - vielleicht - länger, weil man an viel mehr Stellen Fehler machen konnte. Das hat sich - vielleicht - reduziert. Aber ob es tatsächlich so ist, dass man Magento jetzt schneller kapiert - pffffft..... Kannichnichsagen. Der Wikieintrag zu Dependeny Injection existierte noch nicht zum Zeitpunkt dieses Türchens...

Wenn dieser Artikel geholfen hat, ein wenig Einblick in ein neues Konzept und ein paar Andersmachereien unter Magento 2 zu gewinnnen und die Einarbeitungszeit sich um ein paar Stunden verkürzt hat, freue ich mich...

<type name="Webguys\Adventskalender\Model\Tuerchen20">
	<param name="letzteWorte">
            <value>Frohe Weihnachten!</value>
        </param>
    </type>



Ein Beitrag von Carmen Bremen
Carmen's avatar

Carmen Bremen ist Magento Freelancer, firmiert unter Neoshops lebt und arbeitet in Köln, ist Initiatorin des Kölner Magento-Stammtischs, trinkt sporadisch Kölsch, spricht Kölsch nur auf Aufforderung und das nicht besonders gut und darf sich 3x certified nennen.

Alle Beiträge von Carmen

Kommentare
Florin P. am

ich habe mich bereits durch einige Videos und der Magento2 Wiki zu diesen Thema durchgewühlt und ich muss sagen das das der Beste Artikel zu dem Thema ist, vielen dank an dieser Stelle.

Was die Dependency Injections angeht kann ich mich Rokko nur anschließen und halte die neuerungen für wichtig und angebracht bei einer solchen Framework wie Magento.

Weiter so, ich warte spannend auf die nächsten Magento2 Artikeln von den Webguys :)

Carmen Bremen am

Danke für das tolle Feedback!

Ich schließe mich dem an - ich finde es ist ein vielversprechendes Konzept, das erstmal wuchtig erscheint, aber wenn man sich eingewöhnt hat, sicher vieles erleichtert.

Matthias am

Endlich bin ich auch mal dazu gekommen, die offenen Türchen zu lesen, die mir noch fehlten ;) Ein wirklich super Artikel zu einem sehr interessanten Thema!

Insgesamt ein tolles Pattern, welches (hoffentlich) ein wenig Übersicht schaffen wird. Momentan fühlt es sich aber wirklich noch etwas komplizierter an - aber wenn man 2-3 Mal damit gearbeitet hat, kommt man sicher gut klar.

Rokko am

Hallo! Toll und verständlich, dankeschön! :)

Ich glaube nicht, dass hier mit Biegen und Brechen irgendwas versucht wird. Dependency Injection hat sich einfach als Design Pattern als brauchbar herauskristallisiert und es ist nur konsequent, "neue" Entwicklungen in der Softwarebranche in einem so großen Framework wie Magento einzusetzen. Es erhöht eventuell die Einarbeitungszeit und auch das intellektuelle Einstiegsniveau, aber das darf natürlich kein Grund sein, auf gewisse Pattern an Stellen wie diesen zu verzichten,

Wie oben bereits erklärt wurde, ist ein Code, dessen verwendete Klassen minimal zusammenhängen, wesentlich wartbarer, austauschbarer und testbarer.

Was ich bisher noch sehr verwirrend finde, ist die Rolle von ZF1 und ZF2 in Magento 2. Anfangs sah es danach aus, dass das Zend Framework 1 weiterhin die Basis von Magento 2 war, inzwischen (gerade bei DI) scheint Magento 2 sich dann doch auf ZF2 zu stützen. Und welche Rolle spielt Symfony bei dem ganzen?

Konzentriert man sich am Ende auf ein einziges Framework, oder wird das ein Mischmasch aus allem?

Grüße! Rokko,

... der immer noch Ideen für Weihnachtsgeschenke braucht. Hier im Blog ist aber natürlich kein Platz für Anregungen ;)

Nicolai Essig am

Super Artikel.

Und Magicento oder ähnliche Tools werden lebensnotwendig, da man sonst ewig für ein kleines Modul mit ein paar Klassen braucht.

Ich finde es wird eher komplizierter. Da sucht man dann mal ewig nach einem Fehler und es war nur ein fehlender Konstruktor Parameter in der XML. Ich hab so das Gefühl das die Magento Entwickler hier mit biegen und brechen ständig irgendwelche Techniken einbauen wollen.

Roman am

Danke für diesen Artikel, besser kann man es glaube ich nicht beschreiben...

Sven am

Hi,

vielen Dank für den sehr gut geschriebenen Artikel! Ich glaube ich habe jetzt auch endlich mal verstanden wobei es bei DI geht und das wird mir den Umstieg auf Magento 2 sicherlich erleichtern.

Dein Kommentar