Wednesday, September 11, 2013

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

In Part 1, we learned how to inject a single, shared dependency into the controller using the ServiceManager as an inversion of control container, but what if you need multiple instances of a class? For example, what if you needed multiple Brick objects to build that Building? Here is the Brick class:
<?php
#module/Building/src/Building/Model/Brick.php
namespace Building\Model;

class Brick
{
    protected $_color;
    protected $_randomColors = array("red", "brown", "black", "yellow",
        "orange", "purple", "green");

    public function __construct($color)
    {
        $this->_color = ($color===null)?"default":$color;
    }

    public function setColor($color)
    {
        $this->_color = $color;
    }

    public function getColor()
    {
        return $this->_color;
    }

    public function getRandomColor()
    {
        return $this->_randomColors[array_rand($this->_randomColors)];
    }
}


Now the Building model needs to be able to get multiple unique instances of Brick. To accomplish this, we'll need a basic understanding closures and capturing state. If we follow the same pattern as before, and add Brick as a dependency in Building, we will only have access to a single instance of Brick. Additionally, we will be unable to provide any runtime dependencies, such as the $color parameter in Brick's construction.

<?php
#module/Building/src/Building/Model/Building.php (wrong)
namespace Building\Model;

class Building
{
    protected $_brick;

    public function __construct($brick)
    {
        $this->_brick = $brick;
    }

    public function getBrick()
    {
        return $this->_brick;
    }

In order to have multiple Bricks instantiated from within Building, we'll need to pass in the ability for Building to do so on its own. This can be done by passing in a BrickFactory. The factory is another closure, so we will have a closure within a closure. Notice that the runtime parameter $color is able to be passed in for the Brick construction

