jump to navigation

Using ZF2′s ServiceManager Component to Lazy-load Dependencies on Standalone Codebases April 5, 2014

Posted by Tournas Dimitrios in PHP, Zend Framework 2.
Tags: , ,
trackback

Zend Framework 2 logoThis article is intended to familiarize you with various features of  Zend Framework 2’s ServiceManager component (in short, SM). By demonstrating a couple of practical examples, concepts like : IOC , DI and SOLID principles are better digested. So, what is the ServiceManager used for ? In simple words, it’s a registry, or container that “holds” various objects needed by our application, allowing us to easily implement Inversion of Control. The main goal here is to be able to wire up and configure our application from the highest level possible. This results into an testable/maintainable and pluggable application (which I believe, is an objective of every [PHP] developer) .
By default, Zend Framework 2 utilizes Zend\ServiceManager\ServiceManager within its MVC layer (SM “plays” a central role). As such, knowing various features of this component (the SM) will allow us to better understand how  Zend Framework 2 is structured. Although the official documentation has an extensive introduction to SM’s concepts , it might be intimidating for a newcomer . This article will take an alternative approach to demonstrate SM’s basic features , it will detach the component from ZF2’s MVC layer and use it as a supplementary ingredient  into a standalone code-base. Besides all , ZF is composed from loosely coupled components .

Each object registered into SM provides a specific service to our application. A common web-application has services for : mailing , logging , authentication , session , database-connection , temporarily-storage … (the list goes on) . We distinguish these services by the kind of service they provide and how these services are instantiated (while some [objects|services] are autonomous blocks ,other objects have dependencies) . Zend Framework 2’s SM component has “labeled” its registered services (objects) as follows :

  • invokables
  • factories
  • aliases
  • initializers
  • abstract-factories

The official documentation has made a very good job in this regard (describing each service) , replicating its content has totally no meaning (neither could I do a better description) . In contrast , this article will use the first four services from the list ( invokables , factories , aliases and initializers) to build a simple logging application . The last service-type (abstract-factories) is omitted because it is difficult to demonstrate its functionality without breaking the barriers of this article’s simplicity .

Prerequisites : The reader should have a good grasp of OOP concepts . Also knowing to use Composer’s basic functionality is helpful , as it will be used to offload basic tasks like : downloading the required component and using its auto-load functionality .
Composer file used for this article :

{
	"repositories": [
		{
		    "type": "composer",
		    "url": "https://packages.zendframework.com/"
		}
	],
	"require": {
		"zendframework/zendframework": "2.2.*",
		"firephp/firephp-core": "*"
	}

}

Beside the fact that Zend Framework 2’s ServiceManager allows us to programmatically configure the instantiation of  objects (services) and their associated dependencies, these services are only instantiated the moment there is a need for their functionality. The process of instantiating a service/object (and its associated dependencies) at the moment these services are required, is named “Lazy loading” (I know , funny technical jargon). Three examples will clearly describe what I’m talking about :

  1. Why would we instantiate an object (service) if that object is not used by our application ? For instance  : why would we go through the whole process of querying a remote service , while we  have already cached the data from a previous request ?
  2. Why would we go through the whole process of establishing a database connection while our visitor hasn’t used the database in this current visit (for instance , he/she canceled the process of updating a Blog-article).
  3. Why would we go through the whole process of initiating a “Logging” object while this object will not be required on current request . For instance, our application was configured to “Log”  errors , but there hasn’t occurred an error during current session.

All frameworks nowadays ,  have plethora of features (services) . These services might be consumed, or might not be consumed (that  decision will be made by the visitor, most likely, momentarily ) . Lazy-loading has a notable impact into an application’s performance (CPU-cycles , memory consumption).

hooray picture

 

Hooray …. hooray , boring introduction is finished. Let’s begin the show with three practical examples . 

First example : 

This example uses the simplest methodology (constructor DI), objects of a “Logging” library will be instantiated first , then injected into the constructor of each Class that might use their functionality . To keep things simple we will be using ZF2’s “Log” component to log errors into a file . 

