Services

1. Introduction

Encapsulation of certain functionality into separate components is a well-known pattern in object oriented development. The advantages are described as Multi-tier Architecture Pattern. This means that each layer takes responsibility of dedicated parts of the application.

Typically, layers or services are represented by one ore more classes what drives the need to create those classes. Moreover, it's your challenge to design and create reusable layers - services - that expose an easy-to-use and self-explantory API. An application's data layer for instance requires a connection to an external data source whereas a business component has to know about the environment the application is used. In addition, layers define dependencies amongst each other.

In order to ensure proper encapsulation and thus exchangeability of layers within an application an upper layer should not know about the internal structure and functionality of low layer components. To achieve this creation methods and configuration should be separated from the calling layer.

The following chapters explain mechanisms and tools for Creation of objects that allow encapsulation of functionality and creating clear structures within your application.

2. Creation of objects

The Adventure PHP Framework uses different paradigms to create objects within the Front controller and Page controller. Especially Front-Controller-Actions and Tags containing UI functionality have to be provided with their runtime environment. This is about injecting the parent component and applying the current context and language in order to access Configuration depending on those values. Therefor, the APF uses both Factory pattern and Dependency Injection Pattern.

Creating objects APF components hand on
  • Context and
  • Language
These are essential information that are applied to each and every object to for instance load Configuration items or realize language-dependent functionality (e.g. Standard taglibs).

The framework lays it's clear focus on creating UI along with mechanisms and functionality required for that. This means, that you as a developer are responsible for creating objects outside of the UI area. For this reason, the APF contains two tools to support this at best: ServiceManager and the DIServiceManager.

Please ensure that all objects having access to configurations or create further objects that make use of the current context or language of your application are created by either ServiceManager or DIServiceManager. Otherwise, there will be errors loading configurations or create context- or language-dependent objects.

Creating and - at the same time - initializing any kind of classes as Singleton, SessionSingleton, or ApplicationSingleton the APF supports two well-known concepts: constructor injection and method injection or setter injection respectively.

Using constructor injection dependencies are injected into the constructor of the class using method injection or setter injection all required data and dependencies are injected after creation.

Chapter 4.3.1 depicts mechanisms to do further initialization using the method injection or setter injection (see: setupmethod).

3. ServiceManager

The ServiceManager is an extension to the existing Singleton-, SessionSingleton-, and ApplicationSingleton implementations described under Creation of objects. Compared to these implementations, the ServiceManager is better integrated into the framework and APFObject offers a convenience method APFObject::getServiceObject() that can be used to easily create objects.

The current context and language are automatically applied to the created instance by the ServiceManager and is thus returned fully initialized.

Please note, that context and language are not available during construction of the object by the ServiceManager and thus not within the constructor of the created service. The reason behind this fact is that the ServiceManager uses setter injection to initialize objects (see interface definition APFService in chapter 3.1). In case you intend to load configurations or create further services within the present service, kindly do so in a separate method called after the constructor.

3.1. Service definition

To create objects with the ServiceManager the class definition must comply with the APFService interface. This interface allows to inject context and language and is used to mark services classes.

The interface is as follows:

PHP code
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(); }

The constants listed above define the list of possible creation patterns of services. The subsequently listed methods allow injection of context and language.

In case the object to create does not implement the APFService interface and exception is thrown.

3.2. Creation of services

Within your application you can use the ServiceManager either directly (see 3.2.1. Native usage) or via APFObject::getServiceObject() (she 3.2.2. Using the wrapper). The following chapters discuss the pros and cons.

3.2.1. Native usage

The ServiceManager can be used allover the code invoking it's static method getServiceObject(). Here is an example:

PHP code
use APF\core\service\ServiceManager; $instance = &ServiceManager::getServiceObject('VENDOR\..\Class', $context, $language);

As you can take from the code snippet you need to know about the context and the current language retrieving an object instance. In general you always have these pieces of information present within an APF-created object since the framework takes care to distribute all relevant information.

In case you create objects on your own or you are out of the area of validity of an object - e.g. within your index.php - you may want to use the Front controller instance as source. Usually, the instance is created within your bootstrap file using setting the current context and language. The relevant code is as follows:

