Stets zu Diensten

Zend Framework 2: Ein Überblick über den Service-Manager
Kommentare

In diesem Artikel erfahren Sie zuerst einige grundlegende Informationen zum Einsatz des Service-Managers in Zend Framework 2 und zu möglichen Fallstricken. Der Artikel zeigt auf, welche spezialisierten Service-Manager es gibt und wie Sie diese gewinnbringend einsetzen können. Zudem wird erläutert, wie hilfreich Initializer sein können und warum Sie den Einsatz vom „berüchtigten“ ServiceLocatorAwareInterface eher vermeiden sollten.

Die Komponente ZendServiceManager hat im Zend Framework 2 die alte statische Zend_Registry vollkommen ersetzt. Der Service-Manager ermöglicht dem Entwickler, die Konfiguration und Initialisierung seiner Objekte und Services an einem zentralen Ort zu bündeln. Das erleichtert die Wiederverwendung und hilft bei der Strukturierung der eigenen Anwendung.

Zend Framework 2 – Beispielanwendung

Damit Sie die Beispiele selbst nachvollziehen können, steht eine Beispielanwendung auf GitHub für Sie bereit. Sie können das Projekt wie folgt klonen (bitte ggf. die Verzeichnisse anpassen) und installieren:

> cd /home/devhost
> git clone ht tps://github.com/RalfEggert/phpmagazin.service-manager 
> cd phpmagazin.service-manager 
> php composer.phar selfupdate
> php composer.phar install
> sudo chmod 777 -R data/ 

Danach richten Sie noch einen Virtual Host für die Adresse phpmagazin.service-manager ein. Wenn Sie nun ht tp://phpmagazin.service-manager/ in Ihrem Browser aufrufen, sollte die Seite ungefähr wie in Abbildung 1 aussehen. Nun können wir loslegen.

Abb. 1: Beispielanwendung im ZF2

Abb. 1: Beispielanwendung im ZF2

Zuerst etwas Theorie

Die ZendService-Manager-Komponente implementiert das Service-Locator-Entwurfsmuster, das unter anderem auch von Martin Fowler beschrieben wurde. Der Service-Locator hat die Aufgabe (ähnlich wie eine Registry), alle benötigten Objekte (Services) bereitzustellen. Während eine Registry diese Objekte zuerst übergeben bekommen muss, um sie dann für die spätere Verwendung bereitzuhalten, kümmert sich der Service-Locator auch um die Instanziierung dieser Objekte. Der Service-Locator hält somit nicht nur die instanziierten Objekte, sondern auch das Wissen bereit, wie ein Objekt zu instanziieren ist. Erst wenn diese Objekte wirklich gebraucht werden, werden diese erzeugt.

Der Service-Manager ist dabei in der Lage, sowohl Klassen ohne Abhängigkeiten direkt zu instanziieren (die so genannten Invokables) als auch Klassen mit Abhängigkeiten mithilfe von Factories zu generieren. Zusätzlich werden abstrakte Fabriken und Initializer unterstützt, sie können für jeden Service definieren, ob dieser nach der Instanziierung mehrfach genutzt werden darf oder nicht, sowie auch fertig generierte Objekte übergeben. Die Konfiguration des Service-Managers kann sowohl über Konfigurationsklassen als auch über Konfigurationsdateien erfolgen.

Übrigens werden die Begriffe Service-Locator und Service-Manager im ZF2 nahezu synonym verwendet. Wenn vom Service-Locator die Rede ist bzw. eine Methode oder ein Parameter als ServiceLocator bezeichnet ist, ist damit meistens der konkrete Service-Manager gemeint, der im MVC konfiguriert und bereitgestellt wird. Im Folgenden werden diese beiden Begriffe auch synonym verwendet.

Service-Manager konfigurieren

Um die grundlegende Funktionsweise vom Service-Manager verstehen zu können, benötigen wir als Erstes einige Beispielklassen, mit denen wir arbeiten können. Als Erstes verwenden wir eine ShopEntityPinkPrincessBell (Listing 1), die ohne Konstruktor auskommt und somit direkt instanziiert werden kann.

