Dependency Injection für OXID eShop

Seit OXID v6 wird die Symfony Dependency Injection Komponente über den Composer mitinstalliert. Jedoch zur Zeit (v6.2.1) nutzt OXID den „Symfony Dependency Injection“ allein für interne Zwecke. Irgendwann wird es sicher ein Konzept geben, sodass auch wir DI in Projekten und in Modulen nutzten können.

Ich habe bereits jetzt eine Möglichkeit gefunden, die Komponente im Projekt zu nutzen, wozu eine zusätzliche Installation erforderlich ist, die ich weiter unten beschreibe. Zunächst ein paar allgemeinere Erklärungen:

Was bedeutet „Dependency Injection“?

Das Grundprinzip: Eine Klasse braucht weitere Klassen, um ihre Arbeit machen zu können.

Ein Beispiel:

FrontendController::class

braucht die

Config::class

und noch die

Session::class

Diese Situation, um die Klassen mit einander zu verbinden, nennt man Dependency Injection. Man kann sie auf zwei bekannte Wegen lösen: „Klassisch“ oder mit einem „Factory Design Pattern“.

1. Factory Design Pattern

Es ist zunächstmal ein anderer Denkansatz: Der Frontend-Controller bekommt die fertig gebaute Klasse quasi „zugereicht“. Dadurch muss er nicht mehr wissen, wie diese gebaut werden, er kann sie direkt nutzen.

Vorteil

  • Bei den Tests könnte man ihm eine Mock (simulierte Klasse) „unterjubeln“, um zu testen ob es wirklich die Funktionen der Session nutzt.
  • Man muss nicht mehr so viel herum abdecken, das spart Zeit beim schreiben der Tests!
  • Die Bauanleitung wie XYZ::class gebaut wird, ist an einem einzigen Ort hinterlegt.

2. Klassisch

`FrontendController::class` baut/holt sich selbst die Klassen. Dazu muss er wissen, wie diese gebaut werden müssen.

Nachteile

  • Wenn man den FrontendController testen möchte, kommt man an die intern gebaute Klassen nicht heran, um sie zu Mocken/Simulieren. So ist man gezwungen, umfangreiche Tests zu schreiben, damit die anderen Klassen funktionieren. Dadurch bläht sich der Test extrem auf, der dann nichts mehr mit dem eigentlichen Testziel zu tun hat. Und genau das kostet dann Entwicklungszeit und Wartungsaufwand.
  • Zum anderen: Die Bauanleitung wie man ein eine XYZ::class baut, steckt im FrontendController. Hat man vor, diese XYZ::class in einer anderen Klasse zu nutzen, könnte man den Code eins zu eins kopieren, was natürlich extrem schlecht ist und keinen wartbaren Code verursacht. Stell dir vor, XYZ::class soll eines Tages anders gebaut werden. Dann kannst du das ganze Projekt nach diesem Baumechanismus durchsuchen (urgs).

Beispiel für ein „Factory Design Pattern“

Eine Factory kümmert sich um den Bau der Klasse.

Beispiel

class Factory
{
    /**
     * Dependency Injection example
     */
    public static function createFrontendController()
    {
        $config = new Config();
        $config->init();

        $session = new Session();
        $session->initNewSession();

        $frontendController = new FrontendController();
        $frontendController->setConfig($config);
        $frontendController->setSession($session);
    }
}

Die Symfony Dependency Injection Componente

Nun stell dir vor, das „Factory Design Pattern“, das dir einen FrontendController mit allen Klassen baut, wird automatisiert generiert. Bei einfachen Konstrukten findet das System selbständig heraus, welche Klasse gebraucht wird. Stichwort Autowiring: Dabei wird untersucht, welche Argumente der Constructor erwartet. Werden Klassen erwarten, erstellt es diese Klassen und sie wird überreicht. Ohne dass du Code geschrieben hast.

Vorteil

  • Wartbarkeit: Die Informationen wie eine Klasse gebaut wird, ist zentral in einer services.yaml Datei abglegt.
  • Testbarkeit: Bei den Tests kann man sagen „Hey SymfonyDI, gib ihm diese Mock Klasse“.