PHP code
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);
Please note, that within the DOM tree of the Page controller context and language can be re-defined for certain child structures within the tree. For this reason and to avoid issues with propagation of context and language it is recommended to use the wrapper method (see chapter 3.2.2).

Besides the mandatory arguments, method getServiceObject() has some more parameters to control creation and initialization of objects. Please take the details on parameters $arguments, $type, and $instanceId from chapter 3.2.2.

3.2.2. Using the wrapper

APFObject contains the getServiceObject() method that encapsulates a ServiceManager call. It also takes care of applying the correct context and language. You can create objects as follows using this approach:

PHP code
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(); } }

APFObject::getServiceObject() offers the following parameters:

  • $serviceClass: Defines the fully-qualified name of the service implementation (e.g. VENDOR\..\MyServiceName).
  • $arguments: Using the optional argument $arguments you can specify constructor arguments that are applied to the instance to be created. From a software design point of view this has an important benefit as dependencies can directly be expressed as constructor arguments. Please note that the order of parameters applied to the getServiceObject() method must match the order of parameters specified in the constructor of the class to be created! Details can be taken from Creation of objects.
  • $type: The $type parameter defines the object creation flavor and thus the area of validity of the instance. Available values:
    • APFService::SERVICE_TYPE_NORMAL
    • APFService::SERVICE_TYPE_SINGLETON
    • APFService::SERVICE_TYPE_SESSION_SINGLETON
    • APFService::SERVICE_TYPE_APPLICATION_SINGLETON
    This parameter is optional. Default value is APFService::SERVICE_TYPE_SINGLETON. Details on the listed areas of validity can be taken from Creation of objects.
  • $instanceId: All implementations described in chapter Creation of objects have the ability to define unique identifiers for object instances. This can be used to create several instances of the same implementation e.g. for several database connections based on the same implementation. This feature can also be used with the ServiceManager.
To ease implementation of services you may want to derive your class from APFObject instead of implementing the APFService interface. APFObject already includes all relevant basics to start from.

4. DIServiceManager

The DIServiceManager is a Dependency Injection and Inversion of Control container creating and configuring services (see Inversion of Control Containers and the Dependency Injection pattern by Martin Fowler). The definition of Services is based on configuration files (paradigm: wire by configuration), that both define the service implementation as well as dependencies and configuration parameters.

Creating the service instanced the DIServiceManager uses the capabilities of the ServiceManager and thus offers applicable areas of validity for all use cases (details can be taken from Creation of objects).

Compared to the ServiceManager the dependency injection container offers another abstraction layer creating and configuring services. Using the container you do not directly refer to an implementation but a configuration. This eases exchanging implementations or use MOCk implementations on-demand.

Using the container you can either use the static method DIServiceManager::getServiceObject() or the convenience method APFObject::getDIServiceObject().

Using APFObject::getDIServiceObject() the current context and language are automatically applied to the DIServiceManager and thus the created object will be fully initialized.

Services can be prepared for usage by both other services or static configuration parameters.

4.1. Service definition

In order to create objects with the DIServiceManager your implementation must comply with the APFDIService interface. This interfaces is based on APFService and provides the necessary structure to create and manage instances with the dependency injection container.

The interface is as follows:

PHP code
interface APFDIService extends APFService { public function markAsInitialized(); public function markAsPending(); public function isInitialized(); }

All methods listed above allow the container to query the status of the instance - e.g. the initialization state.

Due to the fact that the object creation is delegated to the ServiceManager it is required that your implementation is t least an APFService. Otherwise, an exception is thrown.

4.2. Creation of services

Within your application you can use the DIServiceManager either directly (see 4.2.1. Native usage) or via APFObject::getDIServiceObject() (see 4.2.2. Usage of the wrapper). The following chapters describe the pros and cons.

4.2.1. Native usage

The DIServiceManager can be used allover the code invoking it's static method getServiceObject(). Here is an example:

PHP code
use APF\core\service\DIServiceManager; $instance = &DIServiceManager::getServiceObject('VENDOR\..', 'Service-Name', $context, $language);