require 'vendor/autoload.php' ;
$logger = new Zend\Log\Logger() ;
$writer = new Zend\Log\Writer\Stream('logfile.txt');
$logger->addWriter($writer);

class LogService
{
	protected $logger;

	public function __construct(Zend\Log\Logger $logger)
    {
        $this->logger = $logger;
		$this->init() ;
    }

	public function init()
    {
		if (!file_exists('logfile.txt'))
		{
		file_put_contents('logfile.txt' , '..Initializing file .....');
		}
    }

	public function log($msg , $priority = Zend\Log\Logger::NOTICE)
    { 

        if (null !== $this->logger) {
			$this->logger->log($priority , $msg) ;
        }
    }
}// End of LogService

$logService = new LogService($logger) ;
$logService->log('Lorem ipsum ..... ') ;

//..

Explanation :
Nothing special , objects are instantiated “by hand” and injected into LogService’s constructor . All components of ZF’s “Log” library have to be instantiated , even if the “LogService” is not used yet. On small applications , nobody will mention the impact this methodology has on the performance of our application .

Second example :

Using ServiceManager’s “Lazy-loading” functionality

require 'vendor/autoload.php' ; 

interface LoggerAwareInterface
{
	public function setLogger(Zend\Log\Logger $logger);
} // End of LoggerAwareInterface

class LogService implements LoggerAwareInterface
{
	protected $logger;

	public function setLogger(Zend\Log\Logger $logger)
    {
        $this->logger = $logger;
    }

	public function log($msg , $priority = Zend\Log\Logger::NOTICE)
    {
        if (null !== $this->logger) {
			$this->logger->log($priority , $msg) ;
        }
    }
}// End of LogService

$fileWriterFactory = function() {
	if (!file_exists('logfile.txt'))
		file_put_contents('logfile.txt' , '..Initializing file .....');
	return new Zend\Log\Writer\Stream('logfile.txt');

	} ;

$browserWriterFactory = function() {
	return new Zend\Log\Writer\FirePhp(new Zend\Log\Writer\FirePhp\FirePhpBridge(new FirePHP()));

	} ;

$logServiceInitializer = function($logService , $sm) {
    if ($logService instanceof LoggerAwareInterface) {
        $logger = $sm->get('logger');
		$writer = $sm->get('writer') ;
		$logger->addWriter($writer) ;
        $logService->setLogger($logger);
		}
    } ; 

// instantiating a ServiceManager object
$sm = new Zend\ServiceManager\ServiceManager() ;
$sm->setInvokableClass('logger' , 'Zend\Log\Logger')
   ->setFactory('writer' , $fileWriterFactory)
   //->setFactory('writer' , $browserWriterFactory)
   ->addInitializer($logServiceInitializer)
   ->setInvokableClass('logService' , 'LogService')
   ->setAlias('recordToFile' , 'LogService');

$logService = $sm->get('logService') ;
$logService->log('Lorem ipsum .....') ;

//..

Explanation :

  • Line 48 : Instantiating a new ServiceManager object . Imagine this object as a wizard’s magic-box , every magic-trick of this box is configurable with “setter-methods” (setFactory , setInvokableClass , setAlias , addInitializer , setAbstractFactory) . Zend’s theory of operation, has detailed description of each “setter-method” . Let’s start configuring a few magic-tricks .
  • Line 49  :  As “Zend\Log\Logger” doesn’t require any preparation , setInvokableClass is used to define a service .
  • Line 50 : Zend’s Logger requires a configured writer-adapter , there are many adapters to choose from (db , Chrome , FirePHP , XML , file) . To keep things simple , file-adapter is chosen for this example . As writer-adapters requires some kind of preparation (ie passing arguments to its constructor ) ,  factory-type is chosen to set-up a “writer” object . Factories  may be any PHP callable,  class or object that implements  Zend\ServiceManager\FactoryInterface.
  • Line 51 : An example of how to set-up an alternative “writer-adapter” (FirePHP , to display log’s into browsers console panel) . Keep in mind that this type of writer requires an extra library (“firephp/firephp-core”: “*”) and a browser plug-in .
  • Line 52 : Initializer-type services can be used to perform additional initialization tasks. The most common use case is to test the instance against specific “Aware” interfaces, and, if matching, inject them with the appropriate service. This example uses a class “LogService” that  implements a “LoggerAwareInterface”. Our initializer-Callback does lot of preparation :
    1) retrieves a “logger” service from the SM (a Logger service was already defined in Line 49) . If SM receives a request for a service , it creates that object and returns it to the applicant. But also , stores that object into an internal “pool” . Subsequent requests for that specific service will receive the same object (retrieved from the pool) .
    2) retrieves a “writer” service from the SM (a Writer service was already defined in Line 50) .
    3) passes the Writer object into the Logger object
    4) passes the Logger object into LogService object
  • Line 54 : Defining an Alias-type service (which alias one service name to another) .

