Friday, September 28, 2012

(outdated) Using the ServiceManager as an Inversion of Control Container (Part 1)

Note: This post has been superseded. Check out the Part 1 and Part 2. The post you are reading now is outdated, and there is a much better way of doing this, in my opinion. 

Note about superseded posts: The new Part 1's changes mostly involve coding style and use of design patterns. In the outdated Part 2, I attempted to instantiate multiple instances of objects using the AbstractPluginManager. This worked for simple things, but got complicated when one of the non-shared instantiated instances had dependencies. The best I could work out involved creating factory factories, which created the required factories, which then created the model I wanted. Even without dependencies, the AbstractPluginManager method still required creating those extra factory classes for the sole purpose of creating objects, which leads to a plethora of files in bigger applications. Another deficiency of the outdated version is that it still has hard-coded dependencies in the form of type-hinting.

In Zend Framework 1, it was difficult to follow best practices when it came to writing testable code. Sure, you could make testable models, but once you need those models in a controller, what do you do? Zend Framework 2 makes it much easier. In this post, I'll cover the basics of injecting a model into a controller.

Motivation:

The main goal here is to be able to wire up and configure your application from the highest level possible. Constructor injection + inversion of control makes it easy to determine which classes are dependent on other classes. The Getting Started guide uses the ServiceManager in the Controller to pull in the model, which creates "soft dependencies", so you can't completely tell which classes depend on other ones unless you look at the code on the lower levels. For actual testable/maintainable code, avoid this as much as possible.

Prerequisites:
Begin by creating a new Building module, with a structure like the Album module in the Getting Started guide, and a route to /building so it is accessible. For the actual module code, let's start by adding the BuildingController with a Building model as a dependency, which we'll learn how to inject shortly.

<?php
#Building/src/Building/Controller/BuildingController.php
namespace Building\Controller;

use Building\Model\Building;

class BuildingController extends AbstractActionController
{
    protected $building;  

    public function __construct(Building $building)
    { 
        $this->building = $building;
    } 

    public function getBuilding()
    { 
        return $this->building;
    } 

    public function indexAction()
    { 
        $building = $this->getBuilding();
        $building->addLayer('red');
        $viewModel = $this->getViewModel(array('building'=>$building)); #we'll add this function later
        return $viewModel;
    } 
}

Zend Framework 2's ServiceManager allows you to programmatically configure your dependencies. You should already be at least vaguely familiar with this from the ZF2 Getting Started guide. The Application's ServiceManager is responsible for creating services. Internally, when tasked with creating a service with a particular name, "Building\Controller\Building" for example, it runs canCreate("Building\Controller\Building"), which checks all of your aggregated configs. So one of the places it checks by default is the array returned by the method getControllerConfig() in Module.php. This is where we can add the factory closure for the model that the controller needs access to.

    #Module.php
    public function getControllerConfig()
    { 
        return array('factories' => array(
            'Building\Controller\Building' => function ($sm)
            { 
                $building = $sm->getServiceLocator()->get('Building');
                $controller = new Controller\BuildingController($building);
                return $controller;
            } 
        ));
    }  
Creating controllers is actually handled by the ControllerManager, a (sub)subclass of the main ServiceManager, and $usePeeringServiceManagers is set to false, which is why here we need $sm->getServiceLocator()->get() instead of just $sm->get(); it needs to retrieve the main ServiceManager to have access to the rest of the application's services.

It is important to note that if the name of controller configuration (in this case Building\Controller\Building) is listed here, it cannot be defined somewhere else as well. For example, if it is in the $config['controllers']['invokables'] section in your module.config.php, the ServiceManager will try to 'invoke' it, that is, construct it with no arguments, and will fail. Check for this situation now.

Let's create the Building model as a simple class with no dependencies for now.

<?php
#Building/src/Building/Model/Building.php
namespace Building\Model;

class Building
{
    protected $colors = array("red", "brown", "black", "yellow", "orange", "purple", "green");

    public function addLayer($color=null)
    { 
        #add either a random color brick, or the color specified         $newBrickColor = ($color === null)?$this->colors[array_rand($this->colors)]:$color;
        echo "Added $newBrickColor brick";
    }
}

Since there are no dependencies or other required constructor arguments, we can define Building\Model\Building as an invokable in Module.php. The getServiceConfig() method is one of the aggregated configs that the ServiceManager checks to see which services it can create, and you should recognize it from the Getting Started guide.
    #Module.php
    public function getServiceConfig()
    { 
        return array(
            'invokables'=>array(
                'Building'=>'Building\Model\Building',
            ),
        );
    }  