The first two parameters define the namespace and the name of the service definition. This effectively refers a configuration section that defines the services (details see chapter 4.3. Calling this method the current context and language must be present. In general you always have these pieces of information present within an APF-created object since the framework takes care to distribute all relevant information.

In case you create objects on your own or you are out of the area of validity of an object - e.g. within your index.php - you may want to use the Front controller instance as source. Usually, the instance is created within your bootstrap file using setting the current context and language. The relevant code is as follows:

PHP code
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);
Please note, that within the DOM tree of the Page controller context and language can be re-defined for certain child structures within the tree. For this reason and to avoid issues with propagation of context and language it is recommended to use the wrapper method (see chapter 4.2.2).
4.2.2. Usage of the wrapper

APFObject offers the getDIServiceObject() method that encapsulates the DIServiceManager. It takes care to distribute the current context and language. You can create objects as follows:

PHP code
use APF\core\pagecontroller\APFObject; class GodObject extends APFObject { public function doSomething(){ $service = &$this->getDIServiceObject( $serviceNamespace $serviceName ); $service->doSomethingElse(); } }

APFObject::getDIServiceObject() has the following parameters defined:

  • $serviceNamespace: Defines the namespace of the service configuration (e.g. VENDOR\namespace\of\my\component).
  • $serviceName: Defines the reference name of the service configuration within the previously defined namespace (e.g. open-weather-map-service).
To ease implementation of services you may want to derive your class from APFObject instead of implementing the APFService interface. APFObject already includes all relevant basics to start from.

4.3. Configuration

Creation and configuration of services with the DIServiceManager significantly differs from using the ServiceManager. Each service is described by a unique configuration. This is because one service is not only representing itself but can also be used to initialize other services. This allows you to define dependencies between different service with the DIServiceManager.

Definition of a single service is done using a configuration section that is addressed by the namespace of the config file and the name of the section itself. The DIServiceManager uses the configuration mechanism of the APF to load Configuration files.

Using the ConfigurationManager offers several possibilities - e.g. define services depending on namespace, context, and environment. This means:

  • Services can be grouped into logical areas using namespaces. This eases naming and separation within your application.
  • Services can be defined or configured according to their usage scenario (context). This for instance allows you to used two different services for weather forecasts within one application based on the same implementation.
  • Using the environment services can be tailored to different physical platforms (e.g. development, staging production). The same implementation can e.g. be used within a development environment where the service runs in debug mode or on production servers where only dedicated information is written to log files.

The subsequent chapters describe the details on configuration of services.

4.3.1. Configuration scheme

The definition of services is described by a configuration section within a configuration file with name {ENVIRONMENT}_serviceobjects.ini as mentioned above. The value for environment must be replaced by the value that your application defines (default: DEFAULT).

Within this kind of file each section defines a unique and independently usable service definition. The content of one section is defined by the following scheme:

APF configuration
[{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 = ""]
Using construct.* or init.* sections you are able to use complex data structures to initialize services. This means passing service instances as initialization parameter to the instance to be created (dependency injection) and thus configure it for the designed use case (e.g. inject database connection to be used).

The meaning of the listed components are described below:

  • service-name: The service-name forms the unique identifier of a service in conjunction with the namespace of the configuration file. It is used for requesting a service through APFObject::getDIServiceObject() or DIServiceManager::getServiceObject() as well as for initializing services with other services.
  • class: The fully-qualified class name of the service implementation (e.g. VENDOR\..\Class). The content of this parameter is identical to the first parameter of an ServiceManager::getServiceObject() call.
  • servicetype: This directive defines the way of creating the service. Please take the available modes from chapter 3.2.2. In addition, you may want to use APFService::SERVICE_TYPE_CACHED or CACHED. This value means, that the service implementation is created again with each request to the container (see APFService::SERVICE_TYPE_NORMAL) but the configuration file is loaded only once.
  • construct.*: The construct.* area can be used to configure explicit dependencies that the service to be created is expecting as constructor arguments. Similar to the conf.* and init.*areas there are two options: initialisation with simple values and other service instances.
    Please note that the order of parameters applied to the getServiceObject() method must match the order of parameters specified in the constructor of the class to be created! Otherwise, unexpected issues may come up.
    A service can be initialized with any number of values and other services.
    Please note, that the {CONSTRUCT_KEY} placeholder must be unique for each static (attribute value) or dynamic parameter (combination of one namespace and name attribute each). It defines the key of the dependency definition or a group respectively.
    In case you want to configure a service with static values please use the construct.{CONSTRUCT_KEY}.value notation (similar to conf.* sections) and assign the desired value.
    Configuration or initialization with other services can be done using the construct.{CONSTRUCT_KEY}.namespace and construct.{CONSTRUCT_KEY}.name attributes referring to the desired service.
  • conf.*: The conf area is intended to initialize the service with static configuration (method injection or setter injection).
    This kind of initialization only allows to pass primitive data types. Thus, it is recommended for values like user names, passwords, and URLs etc. (also called simple configuration).
    Each service may define any number or attributes. This can be done by using multiple conf blocks choosing an appropriate {CONF_KEY} place holder.
    Please note that the {CONF_KEY} place holder must be the same within one group (for attributes method and value) but different between various groups (combination of one method and one value attribute). The following code block gives you an example for initializing a service with a user, a password, and a URL:
    APF configuration
    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"
    Initializing the user key user has been defined. The value of the conf.user.method attribute refers to the method that is called by the DIServiceManager on the service implementation defined with the class attribute to apply the value of the conf.user.value attribute. Same applies to other sections
  • init.*: The init area is intended to initialize the service with dynamic or complex configuration using other services (method injection or setter injection).
    Each service dan be configured using any number of further services. This can be done by using multiple init blocks choosing an appropriate {INIT_KEY} place holder.
    Please note that the {INIT_KEY} place holder must be the same within one group (for attributes method, namespace, and name) but different between various groups (combination of one method-, one value, and one name attribute). The following code block gives you an example for initializing a service the database connection for a weather forecast service:
    APF configuration
    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"
    Initializing the weather forecast service key weather has been defined. The value of the init.weather.method attribute refers to the method of the implementation defined with the class attribute that the DIServiceManager calls to inject the service that is described by attributes init.weather.namespace and init.weather.name. Same applies to other sections.
  • setupmethod: Optional parameter setupmethod allows to call a method at the end of the configuration of an object using the dependencies that are defined with the conf.* and init.* areas.
    setupmethod can be used to further initialize the object instance using the injected information. This is especially helpful for initialization of a service that needs all dependencies resolved previously (e.g. name of the database connection).
    In order to avoid multiple initialization calls your service may return true for method isInitialized() defined within the APFDIService interface as soon as the inituialization is done. This piece of information is used by the DIServiceManager to skip the setupmethod call in case the service has already been initialized.

The APF Configuration component allows you to use any kind of format for the definition of services as access to configuration resources has been abstracted and standardized. This provides the advantage that service definitions can be stored as PHP files instead of INI files.

In case you want to configure the GuestbookMapper service based on the above described schema the following configuration file is required:

PHP code
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' ] ] ] ];

Using the configuration directive DIServiceManager::$configurationExtension you can define which configuration scheme will be used:

PHP code
DIServiceManager::$configurationExtension = 'php';
Please note, that with changing the configuration scheme all configurations are expected in the selected format - in this case PHP files. At the moment, the DIServiceManager does not support the option to select the format per configuration or service.
4.3.2. Service definition

One single service is defined by a configuration section described in chapter 4.3.1 and references to other sections in case of dependencies. The sections are defined within a configuration file can be addressed by their namespace and the name of the file - {ENVIRONMENT}_serviceobjects.ini.

The name of the configuration file is fixed and cannot be changed. This allows you to omit the file name while requesting services and thus eases the usage of the DIServiceManager. Within one file, you can define several services as long as the name of the service - which in turn is the name of the service - is unique.

In case you try to query a service by

PHP code
use APF\core\service\DIServiceManager; $service = DIServiceManager::getServiceObject( 'VENDOR\namespace\of\service\definition', 'open-weather-map-service', $context, $language );

the DIServiceManager expects a service definition - which in turn is represented by a configuration section - named open-weather-map-service located within

Code
/path/to/VENDOR/config/namespace/of/service/definition/{CONTEXT}/{ENVIRONMENT}_serviceobjects.ini

The parameter /path/to/VENDOR - which is the base path of the application and configuration files for vendor VENDOR - as well as {CONTEXT} and {ENVIRONMENT} directly depend on the configuration of your application. Details on the usage of configuration files and their schema definition can be taken from Configuration and Class loading.

Using namespace of configuration files and service names addressing services contained there developers are free to define and structure service definitions. You can either define dependent services within the same configuration file as well as on other ones.

This opportunity may be used to separate basic services or services that are used multiple times (e.g. database connections) and place them into a basic namespace whereas dedicated services are located within a deeper section of the namespace tree.

4.4. Usage

The subsequent features describe different use cases and the appropriate service implementation and configuration.

4.4.1. Creation of a simple service

The first use case is about creating a simple service that returns a shipment date based on the order date and time. In order to display a possible shipment date the service should be used within a (Document-)Controller.

4.4.1.1. Implementation

At first we are defining the structure of the service. The interface is as follows:

PHP code
namespace ACME\shop\order; interface PreliminaryShipmentDateCalculator { /** * @param \DateTime $orderDate * @return \DateTime */ public function getShipmentDate(\DateTime $orderDate); }

The implementation should now calculate a shipment date based on the input:

PHP code
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')); } }
Please note, that SimpleShipmentDateCalculator extends APFObject. This means that it automatically complies with the APFDIService and it's requirements. Doing so, you can easily implement services without caring about the internals. In case you want to remove dependency to APFObject please follow the instructions noted in chapter 4.4.5..

In order to create a service within a document controller it is required to create the service configuration.

Defining the namespace of the service definition it is recommended to use the same namespace of the implementation to express affiliation.
4.4.1.2. Usage

According to the hint within the last chapter the namespace of the service configuration should be defined as ACME\shop\order and the name of the service definition is shipment-date-calculator. The service can thus be used within a controller like this:

PHP code
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') ); } }
4.4.1.3. Configuration

Configuration - or the configuration file - depends on several parameters. For this use case we assume the following facts:

  • For vendor ACME a StandardClassLoader is registered according to the description under Class loading which defines /path/to/ACME as base path.
  • Context of the application passed to the Front controller is customer-one.
  • Environment has been left as is for the current application and is thus DEFAULT.

Assuming the above points the DIServiceManager expects the configuration file

Code
/path/to/ACME/config/shop/order/customer-one/DEFAULT_serviceobjects.ini

to be filled withe

APF configuration
[shipment-date-calculator] class="ACME\shop\order\SimpleShipmentDateCalculator" servicetype="SINGLETON"
4.4.2. Initialization of a simple service

In chapter 4.4.1 SimpleShipmentDateCalculator has been configured statically. This means, the average shipment days have been configured within the code. Withing this chapter, the definition of the interface and the implementation should be improved to take a configurable amount of days as configuration parameter.

4.4.2.1. Implementation

Interface PreliminaryShipmentDateCalculator now contains an additional method setShipmentPeriod() to allow the service to be configured:

PHP code
namespace ACME\shop\order; interface PreliminaryShipmentDateCalculator { /** * @param int $shipmentPeriodInDays */ public function setShipmentPeriodInDays($shipmentPeriodInDays); /** * @param \DateTime $orderDate * @return \DateTime */ public function getShipmentDate(\DateTime $orderDate); }

The implementation is thus extended as follows:

PHP code
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')); } }
4.4.2.2. Configuration

Based on the assumptions in chapter 4.4.1.3 the configuration of the shipment-date-calculator section can be enhanced as follows:

APF configuration
[shipment-date-calculator] class="ACME\shop\order\SimpleShipmentDateCalculator" servicetype="SINGLETON" conf.shipment-days.method="setShipmentPeriodInDays" conf.shipment-days.value="7"

Using the service the delivery date is 7 days compared to the configuration in chapter 4.4.1.2.

With this change a clear separation of code and configuration has been introduced. This allows to define different shipment days for different applications and different environments without changing the code.

In case a configuration method expects multiple arguments the configuration may define multiple conf.{CONF_KEY}.value elements. Initialization of service method

PHP code
public function setShipmentPeriod($days, $hours, $minutes);

can be achieved with the following configuration:

APF configuration
conf.shipment-period.method="setShipmentPeriod" conf.shipment-period.value.1="7" conf.shipment-period.value.2="2" conf.shipment-period.value.3="10"

Numbers 1 to 3 can be replaced by and numeric or alphanumeric key. The following example uses alphanumeric identifiers to make the configuration more self-explanatory:

APF configuration
conf.shipment-period.method="setShipmentPeriod" conf.shipment-period.value.days="7" conf.shipment-period.value.hours="2" conf.shipment-period.value.minutes="10"
Please note, that the order of the parameter definition within the configuration file must match the method signature!
4.4.3. Using the initialization method

Construction or configuration of objects is challenging in case internal states or resources (e.g. database connections) are dependent on one or more configuration parameters. In such cases it is necessary to first resolve all dependencies or resources and after that create "operating state" of the respective instance.

One possible solution is to use the setter methods to inject configuration parameters and to keep order for all object usages while utilizing the last one to create the operating state. This approach is dangerous since with inappropriate use or using the instance across the session the integrity of the internal object state cannot be guaranteed.

In order to avoid issues with initialization the DIServiceManager offers execution of an initialization method. It can be defined within the service configuration using the setupmethod attribute.

The DIServiceManager calls the specified method at the end of the configuration process - meaning after injecting all dependencies defined within within the respective conf.* and init.* sections. This ensures that all necessary information is available for finally initializing the instance.
4.4.3.1. Implementation

Within the following example SimpleShipmentDateCalculator from chapter 4.4.2 will be extended to calculate the potential shipment date basic value and a time-dependent factor. The factor is calculated by two different configuration parameters and is only valid within the time slot defined by the two parameters.

setupmethod is used to execute calculation of the factor that getShipmentDate() can rely on an existing factor rather than initialize it on-demand calculating the shipment date. For this reason let's first extend the PreliminaryShipmentDateCalculator interface to set the start and end time:

PHP code
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); }