Es hat ein ähnliches verhalten wie Registry::get() oder Registry::set() nur mit mehr Logik, da die Klassen nach einer Anleitung der YAML gebaut werden. Einmal gebaut Klassen bleiben im SymfonyDI Container erhalten, wie bei der Registry auch.

Wie die SymfonyDI in OXID genutzt werden kann

Installation

Source Code (GitHub)

composer require oxidprojects/dependency-injection

services.yaml

Die wichtigste Datei ist „services.yaml“. In einer solchen YAML-Datei beschreibst du die Bauanleitung deiner Klasse. (Siehe Orginal Dokumentation). Es wird nach vorhanden services.yaml-Dateien gesucht, die in den Modulen Ordnern liegen.

Example: source/modules/tm/Sunshine/services.yaml

services:
  tm\ModuleOutput:
    class: 'tm\ModuleOutput'

Im FrontendController

class Controller extends FrontendController
{
    public function render()
    {
        $output = project_container()->get(tm\ModuleOutput::class);

        // With the Integration Test we will see is method html() called
        $this->addTplParam('title', $output->html('hallo'));

        return 'template';
    }
}

Der Integration Test für FrontendController

Mit diesem Test testen wir, ob der FrontendController die html()-Methode von ModuleOutput richtig aufruft.

class ControllerTest extends TestCase
{
    public function testRender()
    {
        //Arrange
        $controller = new Controller();
        $mockModuleOutput = $this->prophesize(tm\ModuleOutput::class);

        //to publish the mock
        project_container()->set(tm\ModuleOutput::class, $mockModuleOutput->reveal());

        //Assert
        $mockModuleOutput->html(Argument::type('string'))->shouldBeCalled();

        //Act
        $controller->render();
    }
}

Ein weiteres Anwendungsbeispiel

Kennst du es auch? Konfigrationen mal an einem Ort zu haben, ohne sich ständig die IDs der oxConfig merken zu müssen:

    $wawiAPi->getUrl();
    $wawiAPi->getUsername();
    $wawiAPi->getPassword();

SymfonyDI kann genutzt werden, um die Klasse mit den Informationen zu füllen:

    $wawiAPi = project_container()->get(tm\WawiApi::class)
    $wawiAPi->getUrl();
    //...

In der YAML-Datei werden die IDs hinterlegt, so dass sie sich dort an einem zentralen Ort befinden. Dokumentation

services:
      oxRegistry:
        class: 'OxidEsales\Eshop\Core\Registry'
    
      tm\WawiApi:
        class: 'tm\WawiApi'
        properties:
          url:      '@=service("oxRegistry").getConfig().getConfigParam("WAWI_API_ENDPOINT")'
          username: '@=service("oxRegistry").getConfig().getConfigParam("WAWI_API_USERNAME")'
          password: '@=service("oxRegistry").getConfig().getConfigParam("WAWI_API_PASSWORD")'

Die Klasse würde so ausschauen:

namespace tm;

class WawiApi {
    public $url = '';
    public $username = '';
    public $password = '';

    /**
     * @return string
     */
    public function getUrl(): string
    {
        return $this->url;
    }

    /**
     * @return string
     */
    public function getUsername(): string
    {
        return $this->username;
    }

    /**
     * @return string
     */
    public function getPassword(): string
    {
        return $this->password;
    }
}

Dadurch können an jedem Ort, zu jeder Zeit, die Configs geholt werden. Sie werden pro Prozess nur einmal gebaut.

Weiter Documentationen und Anleitungen

Viel Spass und Happy Coding!



Start the discussion at OXID forums

Prior comments
1 Antwort

Trackbacks & Pingbacks

  1. […] kann. In diesem Artikel beschreibe ich, wie man eine einfache Schnittstelle mit Hilfe von Symfony Dependency Injection erstellen […]

Dein Kommentar

An Diskussion beteiligen?
Hinterlasse uns Deinen Kommentar!

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.