Inversion of Control is a pattern I did not know yesterday, I know it a little better today and love it.
Leaving the theory behind I’d like to show an application of its principles I’m using while developing my theme framework.
The situation before applying the IoC pattern
The Main
class uses an instance of the Bot
class to “find itself” both in filesystem and namespacing terms.
In the MainTest
class file I’m trying to test the expectation of the show
method being called on the FirstController
object.
TestMain
extends Main
and FirstController
extends Controller
; this is due to the fact that both Main
and Controller
are abstract classes and cannot be tested on their own.
public function testShowWillCallShowOnController()
{
// get an instance of the subject under test
$sut = TestMain::getInstance();
// get a mock instance of the FirstController class to inject it in the TestMain but...
$mockController = $this->getMockBuilder('\TAD\Test\controllers\FirstController')->setMethods(array(
'show'
))->disableOriginalConstructor()->getMock();
// ... it's the Bot that will provide the FirstController instance to the TestMain and I have to mock it...
$mockBot = $this->getMockBuilder('\TAD\MVC\helpers\Bot')->setMethods(array(
'getControllerInstance'
))->disableOriginalConstructor()->getMock();
// ... to return the mock FirstController instance when requested via the `getControllerInstance` method
$mockBot->expects($this->once())->method('getControllerInstance')->will($this->returnValue($mockController));
// finally I set an expectation on the FirstController mock
$mockController->expects($this->once())->method('show');
// inject the mock Bot
$sut->bot = $mockBot; // <<<<<<<<<<<<<<< DEPENDENCY INJECTION via a purposedly made set method
$sut->show('First');
}
There is nothing wrong with this implementation of the test but for the fact that I’ve added a __set
method to the Main
class to allow, see the code above, for dependency injection.
The problem with this is that my code exposes now one of its properties, bot
, to allow testing. Having implemented the __set
magic method actually exposes all the Main
class properties but the same stands if I had implemented the setBot
method alone.
I do not like implementing a method for the sake of testing, sure it’s better than non-tested code, but still I know I will not use the setBot
method in production.
The good
My IoC implementation is, right now, trivial
<?php
namespace TAD\helpers;
class IoC{
protected static $resolvers = array();
public static function register($key, \Closure $resolver){
static::$resolvers[$key] = $resolver;
}
public static function make($key, $params = array()){
if (!isset(static::$resolvers[$key])) {
throw new \RuntimeException("Resolver for $key is not set in IoC", 1);
}
$resolver = static::$resolvers[$key];
return call_user_func_array($resolver, $params);
}
public static function deregister($key = null){
if (isset($key)) {
static::$resolvers[$key] = null;
}
else static::$resolvers = array();
}
}
and very common place among the internet. In my Main
class the __construct
method reads like
protected function __construct()
{
// set the called class
$this->calledClassName = get_called_class();
// init the root namespace and root folder path
$this->setRoots();
// get controllers information
$this->bot = IoC::make('Bot', array($this->mainFilePath, $this->mainClassNamespace));
// $this->bot = new Bot($this->mainFilePath, $this->
// namespace);
$this->controllers = array();
}
and I get an instance of the Bot
class from the IoC
and no more from the Bot
class directly.
Somewhere else I’ve registered the default IoC resolver for the Bot
class like
// set the Bot class default resolver
IoC::register('\TAD\MVC\helpers\Bot', function ($path, $fullNamespace)
{
return new \TAD\MVC\helpers\Bot($path, $fullNamespace);
});
and thus using IoC::make('Bot', ...)
will actually return an instance of the Bot
class. I can now refactor my test code to make its dependency injection in the Main::__construct
method removing the way-too-much-stuff-exposing __set
method from the Main
class.
public function testShowWillCallShowOnController()
{
// register the mock bot in the IoC
IoC::register('Bot', function ()
{
$mockBot = $this->getMockBuilder('\TAD\MVC\helpers\Bot')->setMethods(array(
'getControllerInstance'
))->disableOriginalConstructor()->getMock();
$mockController = $this->getMockBuilder('\TAD\Test\controllers\FirstController')->setMethods(array(
'show'
))->disableOriginalConstructor()->getMock();
$mockBot = $mockBot->expects($this->once())->method('getControllerInstance')->will($this->returnValue($mockController));
$mockController->expects($this->once())->method('show');
return $mockBot;
});
// dependency injection happens in the constructor
$sut = TestMain::getInstance();
$sut->show('First');
}
The bad
While setting IoC resolvers for dependency injection in tests is a breeze some code is added to default IoC resolution for production code either in some fallback way or via an explicit setting (the way I did it).
The ugly
There is none beside me staring at the screen after 10 hours straight of coding but I really could not use that header…
See the code using Inversion of Control
I’ve setup a different git
branch to try my hand at IoC, here it is.