Services

1. Einleitung

Die Kapselung von Funktionalität in diverse eigenständige Komponenten ist eine in der objektorientierten Welt sehr verbreitete Methodik. Der Vorteil dieser Aufteilung wird im Mehrschicht-Architektur-Pattern beschrieben. Dabei geht der Entwickler beim Erstellen des Designs der Software davon aus, dass unterschiedliche Schichten der Software jeweils typischen Aufgaben übernehmen.

Da Schichten oder Services üblicherweise durch ein oder mehrere Klassen repräsentiert werden, leitet sich daraus unmittelbar die Aufgabe ab, Objekte der jeweiligen Schichten zu erstellen. Gleichermaßen ist der Entwickler beim Entwurf von wiederverwendbaren Schichten - Services - gefordert, diese mit einer klaren und einfach zu konfigurierenden API auszustatten. Die Datenschicht einer Anwendung benötigt beispielsweise eine Komponente zur Anbindung an eine externe Datenquelle, eine Business-Komponente benötigt Kenntnis über das Umfeld, in dem die Applikation eingesetzt ist. Darüber hinaus bestehen Abhängigkeiten der verschiedenen Schichten untereinander.

2. Grundlagen

Wie bereits im Kapitel Page-Controller beschrieben wurde, besitzt das APF eine spezielle Art der Erstellung von Objekten, die zur Erzeugung von GUI-Elementen eingesetzt werden. Da auch die Objekte der Business- und Datenschicht das Umfeld, in dem sie eingesetzt sind, kennen müssen (siehe Kapitel Konfigurations-Schema), ist es notwendig, dass diese mit einem ähnlichen Mechanismus erzeugt werden.

Das Framework stellt zwei Komponenten zur Erzeugung von Objekten zur Verfügung, die die erforderlichen Daten des Umfelds (Kontext, Sprache, ...) den erzeugten Objekten automatisch mitgeben.

Die folgenden Kapitel beschreiben die genannten Komponenten und deren Bedeutung für die Entwicklung.

3. Erstellung von Objekten

Das Adventure PHP Framework kennt mehrere Mechanismen zur Erstellung und Initialisierung von Objekten. Um die bereits genannten Informationen des Umfelds an jedes neu erstellte Objekt zu übermittel, wird das Prinzip der dependency injection eingesetzt.

Da das Framework eine generische Möglichkeit bietet, ein beliebige Klasse als Singleton oder Session-Singleton zu erzeugen, verzichtet die hierfür bereitgestellte Implementierung zu Gunsten des generischen Ansatzes auf Konstruktor-Argumente. Aus diesem Grund wird bei der Initialisierung ausschließlich auf method injection bzw. setter injection gesetzt. Die erforderlichen Daten oder Abhängigkeiten werden also nach der Erzeugung dem Objekt zur Verfügung gestellt.

3.1. GUI-Objekte

Die Erstellung von Objekten der Präsentationsschicht wird dabei von den Factory-Methoden des Page-Controller übernommen. Jedem Objekt wird nach der Erstellung über definierte Methoden der aktuelle Context, die aktuelle Sprache und die Attribute des APF-DOM-Elements übergeben.

Sofern ausschließlich im Release enthaltene TagLibs oder Parser-Methoden verwendet werden, muss dieser Mechanismus nicht näher betrachtet werden. Hinweise zur Erstellung eigener TagLibs können dem Kapitel Implementierung von Tags entnommen werden.

3.2. Service-Objekte

Damit sich der Entwickler bei der Erstellung von Services bzw. weiteren Schichten seiner Applikation nicht um die Interna des Frameworks kümmern muss, stehen ihm drei Möglichkeiten bereit, vorkonfigurierte Objekte zu erzeugen. Dazu stellt die Klasse APFObject nachfolgend beschriebene Methoden bereit, die auf die Funktionalität des ServiceManager und DIServiceManager zurückgreifen.

Details der im folgenden beschriebenen Methoden finden sich in der API-Dokumentation oder über die integrierten Funktionen der IDEs (siehe Empfohlene Entwicklungs-Werkzeuge).

3.2.1. Einfache Services mit dem ServiceManager

Die Methode getServiceObject() liefert ein mit dem aktuellen Context und der aktuellen Sprache initialisiertes Objekt zurück. Sie nutzt dabei die Funktionalität des ServiceManagers (siehe API-Dokumentation). Da die Funktion auf den Context und die Sprache des aktuellen Objektes zurückgreift, ist es sogar innerhalb von Modulen möglich, zur Applikation differente Contexte - etwa für einzelne Module - zu vergeben.

3.2.2. Initialisierte Services mit dem ServiceManager

Die Methode getAndInitServiceObject() erweitert die Initialisierung aus Kapitel 3.2.1 um einen dynamischen Initialisierungsparameter. Der als drittes Argument übergebene Parameter wird der init()-Methode des Objektes nach der Erstellung übergeben und kann dort zur Konfiguration genutzt werden. Der Parameter ist typ-unabhängig. Das bedeutet, dass zur Initialisierung nicht nur einfache Datentypen sondern auch beliebige Objekte genutzt werden können.

Nachteil dieser Methode ist jedoch, dass die zur Initialisierung der Komponente genutzten Parameter der Anwendung bekannt sein müssen. Aus diesem Grund eignet sich diese Variante vor allem für die Konfiguration eines Services mit Applikations-spezifischen Inhalten (z.B. Kennung einer Applikation bei dynamischer Einbindung über eine TagLib).

3.2.3. Komplexe Services

Die Methode getDIServiceObject() nutzt die Funktion des DIServiceManager, der einen dependency injection container für das APF zur Verfügung stellt. Services können dabei sowohl durch wiederum andere Services als auch durch statische Konfigurationsparameter für den Einsatz vorbereitet werden.

Die Nutzung des DIServiceManagers bietet zudem den Vorteil, dass der Code deutlich besser von der Konfiguration der Applikation getrennt werden kann. Parameter werden dem Service von aussen bei der Erzeugung eingeimpft und müssen nicht mehr Teil der Applikationslogik sein. Gut gekapselte Services sind zudem auf sehr einfache Weise wiederverwendbar und die zusätzliche Kapselung einer Komponente erhöht sich die Testbarkeit. Dies wirkt sich insbesondere für Unit-Tests positiv aus.

4. Praktischer Einsatz

Der praktische Einsatz gestaltet sich im Gegensatz zu den theoretischen Grundlagen denkbar einfach. Die folgenden Kapitel zeigen dabei jeweils die Verwendung der genannten Methoden an einem Beispiel.

4.1. Einfache Services

Wie im Kapitel 3.2.1 beschrieben, erzeugt die Methode getServiceObject() ein einfaches Service-Objekt. Die Verwendung gestaltet sich wie folgt:

PHP-Code
class MyObject extends APFObject { public function doSomething(){ $myService = &$this->getServiceObject( $namespace, $serviceName, [$type = APFService::SERVICE_TYPE_SINGLETON], [$instanceId = null] ); $myService->doSomethingElse(); } }

Die Methode APFObject::getServiceObject() besitzt folgende Parameter:

  • $namespace: Gibt den Namespace an, in dem die Klasse der Service-Implementierung zu finden ist (Beispiel: namespace::of::my::service).
  • $serviceName: Gibt den Namen des Services und gleichzeitig den Namen der entsprechenden Klasse an (Beispiel: MyServiceName).
  • $type: Der Parameter $type definiert den Service-Typ, der die Gültigkeit der Instanz beschreibt. Möglich sind die Werte SINGLETON, SESSIONSINGLETON und NORMAL. Es empfiehlt sich dazu die Konstanten aus APFService zu verwenden, z.B. APFService::SERVICE_TYPE_SESSIONSINGLETON. Dieser Parameter ist optional, als Standard wird SINGLETON verwendet. Siehe auch Singleton / SessionSingleton.
    Ab Version 1.16. sind Services vom Typ SINGLETON bzw. SESSIONSINGLETON pro Kombination aus Sprache und Kontext einmalig. Das ermöglicht, einzelne Services oder komplette Module in einer Anwendung mehrfach mit verschiedenem Kontext unabhängig voneinander zu nutzen. Mehr Infos finden Sie im Artikel Migration von 1.15 auf 1.16.
  • $instanceId: Ermöglicht bei den Service-Typen SINGLETON und SESSIONSINGLETON eine eigene Instanz-Bezeichnung anzugeben, mit der der Service identifiziert wird. Dieser Parameter ist optional, als Standard wird eine Instance-ID aus Namespace, Service-Name, sowie ab Version 1.16. Sprache und Kontext, vom ServiceManager gebildet.
Das Service-Implementierung muss vor der Erstellung per import() nicht explizit eingebunden werden. Dies erledigt der ServiceManager für Sie.

4.2. Initialisierte Services

Die Erstellung eines vorkonfigurierten Service-Objekts gestaltet sich wie folgt:

PHP-Code
class MyObject extends APFObject { public function doSomething(){ $initParam = 'foo'; $myService = &$this->getAndInitServiceObject( $namespace, $serviceName, $initParam, [$type = APFService::SERVICE_TYPE_SINGLETON], [$instanceId = null] ); $myService->doSomethingElse(); } }

Die Methode APFObject::getAndInitServiceObject() besitzt folgende Parameter:

  • $namespace, $serviceName, $type und $instanceId: Diese Parameter entsprechen denen in getServiceObject() (siehe oben).
  • $initParam: Der Parameter, der dem erzeugten Service-Objekt mit der Methode init() übergeben wird. Er kann jeden beliebigen Typ annehmen, daher können auch andere Objekte (Services), Arrays oder skalare Typen übergeben werden.

4.3. Komplexe-Services

Die Erstellung eines Services mit Hilfe des dependency injection-Containers unterscheidet sich von den zuvor aufgezeigten Varianten. Jedes DI-Service-Objekt ist durch eine Konfiguration beschrieben, da ein Service nicht nur für sich steht, sondern auch zur Initialisierung eines anderen dienen kann.

So lassen sich mit Hilfe des DIServiceManager Services mit Abhängigkeiten zu anderen Services definieren.

4.3.1. Konfiguration

Für die Beschriebung eines Services mit dem Namen MyService und dem Namespace modules::mymodule::services muss die Konfigurationsdatei

Code
/config/modules/mymodule/services/{CONTEXT}/{ENVIRONMENT}_serviceobjects.ini

angelegt werden. Die Definition des Services delbst erfolgt über das folgende Schema:

APF-Konfiguration
[{ServiceName}] servicetype = "" namespace = "" class = "" [init.{INITKEY}.method = "" init.{INITKEY}.namespace = "" init.{INITKEY}.name = ""] [conf.{INITKEY}.method = "" conf.{INITKEY}.value = ""]
Definition:
  • servicetype: Die Direktive servicetype definiert die Art des Services. Gültige Werte sind SINGLETON, SESSIONSINGLETON, NORMAL und CACHED (ab 1.16). Details können dem Kapitel Verwendung des ServiceManager entnommen werden.
    Seit Version 1.16 werden Objekte vom Typ SINGLETON und SESSIONSINGLETON pro Kontext-Sprach-Kombination einmalig erzeugt. Der Service-Typ CACHED wurde mit dieser Version neu eingeführt und entspricht dem Service-Typ NORMAL für Version <= 1.15. Dabei wird das Service-Objekt vom DIServiceManager vorgehalten und bei der nächsten Anforderung nicht neu erzeugt und initialisiert, sondern aus dem Cache bedient. NORMAL liefert nun für jeden Aufruf ein neues Objekt zurück. Weitere Informationen finden sich im Artikel Migration von 1.15 auf 1.16.
  • namespace definiert den Namespace der Service-Implementierung.
  • class beschreibt den Namen der Service-Klasse.
  • Die init-Sektion definiert eine Initialisierung per dependency injection, conf eine Konfiguration mit statischen Daten.
  • Das Attribut method einer init-Subsektion definiert die Method, mit der die Initialisierung vorgenommen werden soll, namespace und name referenzieren das dabei zu verwendende Service-Objekt. Für das definierte Objekt muss eine Konfiguration gemäß dem oben beschriebenen Schema existieren.
  • Das Attribut method in der conf-Subsektion definiert die Method, mit der die "statische" Initialisierung vorgenommen werden soll. Dabei wird der Wert, der im Attribut value definiert ist, der zuvor beschriebenen Methode als Parameter mitgegeben.
4.3.2. Beispiel

Im folgenden Beispiel soll eine Business-Schicht-Komponente mit statischen Konfigurationsparametern, dem zur Applikation gehörenden Daten-Schicht-Service und einem Provider, der sich um die Bereitstellung von Konfigurationsparametern kümmert initialisiert werden. Der Name des Services soll GuestbookService lauten, der Namespace der Komponente ist modules::guestbook. Die Implementierung des Services übernimmt die Klasse GuestbookManager aus dem Namespace modules::guestbook::biz.

Um den dependency injection-Container nutzen zu können, muss zunächst eine Konfiguration für den GuestbookService angelegt werden. Die Datei

Code
/config/modules/guestbook/{CONTEXT}/{ENVIRONMENT}_serviceobjects.ini

angelegt und mit der Definition des Services gefüllt werden:

APF-Konfiguration
[GuestbookService] class = "GuestbookManager" namespace = "modules::guestbook::biz" servicetype = "SINGLETON"

Für die Datenschicht-Komponente und den Provider muss ebenso eine Konfiguration extistieren. Der Namespace der Datenschicht-Komponente DataService lautet dabei modules::guestbook, der Provider mit dem Namen ExtendedConfigProvider residiert im Namespace modules::guestbook::provider. Da der Namespace der Datenschicht-Komponente mit der ds GuestbookService identisch ist, kann die Konfiguration desselben in der oben erstellten Konfigurationsdatei vorgenommen werden. Der Provider hingegen muss in einer eigenen Konfigurationsdatei definiert werden. Diese lautet:

Code
/config/modules/guestbook/provider/{CONTEXT}/{ENVIRONMENT}_serviceobjects.ini

Der Inhalt der Datei {ENVIRONMENT}_serviceobjects.ini unter dem Namespace modules::guestbook hat damit den folgenden Inhalt:

APF-Konfiguration
[GuestbookService] namespace = "modules::guestbook::biz" class = "GuestbookManager" servicetype = "..." [DataService] namespace = "modules::guestbook::biz" class = "GuestbookMapper" servicetype = "..."

Die Datei {ENVIRONMENT}_serviceobjects.ini aus dem Namespace modules::guestbook::provider beinhaltet die Konfiguration des Konfigurationsservice:

APF-Konfiguration
[ExtendedConfigProvider] namespace = "modules::guestbook::biz::provider" class = "SpecialConfigProvider" servicetype = "..."

Um dem DIServiceManager mitzuteilen, dass dieser mit den beiden genannten Komponenten und zwei statischen Parametern initialisiert werden soll, muss die Sektion GuestbookService um die Initialisierungsanweisungen erweitert werden. Diese haben die folgende Gestalt:

APF-Konfiguration
init.database.method = "setDBService" init.database.name = "modules::guestbook" init.database.namespace = "DataService" init.exconf.method = "setConfigProvider" init.exconf.name = "modules::guestbook::provider" init.exconf.namespace = "ExtendedConfigProvider" conf.appname.method = "setAppId" conf.appname.value = "123" conf.cache.method = "setCacheActive" conf.cache.value = "false"

Wie dem Kasten zu entnehmen ist, können beliebig viele Initialisierungen definiert werden. Hierzu ist es lediglich notwendig Schlüssel für die Sub-Sektionen zu vergeben.

Die Nutzung des GuestbookService gestaltet sich dann wie folgt:

PHP-Code
class ServiceConsumer extends APFObject { public function doSomething(){ $service = &$this->getDIServiceObject( 'modules::guestbook', 'GuestbookService' ); $service->doSomethingElse(); }

Die Implementierung des Services (Klasse: GuestbookManager) hat dabei folgende Gestalt:

PHP-Code
class GuestbookManager extends APFObject { public function setDBService($dbService){ $this->dbService = $dbService; } public function setConfigProvider($provider){ $this->configProvider = $provider; } public function setAppId($appId){ $this->appId = $appId; } public function setCacheActive($cacheActive){ $this->cacheActive = $cacheActive; } public function doSomethingElse(){ } ... }

Innerhalb der Methode doSomethingElse() stehen dann alle injizierten Parameter zur Nutzung bereit.

Ein weiteres Beispiel findest sich auf der Wiki-Seite Erzeugen des GORM mit dem DIServiceManager.

Kommentare

Möchten Sie den Artikel eine Anmerkung hinzufügen, oder haben Sie ergänzende Hinweise? Dann können Sie diese hier einfügen. Die bereits verfassten Anmerkungen und Kommentare finden Sie in der untenstehenden Liste.
« 1   »
Einträge/Seite: | 5 | 10 | 15 | 20 |
1
Christian 24.09.2009, 22:28:48
Hallo Sebastian,

dies ist seit Version 1.10 nicht mehr der Fall, hier werden die Komponenten ebenfalls vom ServiceManager für dich eingebunden. Die Doku wird dahingehend noch im Rahmen von Release 1.11 überarbeitet.
2
Sebastian 24.09.2009, 10:53:44
Entgegen der Funktionsweise des DIServiceManagers müssen die betreffenden Services bei _getServiceObject bzw. __getAndInitServiceObject ja noch per import() eingebunden werden. Welchen Sinn macht dann noch die Angabe des Namespaces bei der Referenzierung?