Everything is set-up and works smoothly , there is a limiting factor though . What if we wanted to consume this logging service from inside of other services ? Simple , you might say . We just have to retrieve the LogService from the ServiManager and pass it into the constructor of the consumer service . We  love to automate every aspect of our application and want SM to do this job for us (passing a LogService object into other services ) . The example below , demonstrates how this can be achieved .

Third example : 

require 'vendor/autoload.php' ; 

interface LoggerAwareInterface
{
	public function setLogger(Zend\Log\Logger $logger);
} // End of LoggerAwareInterface

class LogService implements LoggerAwareInterface
{
	protected $logger;

	public function setLogger(Zend\Log\Logger $logger)
    {
        $this->logger = $logger;
    }

	public function log($msg , $priority = Zend\Log\Logger::NOTICE)
    {
        if (null !== $this->logger) {
			$this->logger->log($priority , $msg) ;
        }
    }
}// End of LogService

$logServiceInitializer = function($logService , $sm) {
    if ($logService instanceof LoggerAwareInterface) {
		$writer = $sm->get('writer') ;
        $logger = $sm->get('logger');
		$logger->addWriter($writer);
        $logService->setLogger($logger);
		}
	} ;

$fileWriterFactory = function() {
	if (!file_exists('logfile.txt'))
		file_put_contents('logfile.txt' , '..Initializing file .....');
	return new Zend\Log\Writer\Stream('logfile.txt');

	} ;
$browserWriterFactory = function() {

	return new Zend\Log\Writer\FirePhp(new Zend\Log\Writer\FirePhp\FirePhpBridge(new FirePHP()));

	} ;

$logServiceInitializer = function($logService , $sm) {
    if ($logService instanceof LoggerAwareInterface) {
        $logger = $sm->get('logger');
		$writer = $sm->get('writer') ;
		$logger->addWriter($writer) ;
        $logService->setLogger($logger);
		}
    } ;
$myCustomInitializer = function($myCustom , $sm) {
    if ($myCustom instanceof Zend\ServiceManager\ServiceLocatorAwareInterface) {
     	$myCustom->setServiceLocator($sm) ; 

		}
    } ;

// instantiating a ServiceManager object
$sm = new Zend\ServiceManager\ServiceManager() ;
$sm->setInvokableClass('logger' , 'Zend\Log\Logger')
   ->setFactory('writer' , $fileWriterFactory)
   //->setFactory('writer' , $browserWriterFactory)
   ->addInitializer($logServiceInitializer)
   ->setInvokableClass('logService' , 'LogService')
   ->setAlias('recordToFile' , 'LogService')
   ->setInvokableClass('myCustom' , 'MyCustom')
   ->addInitializer($myCustomInitializer) ;   

// serviceLocatorAwareInterface
class MyCustom implements Zend\ServiceManager\ServiceLocatorAwareInterface
{

	protected $serviceLocator ; 

    /**
     * Setting service locator
     *
     * @return void
     */
	public function setServiceLocator(Zend\ServiceManager\ServiceLocatorInterface $serviceLocator)
	{
		$this->serviceLocator = $serviceLocator ;
	}