    #module/Building/Module.php excerpt
    public function getServiceConfig()
    {
        return array(
            'factories'=>array(
                'BrickFactory'=>function($sm)
                {
                    $factory = function ($color=null)
                    {
                        $brick = new Model\Brick($color);
                        return $brick;
                    };
                    return $factory;
                },
                //...

The $factory in this BrickFactory service doesn't need access to the ServiceManager in this example, but to imagine the Brick class having a BrickMapper dependency. If you are following along, don't copy this into your code.

    #module/Building/Module.php excerpt (aside)
    public function getServiceConfig()
    {
        return array(
            'factories'=>array(
                'BrickFactory'=>function($sm)
                {
                    $factory = function ($color=null) use ($sm)
                    {
                        $mapper = $sm->get('BrickMapper');
                        $brick = new Model\Brick($mapper, $color);
                        return $brick;
                    };
                    return $factory;
                },
                'BrickMapper'=>function($sm)
                {
                    $factory = $sm->get('BrickFactory');
                    $mapper = new Model\BrickMapper($factory);
                    return $mapper;
                },
                'Brick'=>function($sm)
                {
                    $factory = $sm->get('BrickFactory');
                    $model = $factory->__invoke();
                    return $model;
                },
                //...
Luckily, doing it this way does not create circular dependencies, even though the mapper uses the factory, and the factory uses the mapper. Notice also that if we needed a single Brick model in some other class, we can create a brick for the ServiceManager using the BrickFactory, albeit without runtime construction parameters.

The use keyword is what captures the ServiceManager for use later, when the factory is invoked. Without it, the factory would be unable to create the BrickMapper, because the scope of the closure is limited to itself.
Anyway, back to creating Bricks. We can now modify the Building class to use the BrickFactory to create new Bricks. The factory closure needs be called, or 'invoked'. This can be done with either $factory() or $factory->__invoke(). I prefer the latter, as it is slightly less mysterious what is going on. The parameters inside the parenthesis are passed to the $factory closure.

    #module/Building/src/Building/Model/Building.php excerpt

    public function __construct($brickFactory)
    {
        $this->_brickFactory = $brickFactory;
    }

    public function getNewBrick($color=null)
    {
        $factory = $this->_brickFactory;
        $brick = $factory->__invoke($color);
        return $brick;
    }

    public function addLayer($color=null)
    {
        $layer = array();
        for ($i=1; $i<=6; $i++)
        {
            $brick = $this->getNewBrick();
            if ($color === null)
            {
                //constructed with the default color and then initialized
                $brick = $this->getNewBrick();
                $brick->setColor($brick->getRandomColor());
            }
            else
            {
                //constructed fully initialized with runtime 
                //  parameter (usually preferred)
                $brick = $this->getNewBrick($color);
            }
            $layer[] = $brick;
        }
        $this->_bricks[]=$layer;
    }

What we've just done can be difficult to comprehend if closures are new to you. They are a tricky concept, but once understood, can be used to create such elegant code. Leave me a comment if this needs elaboration. For now, we are done with the conceptual stuff. Continue reading to get everything running.
Next, we need to configure and inject this dependency. While we're here, lets do the same thing for the ViewModel that the controller needs.

    #module/Building/Module.php excerpt
    public function getServiceConfig()
    {
        return array(
            'factories'=>array(
                //...
                'Building'=>function($sm)
                {
                    $factory = $sm->get('BrickFactory');
                    $building = new Model\Building($factory);
                    return $building;
                },
                'ViewFactory'=>function($sm)
                {
                    $factory = function($variables=null, $options=null)
                    {
                        $viewModel = new \Zend\View\Model\ViewModel($variables,
                            $options);
                        return $viewModel;
                    };
                    return $factory;
                },
                //...
And finally, add those dependencies to the constructors in their respective classes
    #module/Building/Module.php excerpt
    public function getControllerConfig()
    {
        return array('factories' => array(
            'Building\Controller\Building' => function ($sm)
            {
                $building = $sm->getServiceLocator()->get('Building');
                $viewFactory = $sm->getServiceLocator()->get('ViewFactory');
                $controller = new Controller\BuildingController(
                    $building, $viewFactory);
                return $controller;
            }
        ));
    }
    #module/Building/src/Building/Controller/BuildingController.php excerpt
    public function getViewModel($variables = null, $options = null)
    {
        return $this->_viewFactory->__invoke($variables, $options);
    }

    public function indexAction()
    {
        $building = $this->getBuilding();
        $building->addLayer('blue');
        $building->addLayer();
        $building2 = $this->getBuilding(); //gets same instance
        $building2->addLayer('green');
        $building2->addLayer();
        $viewModel = $this->getViewModel(array('building'=>$building));
        return $viewModel;
    }
<?php
#module/Building/src/Building/Model/Building.php
namespace Building\Model;

class Building
{
    protected $_brickFactory;
    protected $_bricks;

    public function __construct($brickFactory)
    {
        $this->_brickFactory = $brickFactory;
    }

    public function getNewBrick($color=null)
    {
        $factory = $this->_brickFactory;
        $brick = $factory->__invoke($color);
        return $brick;
    }

    public function addLayer($color=null)
    {
        $layer = array();
        for ($i=1; $i<=6; $i++)
        {
            $brick = $this->getNewBrick();
            if ($color === null)
            {
                $brick->setColor($brick->getRandomColor());
            }
            else
            {
                $brick->setColor($color);
            }
            $layer[] = $brick;
        }
        $this->_bricks[]=$layer;
    }

    public function getBricks()
    {
        return $this->_bricks;
    }

}
<?php #module/Building/view/building/building/index.pthml ?>
<table >
<?php
foreach ($this->building->getBricks() as $key=>$layer)
{
    echo '<tr>';
    foreach ($layer as $brick)
    {
        echo '<td style="text-align:center; border:1px solid black; '.
            'background-color:' . $brick->getColor() . '">';
        echo $brick->getColor();
        //echo spl_object_hash($brick);
        echo '</td>';
    }
    echo '</tr>';
}
?>

Now you can go to http://localhost/building to see that everything works as expected. You should see 4 layers: a green, a blue, and 2 with random colored bricks. Go to https://github.com/rwilson04/zf2-dependency-injection/tree/part2 for complete code.
For production, you might want to convert the closures in the 'factories' key to classes, as suggested in this article.
I'm still trying to figure out how to include initializers, and have them work on the objects created by the closures. If you have a solution, let me know.
Another thing to consider would be type-hinting. For anything that is swappable, you would define an interface and add that to the method definitions. For other types, I'm not sure yet whether including type-hinting is the right way to go.
Thanks for reading. I hope this helped. Leave me a comment, especially if there is something missing or something that needs clarification.

11 comments:

  1. thanks for the post. Ive been trying to search something like this. Although i zf2 just have a simpler implementation of this through invokables

    $sm->invokeWith('red')->get('brick');

    ReplyDelete
    Replies
    1. That is good to know. I'm not seeing how that is simpler though. Could you explain?

      Delete
    2. I wrote a ZF2 module that would enable for you to do this

      $brick = $sm->get('brick')->construct($someArgs);
      echo $brick->someMethod();

      check it out:
      https://github.com/franz-deleon/ConstructInvoker

      Delete
  2. I'm sorry if I'm missing your point. It looks like that method pulls classes from the servicemanager while running code the controller code, which I am trying to avoid. I want one high-level place where all the configuration and injecting is done, the 'inversion of control container'. The point of these posts is to use the servicemanager as that container, and get away from having class names for new objects inside other classes.

    I was doing something similar to this before, using the abstractpluginmanager, but it required setting up those extra constructor/factory classes just to create objects, and can get really messy really fast, especially when you start needing to inject things which have their own dependencies, and doing this in multiple places across controllers and modules.

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

    ReplyDelete
    Replies
    1. Ah, I think I get it now. Yes, being lazy is the best way to go in programming. If you can both invert control and avoid creating all those factories, that seems like a good thing.

      Delete
    2. I got where you are saying now man. Anyway thanks for the tips and contributions! Have you written any cool zf2 modules?

      Delete
    3. Nothing shareable at the moment, but when I do, I'll probably have a post about it

      Delete
  4. Hi Reese Wilson,

    First of all thanks for the excelent content.

    But in the last paragraph you included a link to an article about how to convert the facotories to classes, but the link is broken.
    Can you please correct the link?

    Thanks again.

    ReplyDelete