Die Klasse repräsentiert eine Fahrradklingel und implementiert das ShopEntityBellInterface. Als Nächstes setzen wir die Klasse ShopEntityWheel ein (Listing 2), deren Konstruktor einen Namen und die Reifengröße benötigt, damit die Klasse instanziiert werden kann. Auch hier wird ein entsprechendes Interface mit Namen ShopEntityWheelInterface implementiert. Als Letztes im Bunde verwenden wir die Klasse ShopEntityBike (Listing 3), die einen etwas komplexeren Konstruktor bereitstellt. Unter anderem werden auch Instanzen von den anderen Klassen benötigt. Dabei kommen das BellInterface und das WheelInterface als Type Hint zum Einsatz, was die Testbarkeit und Wartbarkeit der Klasse erhöht, da wir somit gegen ein Interface und nicht gegen eine konkrete Klasse programmieren. Zusätzlich werden auch noch Name und Farbe als Parameter benötigt. Die Sinnhaftigkeit dieser Klassen wollen wir an dieser Stelle nicht weiter diskutieren, sie dienen für die folgenden Erläuterungen lediglich als Grundlage. Sie finden alle Klassen in der Beispielanwendung im Verzeichnis /module/Shop/src/Shop/Entity.

Um diese Klassen mithilfe des Service-Managers verwenden zu können, müssen wir als Nächstes die Konfiguration in der Datei /module/Shop/config/module.config.php betrachten (Listing 4). Beim Laden der aktiven Module der Zend-Framework-2-Anwendung wird die Konfiguration für den Service-Manager durch den Konfigurationsschlüssel service_manager ausgezeichnet. Darunter werden die weiteren Konfigurationsschlüssel definiert. Mit invokables legen Sie fest, welche Klassen direkt instanziiert werden können. Dies betrifft in unserem Beispiel nur die Klasse PinkPrincessBell.

Für Klassen, die nur mithilfe eines Konstruktors instanziiert werden können, oder die über entsprechende Setter-Methoden konfiguriert werden müssen, kommt der Schlüssel factories zum Einsatz. Dies betrifft die beiden definierten Reifen und die beiden Fahrräder. Bei der Implementation der Factories haben Sie die Wahl zwischen Closures und Factory-Klassen. Für die Reifen wird jeweils eine Factory Closure definiert, die eine Instanz von ShopEntityWheel generiert, dabei den Namen und die Größe festlegt und abschließend zurückgibt. Für die beiden konfigurierten Fahrräder kommen Factory-Klassen zum Einsatz. Eine davon finden Sie in Listing 5. Factory-Klassen müssen das ZendService-ManagerFactoryInterface implementieren und haben ebenfalls die Aufgabe, die Instanz einer Klasse zu generieren und zurückzugeben. Da unsere Bike-Klasse zwingend eine Klingel und zwei Räder benötigt, verwenden wir an dieser Stelle nun den Service-Manager, der bereits die Konfiguration für diese Objekte kennt. Somit werden bei der Abfrage der Objekte für Klingel und Räder die entsprechenden Objekte durch den Service-Manager generiert und zurückgegeben. Mit all diesen Bausteinen kann nun das Fahrradobjekt erstellt werden.

Durch den Schlüssel shared in der Konfiguration für den Service-Manager können wir festlegen, ob ein Objekt bei wiederholtem Abruf immer neu generiert oder wiederverwendet werden soll. Da wir keine Einräder, sondern Zweiräder bauen wollen, stellen wir an dieser Stelle sicher, dass wir beim Abruf der Reifen auch wirklich neue Instanzen bekommen. Natürlich müssten wir das auch für die Klingel und die Räder machen, aber dies ist ja nur ein Beispiel.

Bleibt uns an dieser Stelle nur noch die Aufgabe, den Schlüssel aliases genauer zu betrachten. Jeder Service, der im Service-Manager definiert wird, muss einen eindeutigen Bezeichner bekommen, z. B. ShopEntityBellPinkPrincessBell. Dieser Bezeichner muss nicht zwingend die Klassenstruktur widerspiegeln wie in diesem Beispiel. Jeder Bezeichner wird intern im Service-Manager kanonisiert, d. h. der Begriff wird in Kleinbuchstaben umgewandelt und die Trennzeichen „-“, „_“, „.“, „/“ und „“ werden entfernt. Auf das Beispiel könnten wir somit auch mit dem Bezeichner SHOPentityBELLpinkPRINESSbell zugreifen und würden damit denselben Service ansprechen. Um den Kreis nun zu den Aliasen zu schließen, können wir im Service-Manager aber auch alternative Namen vergeben, um auf die Services zugreifen zu können. Damit wäre die Konfiguration fürs Erste abgeschlossen.

