ZF2's New Controller::init()

In Zend Framework 1, controller's had an init() method, which was called after the controller was instantiated. The reason for it was to encourage developers not to override the constructor, and thus potentially break some of the functionality (as a number of objects were injected via the constructor). init() was useful for doing additional object initialization.


class MyController extends Zend_Controller_Action
{
    public function init()
    {
        // do some stuff!
    }
}

But this feature is missing from ZF2; how can we accomplish this sort of pattern?

Constructor Overriding

Why didn't we add the init() method in Zend Framework 2? Because we don't have a constructor by default, and also to reduce overall method calls; if a controller won't be overriding the method, there's no reason to make the call in the first place. So, that means, in ZF2, unlike ZF1, to do the same thing, you can simply define a constructor in your controller:


use Zend\Mvc\Controller\AbstractActionController;

class MyController extends AbstractActionController
{
    public function __construct()
    {
        // do some stuff!
    }
}

Except there's one specific and often-needed use case where this fails: if you want to wire listeners to the controller's event manager.

Events

Why does this fail with the event manager? Because when we're in the constructor, we don't yet have an EventManager instance! The event manager instance is injected after instantiation. As such, we need to attach to it once we know we have an event manager. Which is... when it's set. This can be done very simply by overriding the setEventManager() method. In the next example, we'll define a listener for the "dispatch" event that redirects if certain criteria is not met.


use Zend\EventManager\EventManagerInterface;
use Zend\Mvc\Controller\AbstractActionController;

class MyController extends AbstractActionController
{
    public function setEventManager(EventManagerInterface $events)
    {
        parent::setEventManager($events);

        $controller = $this;
        $events->attach('dispatch', function ($e) use ($controller) {
            $request = $e->getRequest();
            $method  = $request->getMethod();
            if (!in_array($method, array('PUT', 'DELETE', 'PATCH'))) {
                // nothing to do
                return;
            }

            if ($controller->params()->fromRoute('id', false)) {
                // nothing to do
                return;
            }

            // Missing identifier! Redirect.
            return $controller->redirect()->toRoute(/* ... */);
        }, 100); // execute before executing action logic
    }
}

The beauty of this is that we only override when we need to, and we override in the specific context we're trying to influence. Additionally, we're only registering the event listener if this particular controller is instantiated -- which helps reduce our overall number of listeners, and thus shapes our call graph based on the controller invoked.

Other dependencies

As you'll likely note, the event manager is not the only case where you may need to follow a similar pattern. Any time your initialization logic may be based on a dependency, you'll want to override the setter in which that dependency is injected.

Got any ZF2 tips of your own to share? Blog them!

Update: ServiceManager

A few folks in the comments were remarking that the felt that omitting the init() method makes it harder for developers to identify when and where to do initialization logic, particularly when you may be working with multiple dependencies.

Which made me realize there's another place I missed, one that's potentially even better suited to initialization: the ServiceManager.

Basically, if you find yourself having complex initialization needs, or many dependencies, you should be building a factory for your controller, and wiring it to the ServiceManager. This can happen in one of several places, but my preference is in my module's Module class, in the getControllerConfig() method. This method returns configuration for the controller manager that instantiates, validate, and injects controllers; it's basically a type of ServiceManager, and, in fact, has access to the main application's instance. I'll take the previous example, and wire it in the context of a factory:


namespace My

use Zend\EventManager\EventManagerInterface;

class Module
{
    /*
     * Assume some other methods, such as getConfig(), etc.
     * Also assume that a route will return a controller named
     * "My\Controller\My" which we assume will reference a controller
     * within our current namespace.
     */

    public function getControllerConfig()
    {
        return array('factories' => array(
            'My\Controller\My' => function ($controllers) {
                $services   = $controllers->getServiceLocator();
                $controller = new Controller\MyController();
                $events     = $services->get('EventManager')

                $events->attach('dispatch', function ($e) use ($controller) {
                    $request = $e->getRequest();
                    $method  = $request->getMethod();
                    if (!in_array($method, array('PUT', 'DELETE', 'PATCH'))) {
                        // nothing to do
                        return;
                    }

                    if ($controller->params()->fromRoute('id', false)) {
                        // nothing to do
                        return;
                    }

                    // Missing identifier! Redirect.
                    return $controller->redirect()->toRoute(/* ... */);
                }, 100); // execute before executing action logic

                $controller->setEventManager($events);
                return $controller;
            };
        ));
    }
}

The above will create the controller, grab an event manager instance, attach the listener, and then inject the event manager into the controller. If you wanted to do more complex work, you definitely could -- and this would be the place to do it.

blog comments powered by Disqus