The implementation of the services can then be extended as follows:

PHP code
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); } ... }

Calculation of the $dynamicFactor should now be done within the initialize() method. The implementation of SimpleShipmentDateCalculator is thus extended one more time:

PHP code
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' ) ); } }
initialize() has not been added to the interface since initialization is considered an implementation specific of the current service. In case initialization as described above is something that is essential for the service for every implementation and you intend to create the instance using the DIServiceManager under all circumstances you may add initialize() the the interface as well.

The instance of SimpleShipmentDateCalculator is not explicitly marked as initialized within initialize(). This leads to the fact that the DIServiceManager calls initialize() with any new request of this object again.

In case the state of the object after initialization is valid for a longer period of time (e.g. for the entire lifetime of the object) you can mark it as initialized. The DIServiceManager then does not call setupmethod again. This is especially recommended for expensive initialization code.

To do so implementation of initialize() has to be changed as follows:

PHP code
class SimpleShipmentDateCalculator implements PreliminaryShipmentDateCalculator { ... public function initialize() { $difference = $this->end->diff($this->start)->h; $this->dynamicFactor = $difference > 1 ? $difference : 1; $this->markAsInitialized(); } ... }

Now, all preparations are done to initialize the service. The next chapter describes how to configure the service for it's usage.

4.4.3.2. Configuration