Auf Services zugreifen

Der Zugriff auf die konfigurierten Services erfolgt über die get()-Methode. Im Action-Controller ist der Zugriff auf den ersten Blick sehr einfach, wie Listing 6 zeigt. Wir müssen in einer Aktionsmethode lediglich auf die $this->getServiceLocator()-Methode zugreifen und können damit jeden beliebigen Service anfordern. Hierbei verwenden wir die kürzeren Aliase, die wir im Service-Manager konfiguriert haben. Die angeforderten Objekte werden für die Ausgabe einfach an das ViewModel übergeben, dessen zugeordnetes View-Skript sich dann später um die Ausgabe kümmert. Das View-Skript ersparen wir uns hier, da es neben etwas HTML-Markup nur einige Aufrufe von ZendDebugDebug::dump() enthält. Interessanter ist dabei schon die Ausgabe, die in Abbildung 2 zu sehen ist.

Abb. 2: Ausgabe der Objekte

Abb. 2: Ausgabe der Objekte

Bei der Ausgabe der Objekte finden Sie in eckigen Klammern die so genannte Objekt– oder Instanz-ID. Dabei wird bei dem Objekt der Klasse ShopEntityBellPinkPrincessBell jedes Mal die ID 250 ausgegeben, während die Instanzen von ShopEntityWheelWheel jedes Mal eine andere Objekt-ID zugeordnet bekommen haben (251, 252, 254, 255, 258 und 259). Das betrifft sowohl die direkt ausgegebenen Instanzen als auch die den ShopEntityBike-Instanzen zugeordneten Objekte. Dieses nicht wirklich überraschende Verhalten liegt daran, dass wir für die Services ShopEntityWheel10InchWheel und ShopEntityWheel20InchWheel die Wiederverwendung durch den shared-Parameter deaktiviert haben.

Aufmacherbild: <a href=“http://www.istockphoto.com/photo/human-mind-13485370″ title=“Human mind – Stock Image von iStockphoto / Uhrheberrecht: adventtr “ class=“elf-external elf-icon elf-external elf-icon“ rel=“nofollow nofollow“>Human mind – Stock Image von iStockphoto / Urheberrecht: adventtr [ header = Seite 2: Spezialisierte Service-Manager ]

Spezialisierte Service-Manager

Neben dem Service-Manager, der im MVC automatisch initialisiert und konfiguriert wird, gibt es auch eine ganze Reihe weiterer, spezialisierter Service-Manager. Diese werden auch Plug-in-Manager genannt, da sie in der Regel nur für eine bestimmte Sorte von Plug-ins zuständig sind. Damit wird sichergestellt, dass die Instanz des Service-Managers nicht mit zu vielen verschiedenen Services überladen wird. Außerdem ist es somit möglich, den Bezeichner url sowohl für einen View-Helper, als auch für ein Controller-Plug-in zu verwenden. Insgesamt sind im Zend Framework Release 2.3.0 über dreißig Plug-in-Manager in den verschiedenen Komponenten definiert. Die wichtigsten spezialisierten Service-Manager (Abb. 3) lassen sich auch über den Modul-Manager und damit über die module.config.php-Konfigurationsdateien oder Module-Klassen der Module konfigurieren.

Abb. 3: Spezialisierte Service-Manager

Abb. 3: Spezialisierte Service-Manager

Die entsprechenden Konfigurationsschlüssel und Methoden für die Module-Klassen können Sie der Tabelle 1 entnehmen.

Tabelle 1: Spezialisierte Service-Manager

Tabelle 1: Spezialisierte Service-Manager

