<?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.