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:

  1. Whether you inject the tweet service is up to the interface you check, hence the name interface injection

  2. 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:

  1. 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;

  2. 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:

  1. 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;

  2. 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, the initializers key can also be put in the module configuration, under the service_manager key.