Within the previous chapter SimpleShipmentDateCalculator has been adapted to take all necessary configuration parameters to allows dynamic initialization.

To use the enhanced service implementation the configuration has to be adapted as follows:

APF configuration
[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"

Using the shipment-date-calculator service a dynamic factor is added to the delivery date between 6pm and 0pm.

4.4.4. Initialisation of complex services

This chapter is about the initialization of services using other services. You may use this approach in case simple data types used within conf.* sections are no longer sufficient or one service requires another service to for it's work (e.g. database connection).

Initializing services can be done with init.* sections. There you can apply a service instance to a dedicated method of the service to initialize. Within this chapter we will create the DatabaseConfiguredShipmentDateCalculator that evaluates the shipment dates using a database table.

4.4.4.1. Implementation

Implementation of the DatabaseConfiguredShipmentDateCalculator is based on the interface definition PreliminaryShipmentDateCalculator described in chapter 4.4.1.1 that requires the getShipmentDate() method to be implemented.

Connecting to the database the ConnectionManager or a dedicated driver implementation respectively is used - in this case the MySQLiHandler. Implementation is as follows:

PHP code
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']; } }

Similar to chapter 4.4.3 the DatabaseConfiguredShipmentDateCalculator defines setDatabaseConnection() to allow configuration of the service. In this case using an instance of MySQLiHandler instead of any scalar value.

Within getShipmentDate() the database connection is used to evaluate the shipment date and the method relies on the database connection being present.

4.4.4.2. Configuration

To use service DatabaseConfiguredShipmentDateCalculator a respective configuration is required that defines the service and it's dependent components (database connection via MySQLiHandler) and their configuration.

The definition is as follows:

APF configuration
[shipment-date-calculator] class="ACME\shop\order\DatabaseConfiguredShipmentDateCalculator" servicetype="SINGLETON"

Configuration of the database connection - another service definition that is used for initialization purposes later one - can be done with the following section:

APF configuration
[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"

Section shipment-database first defines the service implementation that is an APF component in this case - class MySQLiHandler. Since it implements the DatabaseConnection interface you can create instances using the DIServiceManager.

After configuration using various conf.* sections the instance is finally initialized using the setupmethod described in chapter 4.4.3. and is then ready for usage.

To use this database connection within our DatabaseConfiguredShipmentDateCalculator implementation it has to be injected into the service. You can do so by extending the shipment-date-calculator configuration section as follows:

APF configuration
[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"
The above configuration assumes that both services are defined within one single configuration file. In case your application behaves different, please adapt init.db.namespace to point to the desired namespace.

Using the shipment-date-calculator service getShipmentDate() can directly access the database connection that is ready to use.

Another example can be taken from wiki page Erzeugen des GORM mit dem DIServiceManager (German).
4.4.5. Complex initialisation via constructor

For the example described in chapter 4.4.3 method initialize() is used to initialize the instance that has been configured with setter injection or method injection respectively.

In this chapter the constructor injection concept will be used. The advantage of this approach is that all necessary parameters are injected and the instance is initialized at the same time. From a software design point of view this has also an important benefit as dependencies can directly be expressed as constructor arguments.

Please note, that injection of dependencies into the constructor has many advantages but is not applicable for all use cases. In case you intend to load context and/or language dependent configurations this is not possible within the constructor as the values for context and language are not yet injected at this point in time.
4.4.5.1. Implementation

SimpleShipmentDateCalculator implemented in chapter 4.4.3 defines three dependencies: $shipmentPeriodInDays, $start und $end. These are used to calculate the dynamic factor $dynamicFactor.

In order to resolve all dependencies within the constructor the implementation has to be changed as follows:

PHP code
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; } ... }

With this approach the SimpleShipmentDateCalculator can be created and initialized at the same time providing all necessary information to the constructor.

4.4.5.2. Configuration

SimpleShipmentDateCalculator implemented in chapter 4.4.3 has been adapted in the last section to take all necessary parameters within the constructor.

In order to use the service implementation the following configuration is required:

APF configuration
[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"

After calling

PHP code
/* @var $service SimpleShipmentDateCalculator */ $service = & $this->getDIServiceObject('ACME\shop\order', 'shipment-date-calculator');

the service is already configured and initialized.

4.4.6. Implementation of APFDIService

APFObject already implements the APFDIService interface and provides all basic conditions to create objects with the DIServiceManager. In case you intend to resolve this dependency you can implement the interface directly within your service at any time.

For your custom implementation of the APFDIService interface please keep the following in mind:

  • Managing context, language and service type is part of your implementation now. This means that the respective values are stored and returned as per interface definition (e.g. getContext()).
  • Initialization and marking the service' initialization state is done using markAsInitialized() as well as markAsPending(). The current state is requested by the DIServiceManager invoking isInitialized(). Storing and returning the initialization state is to be handled by your implementation.
Implementing custom services initialized by setupmethod marking services as initialized can also be done within the isInitialized() method. Using APFObject as your basis for implementation flagging the current instance as initialized has to happen during initialization with the setupmethod calling markAsInitialized().
4.4.7. Usage of services

Consuming services created with the DIServiceManager has many flavours. Some of them have already been discussed within previous chapters. The following table summarizes all possibilities and includes further hints and tips:

Descriptions Areas of usage
Chapters 4.4.1 up to 4.4.4 describe using the DIServiceManager to obtain an instance. You may either use convenience method APFObject::getDIServiceObject() or DIServiceManager::getServiceObject() directly. This kind of usage is recommended for (Document-)Controller and Implementation of tags.

Chapter 4.4.4 outlines initialisation and configuration of services with multiple values and other services. The service requested by e.g. a document controller is already finally configured after retrieval and the user does not have to investigate further (inversion of control).

Within a service you can directly use another service that has been injected by the DI container without approaching the DIServiceManager. This eases implementation and removes explicit dependencies and thus improves transparency and testability.

This kind of usage scenario is also available for document controllers. Chapter 4 of the (Document-)Controller documentation describes the necessary steps to create controllers with the DIServiceManager.

It is recommended to use this approach for (Document-)Controller that make use of complex services or use several services. Besides, you can use this paradigm to improve testability of controllers through removal of explicit dependencies.

Similar to creation of document controllers you can also create front controller actions with the DI container. This eases implementation and removes explicit dependencies to improve transparency and testability as well.

Chapter 3.3 of the Front controller documentation summarizes the steps to create actions with the DIServiceManager.

This kind of usage is recommended for Front controller actions that make ise of complex actions or use several services. Besides, you can use this paradigm to improve testability of actions through removal of explicit dependencies.

4.5. Areas of validity

The Adventure PHP Framework offers several possibility for Creation of objects that allow creating instances with different kind of areas of validity. Using ServiceManager or DIServiceManager you can also leverage these mechanisms. Creating objects with APFObject::getServiceObject() described in chapter 3.2.2 and configure services as noted in chapter 4.3 the area of validity can be defined programatically or by configuration per instance.

The following table describes the available areas of validity and their recommended use case scenario:

Area of validity Description Use case
NORMAL Object is being created at any request to ServiceManager or DIServiceManager and is thus probably initialized every time. In case a new object should be automatically applied with the current context and language but there is no need to operate on the same instance this area of validity is recommended. In general SINGLETON is recommended for services.
SINGLETON Object is created and initialized on first request at ServiceManager or DIServiceManager only. It is considered valid during the entire HTTP request. Objects having this area of validity can be used to exchange information between different HMVC elements within one HTTP request.

Another use case is multiple usage of services within different areas of the software and within one HTTP request in case initialization is complex and should therefore be performed only once.
SESSION_SINGLETON Object is created and initialized on first request at ServiceManager or DIServiceManager only. It is considered valid during the entire HTTP session. Objects having this area of validity can be used as view model e.g. for multi-page workflows to temporarily store data across multiple HTTP requests within one visit being processed at the end of a user flow consolidated.

Another use case is multiple usage of services within different areas of the software and within one HTTP session in case initialization is complex and should therefore be performed only once.
APPLICATION_SINGLETON Object is created and initialized on first request at ServiceManager or DIServiceManager only. It is considered valid during the entire web server up-time. Objects having this area of validity can be used to exchange data across components within your application independent of request or session.

Another use case is multiple usage of services within different areas of the software and within an entire application in case initialization is complex and should therefore be performed only once.

Please note that using SESSION_SINGLETON and APPLICATION_SINGLETON as desired area of validity objects will be serialized between different requests. Since resources (e.g. file pointer, database connections) cannot be serialized they have to be re-initialized at every new request if necessary.

In order to maintain a database connection within class DataMapper that is requested from ServiceManager oer DIServiceManager you may want to use the following code:

PHP code
class DataMapper extends APFObject { /** * @var MySQLiHandler */ private $connection; ... public function __wakeup() { $this->connection = ...; } }

As soon as the service is requested within a subsequent request __wakeup() is called and the connection is re-created.

In case your are using the DIServiceManager and you make use of the setupmethod you can also reset the initialization status of your instance. This ensures that the setupmethod is called again at the first request of the respective service and thus the service is re-initialized. To use this approach you may want to use the following code:

PHP code
class DataMapper extends APFObject { /** * @var MySQLiHandler */ private $connection; ... public function __sleep() { $this->markAsPending(); } public function initialize() { $this->connection = ...; } }

This option is limited to usage with the DIServiceManager and requires usage of the setupmethod to initialize the service.

Comments

Do you want to add a comment to the article above, or do you want to post additional hints? So please click here. Comments already posted can be found below.
There are no comments belonging to this article.