That's it. Control has now been inverted. You are injecting a model into a controller using the ServiceManager, and all of your wiring and configuration is in one place. It is possible to separate the configuration into multiple files, which might be advisable once your wiring starts getting complicated. The factories can be their own classes which implement 'FactoryInterface' instead of closures defined in the config array.

If you want to try it out now, you'll need to replace the line in the controller
$viewModel = $this->getViewModel(array('building'=>$building));
with
$viewModel = new ViewModel(array('building'=>$building));
Then just go to http://localhost/building, and if you set your routing up, it will just be an empty page with the line echo'd in the indexAction. Hint: don't forget the getConfig() and getAutoloaderConfig() methods in your Module.php

Check out Part 2 to learn how to inject a dependency where each instance needs to be distinct and have its own configuration.

Please leave me some comments, especially if you see something I did wrong, could do better, needs clarification, etc.

10 comments:

  1. Awesome , solved the mystery for me. Keep it up.

    ReplyDelete
  2. This comment has been removed by the author.

    ReplyDelete
  3. Thanks for the tutorial, very useful. Only problem I'm having is with the routing. I cannot seem to add the route to the building correctly.
    What I did is this:

    - added the Building module to the application.config.php:
    array(
    'Application',
    'Album',
    'Building',
    ),
    ... etc.

    - Added the route in module/Building/config/module.config.php:
    array(
    'invokables' => array(
    'Building\Controller\Building' => 'Building\Controller\BuildingController',
    ),
    ),

    'router' => array(
    'routes' => array(
    'building' => array(
    'type' => 'segment',
    'options' => array(
    'route' => '/building[/:action]',
    'constraints' => array(
    'action' => '[a-zA-Z][a-zA-Z0-9_-]*',
    ),
    'defaults' => array(
    'controller' => 'Building\Controller\Building',
    'action' => 'index',
    ),
    ),
    ),
    ),
    ),

    'view_manager' => array(
    'template_path_stack' => array(
    'building' => __DIR__ . '/../view',
    ),
    ),
    );

    So pretty much the same as I did for the Album module in the Getting Started guide. Am I missing something?
    Btw, for someone who's used to ZF1's routing I feel like ZF2's routing is a lot more complicated. Really have to get used to all those long config arrays in different locations.

    ReplyDelete
    Replies
    1. Check your routing in your application and album modules to see if something there is catching the route first. What error is it giving when you try to go to the route? If you figure this out, I'd like to update the post so that other people don't run into the same problem

      Delete
    2. Hi Reese,

      Thanks for the reply. I'm simply getting a 404 with the message "The requested URL could not be matched by routing.".
      So it doesn't look like something is catching the route first, it simply can't find it.
      I'll continue trying to figure this out and if I find the solution I'll post it. If in the meantime you have any more suggestions then please let me know.

      Delete
    3. Okay, I figured it out.

      Two things were wrong in my setup:

      - My Module.php (in the Building module) didn't contain implementations of getConfig and getAutoloaderConfig, so I added both:

      public function getConfig()
      {
      return include __DIR__ . '/config/module.config.php';
      }

      public function getAutoloaderConfig()
      {
      return array(
      'Zend\Loader\ClassMapAutoloader' => array(
      __DIR__ . '/autoload_classmap.php',
      ),
      'Zend\Loader\StandardAutoloader' => array(
      'namespaces' => array(
      __NAMESPACE__ => __DIR__ . '/src/' . __NAMESPACE__,
      ),
      ),
      );
      }

      - Besides that I still had the invokables array in my module.config.php. I removed that to prevent the constructor of the BuildingController being invoked with no arguments (as explained in your article already).

      All works nice and dandy now.

      Delete
  4. When we put type hinting in a controller constructor like:

    use Building\Model\Building;
    public function __construct(Building $building)

    do we also create a hard-wired dependency?

    ReplyDelete
    Replies
    1. I've been wondering that myself lately. I created my applications to work with unit testing, so the type-hinting works with mocks, but it wouldn't work so well if I wanted to completely switch out a class. I've been thinking of changing it to either an interface type, so that the methods called on the injected object are pre-defined, or just leaving it out. I don't know yet though, what are your thoughts?

      Delete
    2. I'm leaning towards type-hinting with interfaces, but I imagine there being redundancy (copying all the public functions from a class into an interface, which I guess is similar to a c++ header file...), so if I go that route, I would probably define that after most of my development is done.

      Delete