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.

21 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
  5. Another amazing component from the Body Champ IT8070 inversion therapy is that it's equipped for supporting a weight as substantial as 250lbs yet is reduced adequate when collapsed and can be put away effectively anyplace. The highest point of the line wellbeing lock holds the gear secure from unfurling without anyone else's input. boots back pain

    ReplyDelete
  6. You readily explained every topic! Looking forward to reading more posts from your blog…t his site is equally good, just check it out...check my post on non slip shoes restaurant

    ReplyDelete
  7. This post came at the very right time. Thanks for taking time to write this educating post. I have save this post and will come back for new post. HSBC Management Trainee

    ReplyDelete
  8. Thanks for the great content, it is such a pleasure to read it. hsbc internship deadline 2021

    ReplyDelete
  9. I always found very much interesting content on your posts. keep posting, thanks for sharing with us and giving us your precious time. You may also like letter to a mother who lost her son 

    ReplyDelete
  10. Your article is very informative and helpful to me. Thank you for the post, it’s really nice. This is more good! Visit my site. norwegian+grants for africa

    ReplyDelete
  11. The wire betting | The wire betting app - JetSports
    The 화성 출장마사지 wire betting app lets you bet from 서귀포 출장샵 anywhere in the 서귀포 출장안마 world and on a single With 강릉 출장마사지 the app, you can 광주 출장마사지 place wagers on sporting events from anywhere.

    ReplyDelete
  12. The hottest betting sport is determined by} the placement and season. Football is a popular 바카라사이트 betting sport within the United Kingdom, compared to with} basketball within the United States. However, soccer is the most well-liked sport to bet on in comparison with} other sports. If you want to improve your probabilities, think about these elements earlier than betting.

    ReplyDelete
  13. Love to read this post. Thank you for posting part 1.
    New Vehicle Inventory

    ReplyDelete