    /**
     * Get service locator
     *
     * @return ServiceLocatorInterface
     */
    public function getServiceLocator()
	{
		return $this->serviceLocator ;
	}

    /**
     * Testing
     *
     * @return void
     */
    public function doSomething()
	{
		if (! $this->serviceLocator instanceof Zend\ServiceManager\ServiceLocatorInterface)
		{
			throw new Exception('"MyClass" can\'t be instantiated directly, get it via the SM');
		}
		$logService = $this->serviceLocator->get('logService') ;
		$logService->log('hello from class :  '. __CLASS__ ) ;
	}
}// End of MyCustom Class

/*
$myCustom = new MyCustom() ;
$myCustom->doSomething() ; // throws an exception
*/
$myCustom = $sm->get('myCustom') ;
$myCustom->doSomething() ; 

//,,

Explanation :
Let’s pretend that “MyCustom” Class is a service that wants to embed (consume) “LogService” into its functionality . We could of course extract “LogService” out of SM and pass it into MyCustom’s constructor . But we want to exploit the benefits that SM has to offer  (by automating the task through configurations) .

  • Every aspect discussed in previous example is also applicable in this example . Let’s just highlight all newly added parts of this example .
  • MyCustom Class implements a ServiceLocatorAwareInterface . This Interface is an contract, a guarantee if you like . Every object that implements this Interface has to provide two methods with a respective implementation (setting and getting a ServiceLocator object) . An Initializer-callback is triggered whenever we request a MyCustom service from SM . The Initializer-Callback will handle all heavy work (extracting a LogService and passing it into MyCustom service through its setServiceLocator method) .
  • Keep in mind, the order in which a service is registered into SM plays a crucial role . Services that depend on other services have to be registered last (ie , all dependencies have to be registered prior the request of an applicant) .

    Zend Framework 2 has the same functionality in its core . Every Controller implements a ServiceLocatorAwareInterface Interface and ServiceManager handles the heavy work to pass ServiceLocator object (a SM object) into each Controller . This gives each Controller the potentiality to access (extract) whatever service it might need to accomplish a task .

Final thoughts :
ServiceManager plays a central role into  Zend Framework 2  . Thanks to ZF’s modular structure we can also use this component (and 70 other ZF-components) to turbo-charge any standalone application.  I had to skip many details to keep this article in reasonable size . If you are new to all these concepts (IOC , DI and SOLID principles ) don’t try to digest the whole article in one read . Nobody can gain knowledge in “one shot” , it demands effort . But at the end , suddenly everything  becomes clear . Don’t hesitate to comment on this article . Keep your self healthy and happy coding.

Links :

Comments»

1. devEric69 - November 4, 2015

Thank you – again – for this tutorial about the ServiceManager: once again, it allowed me to put all my global objects (all created in the script starting with “session_start”, included in all the other scripts) each one in its own service, and to finally create a ServiceProvider (following the example 3). I was able to do that, in an application that is absolutly not ZF2-MVCEvent based.

Just a little information: if Foo1 and Foo2 classes each rise in their service, we must implement a Foo1Interface and a Foo2Interface interface: one class, one typed interface that implements their own injections, their own respective dependencies.
Indeed, in the code for “ServiceManager.pfp-> DoCreate ()”, there are the following loop that launches all the registered initializers, when the SM asks a component:
“foreach ($ this-> initializers as $ initializer) {if ($ initializer instanceof InitializerIterface) {$ initializer_> initialize ($ instance, $ this);} else {call_user_func ($ initializer, $ body, $ this);}
} ”
This Code therefore calls “foo1Initializer = function …”, and then “foo2Initializer = function …”, and then “foo3Initializer = function …”, etc. To resume, the mistake I made, was to create a single “fooInitializer = function …” for all the services’s classes. I did this by relying on a supposed magic behavior of ZF2, which was a silly assertion: the consequence of this mistake was that ZF2 tried to inject for each service-class called, all the dependencies indicated in “fooInitializer = function …”.
🙂


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s