Interface injection with initializers in Zend\ServiceManager
The Zend\ServiceManager
is a component which handles (besides other stuff) dependency injection. During the developments of Zend Framework 2 I have looked at dependency injection thoroughly. A good resource is from Martin Fowler, where he explains there are three types of dependency injection. In this post I am particularly interested in injecting soft dependencies with interface injection.
The help of the Zend\ServiceManager
makes it straightforward to have
decoupled objects. For the use cases where you have a soft dependency, the
ServiceManager has a great tool called initializers. Initializers are small
“callables” providing add-on features for your objects you pull from the
service manager.
Soft dependencies
As an example I will use a logger class. A Zend\Log\Logger
object can log
messages to various so-called writers and I see loggers as a typical soft
dependency. Apart from the definition of dependency I would say that a hard
dependency is " a maximum constraint for a service to perform an executed
task and directly related to the nature of the objective “. A soft dependency
in contrary is " a minimum constraint for a service electively used in the
execution of the task and not related to the nature of the objective “.
In practice, it means if you have a service BloggerService
and a method
createPost()
you have a hard dependency on a database adapter if you persist
your blog posts into a database: it is required to perform the task and the
database is related the the objective of persisting the post. If you
optionally send a tweet that you blogged about something new, you might inject
a TweetService
in the BloggerService
. If the BloggerService
has a
TweetService
, it will tweet. If you don’t set the TweetService
, it does
not tweet: the tweet service is not required to perform the task and is also
not related to the nature of persisting a blog post.
Interface injection
If you want to inject soft dependencies with dependency injection, it is interesting to look at interface injection. It means when a class implements a certain interface you can inject the dependency. I assume here you have already an idea of the terms Dependency Injection or Inversion of Control. Interface injection is explained in the most simple way with a code example:
<?php
interface TweetServiceAwareInterface
{
public function setTweetService(TweetService $service);
}
class BloggerService implements TweetServiceAwareInterface
{
public function setTweetService(TweetService $service)
{
}
}
And if you have a factory then you can look for the interface:
<?php
class ServiceFactory
{
public function createService($name)
{
// Create service here based on $name
if ($service instanceof TweetServiceAwareInterface) {
// Create tweet service
$service->setTweetService($tweetService);
}
return $service;
}
}
As you might notice there are two important aspects here:
-
Whether you inject the tweet service is up to the interface you check, hence the name interface injection
-
The service is a soft dependency: when the service does not implement the
TweetServiceAwareInterface
, the tweet service is not set and the code should still work.
Initializers
You might expect now that initializers in the ServiceManager
are a good
option to use if you want this kind of behavior. Initializers watch every
class created and can “enhance” the instantiation process by doing additional
work, for example injecting soft dependencies.
Let’s get back to the practical example of using a logger. There is a logger
interface to use for your initializer: LoggerAwareInterface
. Assume you have
a service where you might log some actions a user performs. Analogue to above
writings you implement the interface:
<?php
namespace MyModule\Service;
use Zend\Log\Logger;
use Zend\Log\LoggerAwareInterface;
class MyService implements LoggerAwareInterface
{
protected $logger;
public function setLogger(Logger $logger)
{
$this->logger = $logger;
}
public function getLogger()
{
return $this->logger;
}
public function doSomething()
{
// Do stuff here
if (null !== $this->getLogger()) {
$this->getLogger()->info('User did something here');
}
// Continue your work
}
}
In the method you can check if you have a logger. If so, log the message. If not, skip the logging and continue. The final piece here is the initializer you need to write. Important is you met two prerequisites in order to get the interface injection working:
-
The logger must be a service inside the service manager. You can create a factory for the logger, what I already described in an earlier post;
-
The
MyService
class can be fetched from the service manager. You can for example make the service an invokable in the service manger.
Then your initializer can look something like this:
<?php
namespace MyModule;
use Zend\Log\LoggerAwareInterface;
use Zend\Mvc\MvcEvent;
class Module
{
public function getServiceConfig()
{
return array(
'initializers' => array(
'logger' => function ($service, $sm) {
if ($service instanceof LoggerAwareInterface) {
$logger = $sm->get('logger');
$service->setLogger($logger);
}
}
),
);
}
}
If you now call $serviceManager->get('my-service');
the initializer sees
that MyService
is an instance of the LoggerAwareInterface
. It pulls the
logger from the service manager and injects it into your MyService
class.
It allows you to decouple the logging from the service and reuse the
initializer for many classes. If you want more classes consuming a logger,
simply add another class and make that LoggerAware
. If you want to replace
the log instance in all services, just replace the factory for the logger. If
you want to disable logging, remove the initializer. It is extremely flexible
and once you get used to it a great tool for your applications.
Some final notes:
-
I made an initializer now which is a closure. You can make an initializer by any callable or if you provide a class (an instance or a FQCN string) which implements
Zend\ServiceManager\InitializerInterface
; -
I enabled the initializer in the
getServiceConfig()
from a module class. You can add the initializer anywhere in your code base, but remember to add the initializer before you pull the class. Otherwise, your initializer will not inject the dependencies. As I wrote in an earlier post, theinitializers
key can also be put in the module configuration, under theservice_manager
key.