ZF2: Why a service is not available in your constructor

Today people still ask why a certain service is not available in a constructor. You can think of events, the event manager or the service locator: there are some variations, but all are about the same topic. Perhaps you can recognise one or more of the list:

  1. Why is $this->getServiceLocator() not available in my controllers’s __construct()?
  2. Why does $this->getEvent() return null inside __construct() of a controller?
  3. How could I get the event manager in the constructor while I have implemented the EventManagerAware interface?

The answer is actually really simple: you have to understand how objects are constructed in Zend Framework 2. I will explain how object creation works in Zend Framework 2 and give you three methods how to solve this problem.

The basics

Construction of an object happens with the new keyword in php. It will return an instance of the class you specified (so new Foo will return an instance of the class Foo).

If the class has implemented a constructor, this constructor will be called after the object has been instantiated and before the object is returned to it’s caller. This makes it possible to have an object context (i.e. $this) in a constructor, where you can configure the object before it’s given back. In php, a constructor is marked with the magic method __construct().

Zend Framework 2’s object creation

In ZF2, many objects can be created by the service manager. This is true in one of the three cases:

  1. Some class from the Zend\ namespace (think about the Application class, the PhpRenderer or the EventManager)
  2. A class you specified to be “loadable” (think about controllers, controller plugins or view helpers)
  3. A class you specifically construct via the service manager (inside the service_manager configuration key or via the getServiceConfig() method).

The questions stated at the top of this post are all valid for objects within the classification above.

Initializers in the service manager

I have blogged before about the concept of initializers. Initializers are the trick used here. Let’s take a step-by-step example of a POP which is instantiated:

class Foo
{
    public function __construct()
    {
        echo "Foo";
    }
}

Well, if you would do new Foo; it would show you “Foo”. Now let’s give the Foo object a method setBar() to set a Bar object:

class Foo
{
    protected $bar;

    public function __construct()
    {
        echo "Foo";

        echo gettype($this->bar);
    }

    public function setBar(Bar $bar)
    {
        $this->bar = $bar;
    }
}

Everybody understands here that it will output “NULL” (and not “object”) since the constructor is called first and only after construction you’re able to call setBar().

This principle is what happens when you use initializers. An initializer is able to set dependencies through setters (the set*() methods) but this will happen only after the __construct() has been called. Basically, what happens inside the service manager is this:

public function get($name)
{
    // If the object is available, return it
    if ($this->has($name)) {
        return $this->objects[$name];
    }

    $object = new $name;

    // Initializer #1
    if ($object instanceof ServiceLocatorAwareInterface) {
        $object->setServiceLocator($this);
    }

    // Initializer #2
    if ($object instanceof EventManagerAwareInterface) {
        $em = $this->get('EventManager');
        $object->setEventManager($em);
    }

    $this->objects[$name] = $object;
    return $object;
}

This is obviously a simplified version, but you clearly see that first the object is created and then initializers call the setters on the object.

The solution

It’s fairly simple: avoid using the dependencies injected by initializers in the constructor. You cannot (seriously, how hard you try doesn’t matter) access the services if you use initializers. However, there are various alternatives to achieve the same goal. I will explain three of them:

1. Use attachDefaultListeners() for events

If you need to access the event manager to attach a listener, you don’t need to do this inside the constructor. You can, for instance with controllers, use the attachDefaultListeners() method:

class MyController extends AbstractActionController
{
    protected function attachDefaultListeners()
    {
        // Do not forget to call the parent method
        parent::attachDefaultListeners();

        // Do you own things here
        $events = $this->getEventManager();
        $events->attach(/* ... */);
    }
}

If you have a custom class (i.e. a view helper, controller plugin or other service) where you need the event manager, you can create the attachDefaultListeners() yourself:

class MyService implements EventManagerAwareInterface
{
    protected $em;

    public function setEventManager(EventManager $em)
    {
        $this->em = $em;

        $this->attachDefaultListeners();
    }

    public function getEventManager()
    {
        return $this->em;
    }

    public function attachDefaultListeners()
    {
        // Do your own things here
        $events = $this->getEventManager();
        $events->attach(/* ... */);
    }
}

2. Wait to access the service locator in your controller

Very often you can just delay the execution for something in the service locator. There is no need to call in directly in the constructor, you can directly call it in your action:

class MyController extends AbstractActionController
{
    public function FooAction()
    {
        $foo = $this->getServiceLocator()->get('Foo');
    }
}

In case you need to call the service from multiple actions, just extract the call to a separate method:

class MyController extends AbstractActionController
{
    protected function getFoo()
    {
        $foo = $this->getServiceLocator()->get('Foo');
        return $foo;
    }
}

3. Don’t use the service locator, inject dependencies

This is seen as a far superior method than #2, but it is slightly more complicated for new users. The idea is to get rid of all getServiceLocator() calls completely and directly inject all dependencies. This method is not only for controllers, but in fact for every service you have!

Given above example of a Foo service requesting a Bar, you can make the dependency very explicit:

class Foo
{
    protected $bar;

    public function __construct(Bar $bar)
    {
        $this->bar = $bar;
    }
}

This way, you have to set Bar during construction and you cannot leave it out. In other words: Bar is a hard dependency of Foo and this is now made explicit in the code.

The question is of course, how you would inject the dependency in this service. This is where the service locator really shines, because you can create a Factory for the Foo object.

There are two ways to have a factory. One is a simple function, the other is a more maintainable class. The function is a closure:

// inside your Module.php
public function getServiceConfig()
{
    return array(
        'factories' => array(
            'Foo' => function ($sl) {
                // Instantiate it
                $bar = new Bar;
                // Or (better!), get it from the locator
                $bar = $sl->get('Bar');

                $foo = new Foo($bar);
                return $foo;
            },
        ),
    );
}

The second one requires two steps. First, register the factory in your module.config.php:

'service_manager' => array(
    'factories' => array(
        'Foo' => 'MyModule\Factory\FooFactory'
    ),
),

And create the FooFactory class:

use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;

use MyModule\Service\Foo;

class FooFactory implements FactoryInterface
{
    public function createService(ServiceLocatorInterface $sl)
    {
        $bar = $sl->get('Bar');
        $foo = new Foo($bar);

        return $foo;
    }
}

More in-depth information about dependency injection is available a post I wrote some time ago: Refactor towards dependency injection.

Fin

This post explained why you cannot access some of your service you expected to be available. There is a fairly straightforward explanation possible why this is an issue, but often users forget the underlying principles of the framework.

I hope it is made clear now how this can be an issue and especially, how this can be solved. If you find anyone asking about this issue, just link them to this page in the future!