In Listing 7 ist beispielhaft eine Konfiguration für den ControllerManager und den ViewHelperManager zu sehen. Der Aufbau ist genauso wie beim Service-Manager, d. h. es gibt Konfigurationsschlüssel für invokables und factories. Dabei wird ein Controller direkt instanziiert, während für einen View-Helper eine Factory verwendet werden soll. Dieselbe Konfiguration in einer Module-Klasse ist in Listing 8 dargestellt. Dort wird die Konfiguration für Controller und View-Helper mit den entsprechenden Konfigurationsmethoden bereitgestellt. Dabei kommen auch die korrespondierenden Featureinterfaces vom ZendModuleManager zum Einsatz.

Achtung! Bitte bedenken Sie, dass beim Aktivieren des Konfigurationscaches nur die Konfiguration aus der getConfig()-Methode gecacht wird und die anderen Methoden bei jedem Seitenaufruf dennoch erneut aufgerufen werden. Außerdem dürfen Sie keinerlei Factory Closures verwenden, wenn Sie die Konfiguration Ihrer ZF2-Anwendung cachen möchten!

An dieser Stelle schauen wir uns noch die Factory-Klasse für den View-Helper SmallPinkPrincessBike etwas genauer an (Listing 9). Da diese Factory im ViewHelperManager verwendet wird, bekommt die createService()-Methode nicht die Instanz vom Service-Manager übergeben, sondern den ViewHelperManager. Dieser bietet jedoch keinen Zugriff auf die angeforderte Entität für den Bezeichner SmallPinkPrincessBike. Der ViewHelperManager hat aber Zugriff auf den Service-Manager, wie Sie in Abbildung 3 sehen können. Somit können wir über diesen Umweg auf den Service-Manager und damit auf die gewünschte Entität zugreifen. Achten Sie auch darauf, dass der übergebene Parameter von $serviceLocator in $viewHelperManager umbenannt wurde, da diese Variable den übergebenen Parameter besser bezeichnet.

Fazit: Bevorzugen Sie Factory-Klassen statt Factory Closures!

Controller und Service-Manager

Alle Controller werden im ControllerManager konfiguriert, wie wir in den Listings 7 und 8 gesehen haben. In Listing 6 haben wir wiederum gesehen, dass wir in den Aktionsmethoden eines Controllers mithilfe der getServiceLocator()-Methode relativ simpel auf den Service-Manager zugreifen können. Technisch umgesetzt wird dies übrigens durch die Implementation vom ZendService-ManagerServiceLocatorAwareInterface im Controller. Diese Vorgehensweise ist zwar bequem und einfach, birgt aber ein großes Problem.

Das Service-Locator-Entwurfsmuster steht allgemein unter dem Verdacht, ein Anti-Pattern zu sein (siehe z.B. hier oder hier). Entwickler werden leicht dazu verleitet, die Service-Locator-Instanz an alle Objekte zu übergeben, die bei drei nicht auf den Bäumen sind. Das ist auf den ersten Blick eine große Hilfe für den Entwickler, denn somit kann er bequem auf alle registrierten Objekte zugreifen, ohne sich wirklich um die Abhängigkeiten zwischen den Objekten kümmern zu müssen. Ohne es zu wissen, bauen sich Einsteiger damit eine feste Abhängigkeit ihrer Controller zum Service-Manager ein, der das Testen der Controller und die Wartbarkeit nachhaltig verschlechtert.

Je intensiver Sie in Ihren Controllern vom Service-Manager Gebrauch machen, desto größer wird die Abhängigkeit zum Service-Manager und desto schneller verlieren Sie den Überblick, welche Abhängigkeiten Ihre Controller zu anderen Klassen eigentlich haben. Wenn Sie sich erneut Listing 6 anschauen und nun vorstellen, dass der Controller noch ein halbes Dutzend weiterer Methoden aufweist, die ebenfalls auf den Service-Manager zugreifen, dann werden Sie im Laufe der Zeit immer größere Schwierigkeiten haben, den Überblick zu bewahren. Deshalb sollten Sie möglichst frühzeitig in der Entwicklung Ihrer ZF2-Anwendungen dazu übergehen, dass Sie alle Abhängigkeiten für den Controller sauber in einer Factory injizieren, statt sich von dem automatisch injizierten Service-Manager direkt abhängig zu machen.

