Die Kapselung von Funktionalität in 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.
Um die Kapselung von Funktionalität und damit die Austauschbarkeit innerhalb einer Anwendung sicherzustellen, soll die aufrufende Schicht die Interna der aufgerufenen Schicht möglichst nicht kennen. Um dies zu erreichen gilt es insbesondere die Art der Erzeugung und die Konfiguration der aufgerufenen Schicht zu verstecken.
Die folgenden Kapitel beschreiben basierend auf den im APF verfügbaren Methoden zur Erzeugung von Objekten Mechanismen und Tools um die Kapselung von Funktionalität einfach zu realisieren und dabei gleichzeitig klare Strukturen innerhalb einer Anwendung zu realisieren.
Das Adventure PHP Framework nutzt verschiedene Prinzipien zur Erzeugung von Objekte innerhalb des Front-Controller und Page-Controller. Insbesondere Front-Controller-Actions und Tags zur Kapselung von UI-Funktionaliäten müssen mit Informationen ihres Umfelds versorgt werden. Dies reicht von der Bekanntgabe des Eltern-Elements bis zur Weitergabe von Kontext und Sprache um in einer Komponente auf eine von beiden Werten abhängige Konfiguration zugreifen zu können. Das APF nutzt dazu das Factory- und Dependency Injection-Prinzip.
Da das Framework seinen Schwerpunkt auf die Gestaltung der UI und die damit verbundenen Funktionaltäten legt, ist der Entwickler selbst dafür verantwortlich, Objekte bzw. Services ausserhalb dieses Bereichs zu erzeugen. Hierzu stehen geeignete Hilfsmittel wie der ServiceManager bzw. der DIServiceManager zur Verfügung.
Zur Erzeugung und gleichzeitiger Initialisierung einer beliebigen Klasse als Singleton, SessionSingleton oder ApplicationSingleton unterstützt das APF zwei anerkannte Konzepte: constructor injection und method injection bzw. setter injection.
Bei der constructor injection werden die Abhängigkeiten direkt in den Konstruktor der Klasse injiziert, nutzen Sie method injection bzw. setter injection, so werden die erforderlichen Daten oder Abhängigkeiten dem Objekt nach der Erzeugung zur Verfügung gestellt.
Möglichkeiten zur Ausführung von Initialisierungsmethoden für die Nutzung des method injection bzw. setter injection Konzeptes finden Sie in Kapitel Kapitel 4.3.1 (Stichwort: setupmethod).
Der ServiceManager ist eine Erweiterung der Singleton-, SessionSingleton- und ApplicationSingleton-Implementierungen, die unter Erzeugung von Objekten beschrieben werden. Der ServiceManager ist im Vergleich zu den genannten Implementierungen stärker im Framework verwoben und die Klasse APFObject bietet die Convenience-Methode APFObject::getServiceObject() an, die einfach zur Erstellung von Objekten genutzt werden kann.
Dabei werden die in der aktuellen Instanz vorhandenen Informationen wie Kontext und Sprache bereits automatisch an den ServiceManager übergeben und das erzeugte Objekt ist damit bereits vollständig initialisiert.
Um Objekte mit dem ServiceManager erzeugen zu können, müssen diese dem Interface APFService genügen. Dieses definiert die Möglichkeit, Kontext und Sprache zu injizieren und ermöglicht so, beliebige Klassen als Service zu kennzeichnen.
Das Interface gestaltet sich wie folgt:
interface APFService {
const SERVICE_TYPE_NORMAL = 'NORMAL';
const SERVICE_TYPE_CACHED = 'CACHED';
const SERVICE_TYPE_SINGLETON = 'SINGLETON';
const SERVICE_TYPE_SESSION_SINGLETON = 'SESSIONSINGLETON';
const SERVICE_TYPE_APPLICATION_SINGLETON = 'APPLICATIONSINGLETON';
public function setContext($context);
public function getContext();
public function setLanguage($lang);
public function getLanguage();
public function setServiceType($serviceType);
public function getServiceType();
}
Die oben aufgeführten Konstanten definieren die möglichen Erzeugungsmuster von Services, die nachfolgend beschriebenen Methoden ermöglichen die Injektion von Kontext und Sprache.
Innerhalb Ihrer Anwendung können Sie den ServiceManager direkt (siehe 3.2.1. Native Nutzung) oder über die Methode APFObject::getServiceObject() (siehe 3.2.2. Nutzung des Wrappers) nutzen. Die folgenden Kapitel zeigen die Nutzung sowie Vor- und Nachteile auf.
Der ServiceManager kann über die statische Methode getServiceObject() aus beliebigen Code-Stellen angesprochen werden. Der Aufruf gestaltet sich wie folgt:
use APF\core\service\ServiceManager;
$instance = ServiceManager::getServiceObject('VENDOR\..\Class', $context, $language);
Wie dem Aufruf zu entnehmen ist, müssen zum Zeitpunkt des Aufrufs der aktuelle Kontext und die aktuelle Sprache bekannt sein. Dies ist prinzipiell in jedem vom APF erzeugten Objekt der Fall, das das Framework für die Weitergabe der Informationen sorgt.
Erzeugen Sie Objekte selbst oder befinden Sie sich ausserhalb des Gültigkeitsbereichs von Objekten - z.B. in der index.php -, so können Sie z.B. auf die Instanz des Front-Controller zurückgreifen. Dieser wird in der Bootstrap-Datei üblicherweise mit dem aktuellen Kontext und der aktuellen Sprache ausgestattet. Der Aufruf gestaltet sich in diesem Fall wie folgt:
use APF\core\frontcontroller\FrontController;
use APF\core\singleton\Singleton;
$fC = Singleton::getInstance(FrontController::class);
$context = $fC->getContext();
$language = $fC->getLanguage();
use APF\core\service\ServiceManager;
$instance = ServiceManager::getServiceObject('VENDOR\..\Class', $context, $language);
Die Methode getServiceObject() bietet neben den verpflichtenden Angaben noch weitere Parameter zur Steuerung der Erzeugung und Initialisierung von Objekten. Bitte entnehmen Sie die Details der Parameter $arguments, $type und $instanceId Kapitel 3.2.2.
Die in der Klasse APFObject definierte Methode getServiceObject() kapselt den Aufruf des ServiceManager und kümmert sich selbständig um die Weitergabe von Kontext und Sprache. Sie können daher ein Objekt einfach wie folgt erstellen:
use APF\core\pagecontroller\APFObject;
class GodObject extends APFObject {
public function doSomething(){
$service =
$this->getServiceObject(
$serviceClass,
[$arguments = []],
[$type = APFService::SERVICE_TYPE_SINGLETON],
[$instanceId = null]
);
$service->doSomethingElse();
}
}
Die Methode APFObject::getServiceObject() besitzt folgende Parameter:
Der DIServiceManager ist ein Dependency Injection- und Inversion of Control-Container zur Erzeugung und Konfiguration von Services (siehe Inversion of Control Containers and the Dependency Injection pattern von Martin Fowler). Die Definition von Services basiert auf Konfigurationsdateien (Prinzip: wire by configuration), die sowohl die Service-Implementierung als auch Abhängigkeiten und Konfigurationsparameter definieren.
Zur Erzeugung der Services nutzt der DIServiceManager die Funktionalitäten des ServiceManager und bietet damit für alle Anwendungsfälle passende Gültigkeitsbereiche der erstellten Objekte an (Details siehe Erzeugung von Objekten).
Im Vergleich zum ServiceManager bietet der Dependency-Injection-Container eine weitere Abstraktionsschicht bei der Konfiguration und dem Bezug von Services. Sie referenzieren bei Bezug eines Service nicht mehr direkt die Implementierung, sondern seine Konfiguration. Dies erleichtert zum einen den Austausch von Implementierungen und erleichtert zum anderen auch bei Bedarf die Nutzung von MOCK-Implementierungen.
Zur Nutzung des Containers steht sowohl die statische Methode DIServiceManager::getServiceObject() als auch die Convenience-Methode APFObject::getDIServiceObject() zur Verfügung.
Bei der Nutzung von APFObject::getDIServiceObject() werden die in der aktuellen Instanz vorhandenen Informationen wie Kontext und Sprache bereits automatisch an den DIServiceManager übergeben und das erzeugte Objekt ist damit bereits vollständig initialisiert.
Services können dabei sowohl durch wiederum andere Services als auch durch statische Konfigurationsparameter für den Einsatz vorbereitet werden.
Um Objekte mit dem DIServiceManager erzeugen zu können, müssen diese dem Interface APFDIService genügen. Dieses definiert basierend auf dem Interface APFService Strukturen um Objekte mit dem Dependency-Injection-Container zu erstellen und zu verwalten.
Das Interface gestaltet sich wie folgt:
interface APFDIService extends APFService {
public function markAsInitialized();
public function markAsPending();
public function isInitialized();
}
Die oben aufgeführten Methoden erlauben dem Container die Abfrage des aktuellen Zustands des Objektes - beispielsweise den Initialisierungszustand.
Innerhalb Ihrer Anwendung können Sie den DIServiceManager direkt (siehe 4.2.1. Native Nutzung) oder über die Methode APFObject::getDIServiceObject() (siehe 4.2.2. Nutzung des Wrappers) nutzen. Die folgenden Kapitel zeigen die Nutzung sowie Vor- und Nachteile auf.
Der DIServiceManager kann über die statische Methode getServiceObject() aus beliebigen Code-Stellen angesprochen werden. Der Aufruf gestaltet sich wie folgt:
use APF\core\service\DIServiceManager;
$instance = DIServiceManager::getServiceObject('VENDOR\..', 'Service-Name', $context, $language);
Die beiden ersten Parameter definieren den Namespace und den Namen der Service-Definition. Diese referenzieren effektiv eine Konfigurations-Sektion, die den Service beschreibt (Details siehe Kapitel 4.3. Zum Zeitpunkt des Aufrufs müssen der aktuelle Kontext und die aktuelle Sprache bekannt sein. Dies ist prinzipiell in jedem vom APF erzeugten Objekt der Fall, das das Framework für die Weitergabe der Informationen sorgt.
Erzeugen Sie Objekte selbst oder befinden Sie sich ausserhalb des Gültigkeitsbereichs von Objekten - z.B. in der index.php, so können Sie z.B. auf die Instanz des Front-Controller zurückgreifen. Dieser wird in der Bootstrap-Datei üblicherweise mit dem aktuellen Kontext und der aktuellen Sprache ausgestattet. Der Aufruf gestaltet sich in diesem Fall wie folgt:
use APF\core\frontcontroller\FrontController;
use APF\core\singleton\Singleton;
$fC = Singleton::getInstance(FrontController::class);
$context = $fC->getContext();
$language = $fC->getLanguage();
use APF\core\service\DIServiceManager;
$instance = DIServiceManager::getServiceObject('VENDOR\..', 'Service-Name', $context, $language);
Die in der Klasse APFObject definierte Methode getDIServiceObject() kapselt den Aufruf des DIServiceManager und kümmert sich selbständig um die Weitergabe von Kontext und Sprache. Sie können daher ein Objekt einfach wie folgt erstellen:
use APF\core\pagecontroller\APFObject;
class GodObject extends APFObject {
public function doSomething(){
$service =
$this->getDIServiceObject(
$serviceNamespace
$serviceName
);
$service->doSomethingElse();
}
}
Die Methode APFObject::getDIServiceObject() besitzt folgende Parameter:
Die Erstellung und Konfiguration von Services mit dem DIServiceManager unterscheidet sich sehr deutlich von der Nutzung des ServiceManager. Jeder Service ist durch eine Konfiguration eindeutig beschrieben, da er nicht nur für sich steht, sondern auch zur Initialisierung eines anderen dienen kann. Dies ermöglicht Ihnen mit Hilfe des DIServiceManager Services mit Abhängigkeiten zu anderen Services zu definieren.
Die Definition eines Service erfolgt in einer Konfigurations-Sektion, die über den Namespace der Konfigurationsdatei und den Namen derselben adressiert wird. Der DIServiceManager nutzt den Konfigurationsmechanismus des APF zum Laden der Konfiguration.
Die Nutzung des ConfigurationManager bietet den Vorteil, Services in Abhängigkeit von Namespace, Kontext und Umgebung definieren zu können. Das bedeutet:
Die folgenden Kapitel beschreiben die Konfiguration von Services näher.
Die Definition eines Services zeichnet sich wie im vorangegangenen Kapitel beschrieben durch eine Konfigurations-Sektion in einer Datei mit dem Namen {ENVIRONMENT}_serviceobjects.ini aus. Der Wert für die Umgebung ist jeweils mit der Konfiguration Ihrer Applikation zu ersetzen (Standard: DEFAULT).
Innerhalb dieser Datei steht jede Sektion für eine eigenständige und voneinander unabhängig nutzbare Service-Definition. Der Inhalt einer solchen Sektion genügt folgendem Schema:
[{service-name}]
class = ""
servicetype = ""
[construct.{CONSTRUCT_KEY}.value = ""]
[construct.{CONSTRUCT_KEY}.namespace = ""
[construct.{CONSTRUCT_KEY}.name = ""]
[conf.{CONF_KEY}.method = ""
conf.{CONF_KEY}.value = ""]
[init.{INIT_KEY}.method = ""
init.{INIT_KEY}.namespace = ""
init.{INIT_KEY}.name = ""]
[setupmethod = ""]
Die einzelnen Bestandteile haben folgende Bedeutung:
conf.user.method = "setUser"
conf.user.value = "John"
conf.pass.method = "setPassword"
conf.pass.value = "Doe"
conf.url.method = "setUrl"
conf.url.value = "https://example.com/service/v1/soap"
init.weather.method = "setWeatherService"
init.weather.namespace = "VENDOR\namespace\of\service\definition"
init.weather.name = "open-weather-map-service"
init.db.method = "setDatabaseConnection"
init.db.namespace = "VENDOR\namespace\of\database\connection\definition"
init.db.name = "calendar-database-connection"
Die APF-Konfigurations-Komponente erlaubt die Nutzung von beliebigen Formaten für die Definition von Services, da der Zugriff auf die jeweiligen Formate abstrahiert und standardisiert ist. Dieser Vorteil lässt sich nutzen um Service-Definitionen beispielsweise als PHP- statt INI-Dateien abzulegen.
Möchten Sie den Service GuestbookMapper nach dem obigen Schema konfigurieren, so ist dafür folgende Konfigurations-Datei notwendig:
return [
'GuestbookMapper' => [
'servicetype' => 'NORMAL',
'class' => 'APF\modules\guestbook2009\data\GuestbookMapper',
'conf' => [
'db' => [
'method' => 'setConnectionName',
'value' => '...'
]
],
'init' => [
'orm' => [
'method' => 'setORMInitType',
'namespace' => 'APF\modules\guestbook2009\data',
'name' => 'GORM'
]
]
]
];
Mit Hilfe der Konfigurations-Direktive DIServiceManager::$configurationExtension lässt sich anschließend das verwendete Konfigurations-Schema definieren:
DIServiceManager::$configurationExtension = 'php';
Ein Service definiert sich über eine in Kapitel 4.3.1 beschriebene Sektion und gegebenenfalls Referenzen auf weitere Sektionen. Eine Sektion ist innerhalb einer Konfigurations-Datei definiert, die über ihren Namespace und den Namen {ENVIRONMENT}_serviceobjects.ini adressiert wird.
Der Name der Konfigurations-Datei ist fest definiert und kann nicht geändert werden. Dies erspart die Angabe der Konfigurations-Datei und erleichtert damit die Nutzung des DIServiceManager. Innerhalb einer Datei können mehrere Service definiert werden, wobei der Name der Sektionen - und damit die Name der Services - eindeutig sein muss.
Fragen Sie per
use APF\core\service\DIServiceManager;
$service = DIServiceManager::getServiceObject(
'VENDOR\namespace\of\service\definition',
'open-weather-map-service',
$context,
$language
);
einen Service an, so erwartet der DIServiceManager eine Service-Definition bzw. Konfigurations-Sektion mit dem Bezeichner open-weather-map-service in der Datei
/path/to/VENDOR/config/namespace/of/service/definition/{CONTEXT}/{ENVIRONMENT}_serviceobjects.ini
Die Parameter /path/to/VENDOR - sprich der Basis-Pfad zu den Applikations- und Konfigurationsdateien des Herstellers VENDOR - sowie {CONTEXT} und {ENVIRONMENT} sind abhängig von der Konfiguration Ihrer Applikation. Details zum Aufbau und der Verwendung von Konfigurations-Dateien entnehmen Sie bitte dem Kapitel Konfiguration bzw. Laden von Klassen.
Durch die Adressierung von Service über den Namespace der Konfigurations-Datei und den darin definierten Service-Namen hat der Entwickler die Freiheit, Service-Definitionen frei zu gestalten. Dies umfasst die Möglichkeit, abhängige Services innerhalb der gleichen Konfigurations-Datei zu definieren als auch in einer Datei innerhalb eines anderen Namespace.
Dies kann beispielsweise dazu genutzt werden, Basis-Services oder mehrfach eingesetzte Abhängigkeiten (z.B. Datenbank-Verbindungen) auszulagern und in einem Basis-Namespace zu definiert, wohingegen die konkreten Ausprägungen in einem tieferen Abschnitt des Namespace abgelegt werden.
Die folgenden Kapitel beschreiben unterschiedliche Anwendungsfälle und die zugehörige Service-Implementierung und Konfiguration des Services.
Der erste Anwendungsfall befasst sich mit der Erzeugung eines einfachen Services, der ein Liefer-Datum basierend auf dem Datum und der Uhzeit einer Bestellung berechnen soll. Um dem Kunden ein mögliches Liefer-Datum anzeigen zu können, soll der Service in einem (Document-)Controller erzeugt und genutzt werden.
Kümmern wir uns zunächst um die Struktur und den Aufbau des Service. Dieser soll folgendem Interface genügen:
namespace ACME\shop\order;
interface PreliminaryShipmentDateCalculator {
/**
* @param \DateTime $orderDate
* @return \DateTime
*/
public function getShipmentDate(\DateTime $orderDate);
}
Die Implementierung soll nun auf Basis des Eingabe-Datums eine Berechnung durchführen:
namespace ACME\shop\order;
class SimpleShipmentDateCalculator extends APFObject implements PreliminaryShipmentDateCalculator {
private $shipmentPeriodInDays = 10;
public function getShipmentDate(DateTime $orderDate) {
return $orderDate->add(\DateInterval::createFromDateString('+' . $this->shipmentPeriodInDays . 'd'));
}
}
Um den Sevice in einem Document-Controller zu erzeugen, ist es zunächt notwendig, eine Service-Konfiguration zu erstellen.
Dem Hinweis des letzten Kapitels folgend, soll die Service-Konfiguration unter dem Namespace ACME\shop\order abgelegt werden und shipment-date-calculator heißen. Der Service lässt sich damit im Controller wie folgt nutzen:
namespace ACME\shop\ui\checkout;
use ACME\shop\order\SimpleShipmentDateCalculator;
use APF\core\pagecontroller\BaseDocumentController;
class PreliminaryShipmentDateController extends BaseDocumentController {
public function transformContent() {
/* @var $service SimpleShipmentDateCalculator */
$service = $this->getDIServiceObject('ACME\shop\order', 'shipment-date-calculator');
$this->setPlaceHolder(
'shipment-date',
$service->getShipmentDate(new \DateTime())->format('Y-m-d')
);
}
}
Die Konfiguration - bzw. die Konfigurations-Datei - ist von mehreren Parametern abhängig. Für diesen Anwendungsfall gehen wir von folgenden Annahmen aus:
Unter den genannten Annahmen erwartet der DIServiceManager die Konfigurations-Datei
/path/to/ACME/config/shop/order/customer-one/DEFAULT_serviceobjects.ini
mit dem Inhalt
[shipment-date-calculator]
class="ACME\shop\order\SimpleShipmentDateCalculator"
servicetype="SINGLETON"
In Kapitel 4.4.1 wurde der SimpleShipmentDateCalculator statisch konfiguriert, sprich die Anzahl der durchschnittlich notwendigen Liefertage innerhalb des Codes der Klasse definiert. In diesem Kapitel erweitern wir die Definition des Interfaces und die Implementierung des Services so, dass eine konfigurierbare Anzahl von Tagen mitgegeben werden kann.
Das Interface PreliminaryShipmentDateCalculator erhält eine zusätzliche Methode setShipmentPeriod() um den Service konfigurieren zu können:
namespace ACME\shop\order;
interface PreliminaryShipmentDateCalculator {
/**
* @param int $shipmentPeriodInDays
*/
public function setShipmentPeriodInDays($shipmentPeriodInDays);
/**
* @param \DateTime $orderDate
* @return \DateTime
*/
public function getShipmentDate(\DateTime $orderDate);
}
Die Implementierung des Service erweitert sich damit wie folgt:
namespace ACME\shop\order;
class SimpleShipmentDateCalculator implements PreliminaryShipmentDateCalculator {
/**
* @var int
*/
private $shipmentPeriodInDays = 10;
public function setShipmentPeriodInDays($shipmentPeriodInDays) {
$this->shipmentPeriodInDays = $shipmentPeriodInDays;
}
public function getShipmentDate(\DateTime $orderDate) {
return $orderDate->add(\DateInterval::createFromDateString('+' . $this->shipmentPeriodInDays . 'd'));
}
}
Basierend auf den Annahmen in Kapitel 4.4.1.3 kann die Konfigurations-Sektion shipment-date-calculator wie folgt erweitert werden:
[shipment-date-calculator]
class="ACME\shop\order\SimpleShipmentDateCalculator"
servicetype="SINGLETON"
conf.shipment-days.method="setShipmentPeriodInDays"
conf.shipment-days.value="7"
Bei der Nutzung des Service wird nun gegenüber Kapitel 4.4.1.2 ein Datum von 7 Tagen als Lieferungs-Datum ausgegeben.
Erwartet eine Konfigurations-Methode mehrere Argumente, so lässt sich das durch mehrfache Angabe der Direktive conf.{CONF_KEY}.value erreichen. Zur Initialisierung eines Services mit der Methode
public function setShipmentPeriod($days, $hours, $minutes);
ist folgende Konfiguration erforderlich:
conf.shipment-period.method="setShipmentPeriod"
conf.shipment-period.value.1="7"
conf.shipment-period.value.2="2"
conf.shipment-period.value.3="10"
Die Zahlen 1 bis 3 können durch beliebige nummerische und alphanummerische Schlüssel ersetzt werden. Im folgenden Beispiel werden alphanummerische Bezeichner genutzt um die Konfigurations-Definition sprechender zu gestalten:
conf.shipment-period.method="setShipmentPeriod"
conf.shipment-period.value.days="7"
conf.shipment-period.value.hours="2"
conf.shipment-period.value.minutes="10"
Die Konstruktion bzw. die Konfiguration von Objekten und Services ist immer dann eine Herausforderung, wenn interne Zustände oder Ressourcen (z.B. Datenbank-Verbinungen) in Abhängigkeit zu mehreren Konfigurations-Parametern stehen. In diesem Fall ist es erforderlich, zunächst alle Abhängigkeiten aufzulösen, bzw. die benötigten Ressourcen zu injizieren und anschließend den "Betriebs-Zustand" der Instanz herzustellen.
Eine denkbare Lösung ist, die Parameter in einer definierten, gleichbleibenden Reihenfolge in der Konfiguration zu definieren und im Setter des letzten Parameters den gewünschten Objektzustand herzustellen. Dies birgt allerdings die Gefahr, dass bei fehlerhafter Konfiguration oder bei Session-übergreifender Nutzung der Status des Objekts nicht garantiert werden kann.
Um Fehler bei der Initialisierung zu vermeiden, bietet der DIServiceManager die Ausführung einer Initialisierungs-Methode an. Diese kann innerhalb der Konfiguration eines Service mit dem Attribut setupmethod definiert werden.
Im folgenden Beispiel soll der SimpleShipmentDateCalculator aus Kapitel 4.4.2 um die Möglichkeit erweitert werden ein potentielles Liefer-Datum aus einem Basis-Wert und einem Uhrzeit-abhängigen Faktor zu berechnen. Der Faktor soll aus zwei Konfigurations-Parametern berechnet werden und gilt innerhalb der durch die beiden Parameter definierten Uhrzeiten.
Die setupmethod dient nun dazu, die Berechnung des Faktors auszuführen, damit dieser bei der Berechnung des Liefer-Datums in der getShipmentDate()-Methode zur Verfügung steht. Hierzu erweitern wir zunächst das Interface PreliminaryShipmentDateCalculator, um die Start- und End-Uhrzeit zu definieren:
namespace ACME\shop\order;
interface PreliminaryShipmentDateCalculator {
/**
* @param int $shipmentPeriodInDays
*/
public function setShipmentPeriodInDays($shipmentPeriodInDays);
/**
* @param string $start
*/
public function setStartTime($start);
/**
* @param string $end
*/
public function setEndTime($end);
/**
* @param \DateTime $orderDate
* @return \DateTime
*/
public function getShipmentDate(\DateTime $orderDate);
}
Die Implementierung des Service erweitert sich damit wie folgt:
namespace ACME\shop\order;
class SimpleShipmentDateCalculator implements PreliminaryShipmentDateCalculator {
/**
* @var int
*/
private $shipmentPeriodInDays = 10;
/**
* @var \DateTime
*/
private $start;
/**
* @var \DateTime
*/
private $end;
/**
* @var int
*/
private $dynamicFactor;
public function setShipmentPeriodInDays($shipmentPeriodInDays) {
$this->shipmentPeriodInDays = $shipmentPeriodInDays;
}
public function setStartTime($start) {
$this->start = new \DateTime($start);
}
public function setEndTime($end) {
$this->end = new \DateTime($end);
}
...
}
Die Berechnung des $dynamicFactor soll nun in der Methode initialize() erfolgen. Die Implementierung der Klasse SimpleShipmentDateCalculator erweitert sich damit nochmals wie folgt:
class SimpleShipmentDateCalculator implements PreliminaryShipmentDateCalculator {
/**
* @var int
*/
private $shipmentPeriodInDays = 10;
/**
* @var \DateTime
*/
private $start;
/**
* @var \DateTime
*/
private $end;
/**
* @var int
*/
private $dynamicFactor;
public function setShipmentPeriodInDays($shipmentPeriodInDays) {
$this->shipmentPeriodInDays = $shipmentPeriodInDays;
}
public function setStartTime($start) {
$this->start = new \DateTime($start);
}
public function setEndTime($end) {
$this->end = new \DateTime($end);
}
public function initialize() {
$difference = $this->end->diff($this->start)->h;
$this->dynamicFactor = $difference > 1 ? $difference : 1;
}
public function getShipmentDate(\DateTime $orderDate) {
$period = $this->shipmentPeriodInDays;
if ($orderDate->diff($this->start)->h >= 0 && $this->end->diff($orderDate)->h <= 0) {
$period = $this->shipmentPeriodInDays;
}
return $orderDate->add(
\DateInterval::createFromDateString(
'+' . ($period) . 'd'
)
);
}
}
Die Instanz der Klasse SimpleShipmentDateCalculator wird in der Methode initialize() nicht als initialisiert markiert. Dies führt dazu, dass der DIServiceManager die Methode initialize() bei einer erneuten Anfrage des Objekts nochmals ausführt.
Ist der Zustand des Objektes nach der Initialisierung über einen längeren Zeitraum (z.B. die gesamte Lebensdauer des Objektes) gültig, kann dieses als initialisiert markiert werden. Der DIServiceManager ruft die setupmethod danach nicht mehr auf. Dies wird insbesonders für aufwendige Initialisierungsvorgänge empfohlen.
Die Implementierung der Methode initialize() ändert sich dafür wie folgt:
class SimpleShipmentDateCalculator implements PreliminaryShipmentDateCalculator {
...
public function initialize() {
$difference = $this->end->diff($this->start)->h;
$this->dynamicFactor = $difference > 1 ? $difference : 1;
$this->markAsInitialized();
}
...
}
Nun sind alle Vorarbeiten erledigt um den Service initialisieren zu können. Das folgende Kapitel zeigt Ihnen, wie Sie den Service zur Nutzung konfigurieren.
Die Implementierung des SimpleShipmentDateCalculator wurde im vorangegangenen Kapitel so angepasst, dass dieser mit den relevanten Konfigurations-Parametern ausgestattet und initialisiert werden kann.
Um die erweiterte Service-Implementierung nutzen zu können, ist folgende Konfiguration erforderlich:
[shipment-date-calculator]
class="ACME\shop\order\SimpleShipmentDateCalculator"
servicetype="SINGLETON"
setupmethod="initialize"
conf.shipment-days.method="setShipmentPeriodInDays"
conf.shipment-days.value="7"
conf.from.method="setStartTime"
conf.from.value="18:00:00"
conf.to.method="setEndTime"
conf.to.value="23:59:59"
Bei der Nutzung des shipment-date-calculator wird nun zwischen 18Uhr und 0Uhr ein dynamischer Faktor zur Auslieferungsdauer hinzugefügt.
In diesem Kapitel widmen wir uns der Initialisierung eines Services mit einem anderen. Dies wird immer dann der Fall sein, wenn einfache Datentypen in conf.*-Sektionen für die Repräsentation der Konfigurations-Daten nicht mehr ausreichend sind, oder ein Service einen anderen vollwertigen Service (beispielsweise eine Datenbank-Verbindung) für seine Arbeit benötigt.
Zur Initialierung eines Services lassen sich die init.*-Sektionen nutzen. Diese ermöglichen mit Hilfe einer Methode einen definierten Service an einen anderen Service zu übergeben. In diesem Kapitel entwerfen wir den DatabaseConfiguredShipmentDateCalculator, der die Lieferzeiten an Hand einer Datenbank-Tabelle evaluiert.
Zur Implementierung des DatabaseConfiguredShipmentDateCalculator nutzen wir die Interface-Definition PreliminaryShipmentDateCalculator aus Kapitel 4.4.1.1, die Implementierung der Methode getShipmentDate() vorschreibt.
Für den Aufbau der Datenbank-Verbindung nutzen wir den ConnectionManager bzw. die konkreten Treiber-Implementierungen - in unserem Fall den MySQLiHandler. Diese gestaltet sich wie folgt:
namespace ACME\shop\ui\checkout;
use APF\core\database\MySQLiHandler;
class DatabaseConfiguredShipmentDateCalculator implements PreliminaryShipmentDateCalculator {
/**
* @var MySQLiHandler
*/
private $databaseConnection;
/**
* @param MySQLiHandler $databaseConnection
*/
public function setDatabaseConnection(MySQLiHandler $databaseConnection) {
$this->databaseConnection = $databaseConnection;
}
public function getShipmentDate(\DateTime $orderDate) {
$select = 'SELECT `shipment_days` FROM ... WHERE ... ' . $orderDate->format('Y-m-d H:i:s') . ';';
$result = $this->databaseConnection->executeTextStatement($select);
$data = $this->databaseConnection->fetchData($result);
return $data['shipment_days'];
}
}
Ähnlich wie in Kapitel 4.4.3 definiert der DatabaseConfiguredShipmentDateCalculator die Methode setDatabaseConnection() mit der die Konfiguration des Services vorgenommen werden kann. In diesem Fall nimmt die Methode keinen skalaren Wert, sondern eine Instanz der Klasse MySQLiHandler entgegen.
Innerhalb der Methode getShipmentDate() wird die Datenbank-Verbindung dann zur Evaluierung des Lieferzeitraums genutzt und erwartet, dass die Datenbank-Verbindung zu diesem Zeitpunkt aufgebaut ist.
Um den Service DatabaseConfiguredShipmentDateCalculator nutzen können ist eine entsprechende Konfiguration notwendig. Diese definiert Service selbst und die abhängigen Strukturen (Datenbank-Verbindung über den MySQLiHandler) und dessen Konfiguration.
Die Definition des Service lautet wie folgt:
[shipment-date-calculator]
class="ACME\shop\order\DatabaseConfiguredShipmentDateCalculator"
servicetype="SINGLETON"
Zur Konfiguration der Datenbank-Verbindung - in diesem Fall ebenfalls eine Service-Definition, die später zur Initialisierung eingesetzt wird - kann die folgende Sektion verwendet werden:
[shipment-database]
class = "APF\core\database\MySQLiHandler"
servicetype = "SINGLETON"
setupmethod = "setup"
conf.host.method = "setHost"
conf.host.value = "localhost"
conf.name.method = "setDatabaseName"
conf.name.value = "..."
conf.user.method = "setUser"
conf.user.value = "root"
conf.pass.method = "setPass"
conf.pass.value = "..."
conf.charset.method = "setCharset"
conf.charset.value = "utf8"
conf.collation.method = "setCollation"
conf.collation.value = "utf8_general_ci"
Die Sektion shipment-database definiert zunächst die Service-Implementierung, die in diesem Fall eine mit dem APF mitgelieferte Komponente ist - die Klasse MySQLiHandler. Da diese das DatabaseConnection-Interface implementiert ist es möglich, eine Instanz mit Hilfe des DIServiceManager zu erzeugen.
Nach der Konfiguration mit unterschiedlichen conf.*-Sektionen wird die Instanz mit der in Kapitel 4.4.3. beschriebenen setupmethod initialisiert und ist damit für die Verwendung bereit.
Um die Datenbank-Verbindung im DatabaseConfiguredShipmentDateCalculator nutzen zu können, muss diese noch in den Service injiziert werden. Dies lässt sich mit der folgenden Erweiterung der Konfigurations-Sektion shipment-date-calculator erreichen:
[shipment-date-calculator]
class="ACME\shop\order\DatabaseConfiguredShipmentDateCalculator"
servicetype="SINGLETON"
init.db.method = "setConnection"
init.db.namespace = "ACME\shop\order"
init.db.name = "shipment-database"
Bei der Nutzung des Service shipment-date-calculator steht Ihnen in der Methode getShipmentDate() nun der Zugriff auf eine initialisierte Datenbank-Verbindung zur Verfügung.
Für das im Kapitel 4.4.3 beschriebenen Beispiel wird die Methode initialize() genutzt, um die per setter injection bzw. method injection konfigurierte SimpleShipmentDateCalculator-Instanz für den Gebrauch zu initialisieren.
In diesem Kapitel soll das constructor injection-Prinzip angewendet werden. Dies hat den Vorteil, dass gleichzeitig alle notwendigen Parameter injiziert und das Objekt in den gebrauchsfertigen versetzt werden kann. Aus Software-Design-Gesichtspunkten bietet das ebenso den Vorteil, dass Abhängigkeiten direkt über den Konstruktor ausgedrückt werden können.
Der SimpleShipmentDateCalculator definiert in Kapitel 4.4.3 drei Abhängigkeiten: $shipmentPeriodInDays, $start und $end. Aus diesem wird der dynamische Faktor $dynamicFactor errechnet.
Um alle Abhängigkeiten im Konstruktor auflösen zu können ist die Implementierung wie folgt zu ändern:
namespace ACME\shop\order;
class SimpleShipmentDateCalculator implements PreliminaryShipmentDateCalculator {
/**
* @var int
*/
private $shipmentPeriodInDays = 10;
/**
* @var \DateTime
*/
private $start;
/**
* @var \DateTime
*/
private $end;
/**
* @var int
*/
private $dynamicFactor;
public function __construct($shipmentPeriodInDays, $start, $end) {
$this->shipmentPeriodInDays = $shipmentPeriodInDays;
$this->start = new \DateTime($start);
$this->end = new \DateTime($end);
$difference = $this->end->diff($this->start)->h;
$this->dynamicFactor = $difference > 1 ? $difference : 1;
}
...
}
Damit lässt sich SimpleShipmentDateCalculator durch Übergabe der geforderten Informationen gleichzeitig erzeugen und initialisieren.
Die Implementierung des SimpleShipmentDateCalculator aus Kapitel 4.4.3 wurde im vorangegangenen Abschnitt so angepasst, dass dieser alle Konfigurations-Parameter als Konstruktor-Argumente erwartet.
Um die Service-Implementierung nutzen zu können, ist folgende Konfiguration erforderlich:
[shipment-date-calculator]
class="ACME\shop\order\SimpleShipmentDateCalculator"
servicetype="SINGLETON"
construct.shipment-days.value="7"
construct.from.value="18:00:00"
construct.to.value="23:59:59"
Nach dem Aufruf von
/* @var $service SimpleShipmentDateCalculator */
$service = $this->getDIServiceObject('ACME\shop\order', 'shipment-date-calculator');
ist der Service bereits vollständig konfiguriert und initialisiert.
Die Klasse APFObject implementiert das APFDIService-Interface und stellt damit alle notwendigen Voraussetzungen zur Verfügung um Objekte dieses Typs mit dem DIServiceManager zu erzeugen. Ist dies nicht gewünscht - etwa um die von der Klasse APFObject ausgehenden Abhängigkeiten zu lösen -, so können Sie jederzeit die Abhängigkeit durch eine eigene Implementierung des APFDIService-Interface unterbrechen.
Für Ihre eigene Implementierung des APFDIService-Interface gilt es folgende Punkte zu beachten:
Die Nutzung von Services, die mit dem DIServiceManager erzeugt wurden kann auf unterschiedliche Arten erfolgen. Einige davon wurden bereits in den letzten Kapitel besprochen. Die folgende Tabelle fasst alle Möglichkeiten zusammen und gibt Ihnen weitere Hinweise und Tipps:
Beschreibung | Einsatzgebiet |
Die in den Kapiteln 4.4.1 bis 4.4.4 beschriebene Art der Nutzung sieht den direkten Bezug der gewünschten Instanzen vom DIServiceManager vor. Ihnen steht es dabei frei, die Convenience-Methode APFObject::getDIServiceObject() zu nutzen oder direkt auf DIServiceManager::getServiceObject() zuzugreifen. | Diese Art der Anwendung wird für (Document-)Controller und die Implementierung von Tags empfohlen. |
Kapitel 4.4.4 beschreibt die Initialisierung und Konfiguration von Services mit einfachen Werten und wiederum anderen Services. Der z.B. von einem Controller angefragte Service ist bei Bezug bereits fertig konfiguriert und der Anwender muss keine weiteren Code investieren (inversion of control). Innerhalb eines Services kann ein über den DI-Container injizierter Service direkt genutzt werden ohne diesen aktiv vom DIServiceManager zu beziehen. Dies erleichtert die Implementierung und entfernt explizite Abhängigkeiten um Transparenz und Testbarkeit zu verbessern. Diese Möglichkeit steht auch für Document-Controller zur Verfügung. Kapitel 4 der (Document-)Controller-Dokumentation beschreibt die notwendigen Maßnahmen um einen Controller über den DIServiceManager zu erzeugen. |
Diese Art der Anwendung wird für (Document-)Controller empfohlen, die komplexe Services bezieht bzw. auf mehr als einen Service zurückgreift. Ferner ist diese Vorgehensweise nützlich um Code innerhalb von Controllern besser testbar zu gestalten in dem explizite Abhängigkeiten entfernt werden. |
Analog zur Erzeugung von Document-Controllern ist es ebenfalls möglich, Front-Controller-Actions über den DI-Container zu erzeugen. Dies erleichtert ebenfalls die Implementierung und entfernt explizite Abhängigkeiten im Code um Transparenz und Testbarkeit zu verbessern. Kapitel 3.3 der Front-Controller-Dokumentation beschreibt die notwendigen Maßnahmen um eine Action über den DIServiceManager zu erzeugen. |
Diese Art der Anwendung wird für Front-Controller-Actions empfohlen, die komplexe Services bezieht bzw. auf mehr als einen Service zurückgreift. Ferner ist diese Vorgehensweise nützlich um Code innerhalb von Actions besser testbar zu gestalten. |
Das Adventure PHP Framework stellt mehrere Möglichkeiten zur Erzeugung von Objekten zur Verfügung. Mit diesen lassen sich Instanzen mit unterschiedlichen Gültigkeitsbereichen erstellen und innerhalb der Applikation verwenden. Erstellen Sie Services mit dem ServiceManager oder DIServiceManager stehen Ihnen diese Möglichkeiten ebenfalls zur Verfügung. Bei der Nutzung der Methode APFObject::getServiceObject() in Kapitel 3.2.2 und der Konfiguration von Services in Kapitel 4.3 lassen sich die Gültigkeitsbereiche jeweils programmatisch bzw. konfigurativ pro Instanz definieren.
Die folgende Tabelle führt die vorhandenen Gültigkeitsbereiche und deren empfohlenen Einsatz auf:
Gültigkeitsbereich | Beschreibung | Einsatzgebiete |
NORMAL | Objekt wird bei jeder Anfrage an den ServiceManager bzw. DIServiceManager neu erzeugt und ggf. initiaisiert. | Soll ein neu erzeugtes Objekt mit Kontext und Sprache ausgestattet werden, jedoch bei jedem Anwendungsfall unterschiedlich sein, kann dieser Gültigkeitsbereich genutzt werden. Für Services wird i.d.R. jedoch der Gültigkeitsbereich SINGLETON empfohlen. |
SINGLETON | Objekt wird lediglich bei der ersten Anfrage an den ServiceManager bzw. DIServiceManager neu erzeugt und ggf. initialisiert und ist innerhalb einer HTTP-Anfrage gültig. |
Objekte mit diesem Gültigkeitsbereich können zum Austausch von Informationen zwischen unterschiedlichen
HMVC-Elementen innerhalb einer Anfrage verwendet werden.
Ein weiterer Anwendungsfall ist die mehrfache Verwendung eines Service an unterschiedlichen Stellen innerhalb einer HTTP-Anfrage, dessen Initialisierung aufwendig ist und daher nur einmal durchgeführt werden soll. |
SESSION_SINGLETON | Objekt wird lediglich bei der ersten Anfrage an den ServiceManager bzw. DIServiceManager neu erzeugt und ggf. initialisiert und ist innerhalb einer HTTP-Session gültig. |
Objekte mit diesem Gültigkeitsbereich können als View-Model für z.B. mehrseitige Workflows genutzt werden.
Darin lassen sich über mehrere HTTP-Anfragen innerhalb eines Besuchs die Daten zwischenspeichern und am
Ende konsolidiert verarbeiten.
Ein weiterer Anwendungsfall ist die mehrfache Verwendung eines Service an unterschiedlichen Stellen innerhalb eines Besuchs, dessen Initialisierung aufwendig ist und daher nur einmal durchgeführt werden soll. |
APPLICATION_SINGLETON | Objekt wird lediglich bei der ersten Anfrage an den ServiceManager bzw. DIServiceManager neu erzeugt und ggf. initialisiert und ist für die Laufzeit des Web-Servers gültig. |
Objekte mit diesem Gültigkeitsbereich können zum Austausch von Daten einer Anwendung unabhängig von einer
Anfrage oder einem Besuch genutzt werden.
Ein weiterer Anwendungsfall ist die mehrfache Verwendung eines Service an unterschiedlichen Stellen innerhalb einer Applikation, dessen Initialisierung aufwendig ist und daher nur einmal durchgeführt werden soll. |
Bitte beachten Sie, dass bei Nutzung des Gültigkeitsbereichs SESSION_SINGLETON und APPLICATION_SINGLETON die Inhalte eines Objekts zwischen zwei Anfragen serialisiert werden. Da Resourcen (File-Pointer, Datenbank-Verbindungen) nicht searialisiert werden können müssen diese ggf. in der nächsten Anfrage neu initialisiert werden müssen.
Um in einer Klasse DataMapper, die als Service über den ServiceManager oder DIServiceManager bezogen wird, eine Datenbank-Verbindung zu verwalten können Sie folgenden Code nutzen:
class DataMapper extends APFObject {
/**
* @var MySQLiHandler
*/
private $connection;
...
public function __wakeup() {
$this->connection = ...;
}
}
Sobald Sie den Service in einer weiteren Anfrage erneut beziehen, wird die Methode __wakeup() aufgerufen und die Verbindung wieder hergestellt.
Nutzen Sie den DIServiceManager und ist für Ihren Service eine setupmethod konfiguriert, können Sie bei der Serialisierung den Initialisierugstatus des Objekts zurück zu setzen. Dies sorgt ebenfalls dafür, dass das Objekt beim nächsten Bezug über die setupmethod neu initialisiert wird. Hierzu lässt sich folgender Code nutzen:
class DataMapper extends APFObject {
/**
* @var MySQLiHandler
*/
private $connection;
...
public function __sleep() {
$this->markAsPending();
}
public function initialize() {
$this->connection = ...;
}
}
Diese Möglichkeit ist jedoch an den DIServiceManager gebunden und setzt voraus, dass Sie Ihren Service mit einer setupmethod initialisieren.
Um unsere Webseite für Sie optimal zu gestalten und fortlaufend verbessern zu können, verwenden wir Cookies. Durch die Nutzung der Webseite stimmen Sie der Verwendung von Cookies zu. Weitere Informationen finden Sie in den Datenschutzrichtlinien.