Konzept für DI: Eine Möglichkeit schaffen, fremden Code einzubeziehen

Oft wünscht man sich Änderungen oder Ergänzungen zu einem Feed, der von einem Modul gebaut wurde. Oft ist dabei der Gedanke „Der Feed funktioniert zwar in einem Standard OXID eShop aber nicht in unserer angepassten Variante“. Als Benutzer kann man an dieser Stelle oft nur mit Hacks und vielen Tricks operieren. Es wäre schön, vom Modulentwickler eine Schnittstelle zu erhalten, in man sich „einklinken“ kann. In diesem Artikel beschreibe ich, wie man eine einfache Schnittstelle mit Hilfe von Symfony Dependency Injection erstellen.

Das Konzept

DI ermöglicht es, den Klassen in der services.yaml-Datei Tags zu vergeben. Dadurch können alle Klassen mit einem spezifischen Tag gesammeln und weiterverarbeitet werden. Wenn also z.B. eine dritte Person noch etwas ergänzen möchte, muss sie lediglich eine Klasse mit dem entsprechenden Tag in der services.yaml-Datei hinterlegen, so dass dieser offiziell mit verarbeitet werden kann.

Weiterführende Dokumentation auf symfony.com: Work with Service Tags

Die Bauanleitung

Zunächst etwas Grundwissen, bevor wir an die Umsetzung gehen:

Der DI-Container wird einmalig ins Caching kompiliert, um vor allem Ressourcen zu schonen. Der Speicherort dafür in OXID ist tmp/ceProjectServiceContainer.php. Diese Datei muss nach einer Änderung in der service.yaml-Datei immer gelöscht werden, damit der DI-Container automatisch neu gebaut werden kann. Vergesst Ihr das Leeren dieses Caches, wird sich nichts ändern 😉

Das Projekt

Wir wollen einen Feed bauen, der alle URLs des Shops, z.B. von Artikeln und Kategorien usw. beinhaltet.

  1. Artikel und Kategorien sind jeweils eigene Klassen, die mit einem Tag eingesammelt werden sollen und die eine Interface-Funktion getUrls(): array haben, um sicherzustellen, dass alle diese Klassen gleich sind.
  2. Zudem brauchen wir noch eine Collection-Klasse, die diese Klassen beinhaltet, um diese innerhalb einer Schleife abarbeiten zu können.

Schritt 1: Erstellen der Collection-Klasse

namespace tm;

class Collection 
{
    protected $classes = [];
    
    public function getClasses(): array
    {
        return $this->classes;
    }
    
    public function addClass($newClass)
    {
        $this->classes[] = $newClass;
    }
}

Schritt 2: Dem DI-Compiler beibringen, diese Collection-Klasse zu füllen

Die Aufgabe des DI-Compilers soll es sein, die Collection mit den Klassen zu füllen, die einen bestimmten Tag haben. Dazu gibt uns Symfony die Anleitung CompilerPass, die es ermöglicht, dass die Funktion Collection::addClass($newClass) genutzt werden kann. Zudem soll er alle Klassen mit dem Tag module.feed_url finden:

namespace tm;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Reference;

class FeedPass implements CompilerPassInterface
{
    protected $searchTagName = "module.feed_url";

    public function process(ContainerBuilder $container)
    {
        if (!$container->has(\tm\Collection::class)) {
            return;
        }

        $collectionDefinition = $container->findDefinition(\tm\Collection::class);
        $taggedServices = $container->findTaggedServiceIds($this->searchTagName);

        foreach ($taggedServices as $id => $tags) {
            // Dem compiler beibringen `tm\Collection::addClass($newClass)` zu nutzen.
            $collectionDefinition->addMethodCall('addClass', [new Reference($id)]);
        }
    }
}

Schritt 3: Aufname in die service.yaml-Datei

Nun müssen beide Klassen in der service.yaml hinterlegt werden, um später mit project_container()->get('tm\Collection'); die gefüllte Klasse zu bekommen.

services:
  tm\Collection:
    class: 'tm\Collection'

  tm\FeedPass:
    class: 'tm\FeedPass'
    public: false
    tags:
      - name: 'compiler.pass'

Wichtig ist, dass die ‚tm\FeedPass‘ den Tag ‚compiler.pass‘ hat, damit sie im Compiler Prozess berücksichtig werden kann.
Fertig. Nun weiss der DI-Container, wonach er suchen soll.

Schritt 4: Artikel und Kategorien aufnehmen

Nun sollen die Artikel- und Kategorie-Klassen mit in die service.yaml-Datei aufgenommen werden (diese können übrigens auch in jeder service.yaml-Datei eines anderen Moduls liegen):

services:
  tm\Artikeln:
    class: 'tm\Artikeln'
    public: false
    tags:
      - name: 'module.feed_url'

  tm\Kategorie:
    class: 'tm\Kategorie'
    public: false
    tags:
      - name: 'module.feed_url'

Diese wurden mit `public: false` markiert, damit sie im Container unsichtbar bleiben.

Schritt 5: Die Schnittstelle nutzen

Zuletzt holt man sich noch die tm\Collection und schon hat man eine Schnittstelle für alle anderen Modulentwickler gebaut, die in mein Modul eingreifen möchten:

$collection = project_container()->get('tm\Collection');
$classes = $collection->getClasses();

foreach ($classes as $class) {
    echo implode('\n', $class->getUrls());
}

Tipps

    • Vergiss nicht, die tmp/ceProjectServiceContainer.php nach Änderungen an der service.yaml-Datei zu löschen!
    • Schau Dir an, ob die tmp/ceProjectServiceContainer.php Deinen Vorstellungen und Wünschen entspricht.
    • Ist die Datei tmp/ceProjectServiceContainer.php nicht vorhanden, prüfe in der oxideshop.log, welcher Fehler passiert ist.

Viel Spass und Happy Coding!



Start the discussion at OXID forums