Betrachten Sie dazu die neue Factory-Klasse für den Controller in Listing 10. Hierin werden alle benötigten Objekte zuerst mithilfe des Service-Managers geholt, um sie dann im Anschluss an die neue Instanz des Controllers zu übergeben. Diese Factory-Klasse hilft Ihnen nun, alle Abhängigkeiten auf einen Blick zu erfassen. Auch können Sie nun leichter erkennen, wenn Ihr Controller zu viele externe Abhängigkeiten hat, um dann über eine Refaktorierung nachdenken zu können.

In Listing 11 ist der überarbeitete Controller dargestellt. Dieser hat auf den ersten Blick zwar deutlich an Umfang zugenommen, doch besteht der Hauptteil aus Setter- und Getter-Methoden für die benötigten Abhängigkeiten. Wichtiger ist jedoch, dass Sie keine harte Abhängigkeit zum Service-Manager mehr haben. Zusätzlich haben Sie auch die Testbarkeit des Controllers erhöht, da die Setter-Methoden lediglich eine Implementation der entsprechenden Interfaces und keine konkrete Klasse mehr voraussetzen. Sie können nun einen einfachen Unit Test schreiben, der die Funktionalität des Action-Controllers testen kann, ohne den Service-Manager instanziieren oder mocken zu müssen. Dies wäre mit der hart kodierten Abhängigkeit zum Service-Manager nicht möglich gewesen.

Fazit: Vermeiden Sie den Einsatz vom ServiceLocatorAwareInterface, besonders auch für eigene Klassen!

Initializer sparsam einsetzen

Anders als das eben betrachtete ServiceLocatorAwareInterface kann der Einsatz von Initializern jedoch durchaus Sinn ergeben. Die Idee hinter den Initializern ist, dass wir häufig verwendete Objekte nicht jedes Mal selbst in einer Factory injizieren müssen, sondern dies automatisieren können. Zu jedem Initializer gibt es in der Regel ein korrespondierendes AwareInterface. Ein Initializer prüft dann für jedes durch den Service-Manager initialisiertes Projekt, ob das entsprechende AwareInterface implementiert wird. Wenn dies der Fall ist, dann kümmert sich der Initializer darum, dass das benötigte Objekt automatisch injiziert wird.

Während der Einsatz vom ServiceLocatorAwareInterface eher vermieden werden sollte, ergibt ein Einsatz des ZendI18nTranslatorTranslatorAwareInterface durchaus Sinn. Jedes Objekt, das Zugriff auf den Translator benötigt, muss lediglich das Interface implementieren und Sie müssen sich nicht um die Injektion in Ihrer Factory kümmern. Ähnlich verhält es sich auch beim Event-Manager mit dem ZendEventManagerEventManagerAwareInterface.

Dennoch sollten Sie mit dem Einsatz von Initializern eher sparsam umgehen. Jeder in einem Service-Manager registrierte Initializer wird nämlich nach jeder Initialisierung eines Objekts mithilfe dieses Service-Managers automatisch ausgeführt. Wenn Sie nun viele verschiedene Initializer verwenden, kann dies die Initialisierungsphase Ihrer Anwendung unter Umständen durchaus signifikant verlängern. Ist ein Initializer dann auch noch zeitraubend implementiert, kann es sogar noch größere Auswirkungen haben. Außerdem erhöht die Verwendung von vielen AwareInterfaces nicht unbedingt die Übersichtlichkeit Ihrer Anwendung.

Doch wie funktioniert das Prinzip nun an einem Beispiel? Als Erstes benötigen Sie das entsprechende AwareInterface. In Listing 14 ist das Wheel12InchAwareInterface zu sehen, mit dem wir sicherstellen möchten, dass ein Objekt ein 12 Zoll großes Vorderrad und ein 12 Zoll großes Hinterrad automatisch injiziert bekommt. In Listing 15 finden Sie den Wheel12InchInitializer, der ZendService-ManagerInitializerInterface implementiert und für jede Instanz überprüft, ob das Wheel12InchAwareInterface implementiert ist. Wenn dies der Fall ist, injiziert der Wheel12InchInitializer automatisch ein 12 Zoll großes Vorder- und Hinterrad. Die Konfiguration des Initializers in der module.config.php-Datei des Shop-Moduls wird auszugsweise in Listing 16 gezeigt. Für den Konfigurationsschlüssel initializers wird im Service-Manager lediglich der Initializer aufgeführt, der zum Einsatz kommen soll. Ebenfalls in Listing 16 ist die Konfiguration einer neuen Klasse zu finden, die wir uns nun genauer anschauen werden.

Die Klasse ShopEntitySpecialBike (Listing 17) erweitert die bestehende Klasse ShopEntityBike und implementiert zugleich das Wheel12InchAwareInterface. Da wir nun mithilfe des Wheel12InchInitializer sicherstellen, dass Vorderrad und Hinterrad automatisch injiziert werden, können wir den Konstruktor entsprechend überschreiben, sodass nur noch Name, Farbe und Klingel als Parameter erwartet werden. Die Klasse ShopEntityBike stellt übrigens bereits sicher, dass die erforderlichen Methoden aus dem Wheel12InchAwareInterface bereits implementiert werden. In Listing 18 ist nun die SpecialBikeFactory zu sehen, die nur noch Name, Farbe und Klingel an den Konstruktor von SpecialBike übergibt. Wenn wir nun die Instanz von SpecialBike vom Service-Manager anfordern und ausgeben (wird im ShopControllerIndexController in der specialAction() gemacht), erkennen wir in der Ausgabe, dass die beiden Instanzen für das Vorder- und Hinterrad automatisch injiziert worden sind (Abb. 4).

Abb. 4: Ausgabe aus dem Service-Manager

Abb. 4: Ausgabe aus dem Service-Manager

Fazit: Initializer sind sehr hilfreich, wenn Sie bestimmte Objekte in vielen anderen verwenden möchten. Sie sollten dieses Feature jedoch sparsam einsetzen, da dies auch negative Auswirkungen auf die Performance und Übersichtlichkeit Ihrer Anwendung haben könnte.

ZF2 Service Factories

Das Zend Framework 2 stellt von Haus aus eine ganze Reihe an Service-Factory-Klassen für diverse Komponenten bereit, die Sie direkt verwenden können. Dabei verwendet diese Factory die zusammengeführten Konfigurationsdaten aus allen Modulen und prüft, ob ein bestimmter Konfigurationsschlüssel vorhanden ist. Wenn dies der Fall ist, wird die Instanz generiert und zurückgegeben. Listing 12 zeigt ein Beispiel für eine Datenbankadapterkonfiguration unter dem Schlüssel db. Folgende Service-Factory-Klassen stellt das ZF2 bereit:

ZendCacheServiceStorageCacheFactory ZendDbAdapterAdapterServiceFactory ZendI18nTranslatorTranslatorServiceFactory ZendLogLoggerServiceFactory ZendNavigationServiceDefaultNavigationFactory ZendSessionServiceSessionConfigFactory ZendSessionServiceSessionManagerFactory ZendSessionServiceStorageFactory

Zusätzlich zu den „normalen“ Factories gibt es auch einige abstrakte Factory-Klassen, die etwas anders funktionieren. Dabei können z. B. nicht nur ein einzelner Datenbankadapter, sondern eine ganze Reihe an Adaptern mit einer Factory definiert werden. Listing 13 zeigt ein entsprechendes Beispiel. Folgende abstrakte Service-Factory-Klassen stellt das ZF2 bereit:

ZendCacheServiceStorageCacheAbstractServiceFactory ZendDbAdapterAdapterAbstractServiceFactory ZendFormFormAbstractServiceFactory ZendLogLoggerAbstractServiceFactory ZendSessionServiceContainerAbstractServiceFactory

Zusammenfassung und Ausblick

Der Einsatz der ZendService-Manager-Komponente birgt ein paar Stolpersteine, die Sie jedoch leicht vermeiden können:

  • Sorgen Sie dafür, dass Sie in den Factories immer alle benötigten Objekte und nicht den Service-Manager selbst injizieren, um sich nicht direkt vom Service-Manager abhängig zu machen.
  • Verwenden Sie keine Factory Closures, da sich diese nicht cachen lassen.
  • Vermeiden Sie den Einsatz vom ZendService-ManagerServiceLocatorAwareInterface.
  • Initializer sind praktisch, sollten aber eher sparsam eingesetzt werden.

Übrigens wird die ZendService-Manager-Komponente für das kommende Zend Framework 3 besonders hinsichtlich Performance überarbeitet werden.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -