Dmitry Sheiko's

Web Development Blog

Dependency Injection via Factory

24 January 2012

You know, when coupling is not loose, components depend too much on each other. It makes your entire architecture fragile and immobile. You can check how loose the coupling is by making a unit test for a component. If you have no problem substituting dependencies by e.g. mock objects then everything is ok. Let take a model class. It depends on DB connection, here Lib_Db_Adapter_Interface instance. We cannot just create DB adapter instance within model constructor, because it depends on configuration data which don’t belong to the model. We can pass to the model constructor a settings array with DB configuration data.

class Model_User
{
    private $_db;

    public function  __construct($settings)
    {
        $this->_db = new Lib_Db_Adapter_Mysqli($settings['dbConfig']);
    }
}
$model = new Model_User(array(
    "dbConfig" => array("host" => "localhost", )
));

It will serve, but as soon as you need to change adapter, let’s say SQLite instead of MySQLi, you are in trouble.

Well, but if we keep an instance of DB adapter in the registry globally accessible? When testing we just can replace it with another instance.

class Model_User
{
    private $_db;

    public function  __construct()
    {
        $this->_db = Lib_Registry::getInstance()->db;
    }
}
Lib_Registry::getInstance()->db = new Lib_Db_Adapter_Mysqli($dbConfig);
$model = new Model_User();

The model now depends on the registry, what is not really good. Well, instead hardcoding dependency in the model class, let’s inject it.

class Model_User
{
    private $_db;

    public function  __construct(Lib_Db_Adapter_Interface $db)
    {
        $this->_db = $db;
    }
}

$db = new Lib_Db_Adapter_Mysqli($dbConfig);
$model = new Model_User($db);

That is the way most of frameworks deal with dependencies. Most, but not all. For an instance, Symphony 2 follows dependency container approach. In there, dependencies among classes are described in configuration file, automatically put to the container and every time when required taken from there instead of being injected. So every time when you need a model you just call one not bothering of all the dependency references required to make an instance.

But what about a strategy whence we still have dependency injection, but no headache when instantiating a consumer object? Thanks Alex for hinting, if we are in control of object creating, we can keep dependencies on the factory and inject them automatically, when making an instance of a consumer object. Since all the objects will be created using that factory, it is supposed to allow getting as ordinary instances as well as singletons. So the factory is itself a singleton. Let’s have shortcut function creating an instance of the factory and returning it’s the only instance with every request afterwards.

function Factory()
{
    static $instance = null;
    if (!$instance) {
        $instance = new Lib_Factory();
    }
    return $instance;
}

Using magic methods __get and __call we can make it serving following syntax:

$instance = Factory()->SomeClass;
$instance = Factory()->SomeClass($param, $param);

When we need a singleton it will be like that:

Factory()->getInstance("SomeClass");

Now about dependencies. We describe which classes have which dependencies in the configuration file:

return array(
  "Model_*" => array(
        "Lib_Db_Adapter_Interface",
   ),
  "Dao_*" => array(
        "Lib_Db_Adapter_Interface",
   ),
);

Here declared: all the classes which names starts with Model_ (models) depend on Lib_Db_Adapter_Interface (dependency name).

Now we can assign dependency reference:

Factory()->defineDependency('Lib_Db_Adapter_Interface')->Lib_Db_Adapter_Mysqli($dbConfig);

It means, here we create an instance of DB adapter and store it in the factory under the name Lib_Db_Adapter_Interface. Now when we request a model, the Db adapter instance will be injected in the model automatically. Thus we can make a model instance like that:

$instance = Factory()->Model_User;

The code of the factory:


The Factory

function Factory()
{
    static $instance = null;
    if (!$instance) {
        $instance = new Lib_Factory();
    }
    return $instance;
}

class Lib_Factory
{
    private $_instances = array();
    private $_dependencyMap = null;
    private $_dependencyName = false;
    
    public function  __construct()         
    {
        $this->setDependencyMap(PATH_CONFIG . "/Dependencies.php");
    }

    /**
     *
     * @param string/array $map
     */
    public function  setDependencyMap($map)
    {
        $this->_dependencyMap = new Lib_Config($map);
    }

    /**
     * Triggers the chain to consider next created instance as a dependency
     *
     * @param string $dependencyName
     * @param string $className
     * @return instance
     */
    public function  defineDependency($dependencyName = null)
    {
        $this->_dependencyName = $dependencyName;
        return $this;
    }
   
    /**
     * Access to the multiton
     *
     * @param string $className
     * @return instance
     */
    public function  getInstance($className = null)
    {
        if (isset ($this->_instances[$className])){
            return $this->_instances[$className];
        } else {
            return ($this->_instances[$className] = $this->__get($className));
        }
    }

    /**
     *
     * @param string $className
     * @param array $args
     * @return instance
     */
    public function  __call($className,  $args)
    {
        if (class_exists($className)) {
            return $this->_newInstanceArgs($className, $args);
        } else {
            throw new Lib_Factory_Exception($className . ' class not found');
        }
    }

    /**
     *
     * @param string $className
     * @return instance
     */
    public function  __get($className)
    {        
        if (class_exists($className)) {
            $dependedClasses = $this->_dependencyMap->getLike($className)->toArray();
            return $dependedClasses 
                ? $this->_newDependedInstance($className, $dependedClasses) : $this->_newInstance($className);
        } else {
            throw new Lib_Factory_Exception($className . ' class not found');
        }        
    }
    /**
     *
     * @param string $className
     * @param array $dependedClasses
     * @return instance
     */
    private function _newDependedInstance($className, array $dependedClasses)
    {
        $args = array();        
        foreach ($dependedClasses as $id) {
            $args[] = $this->getInstance($id);
        }
        return $this->_newInstanceArgs($className, $args);
    }

    /**
     * Makes new instance when arguments given
     *
     * @param string $className
     * @param array $args
     * @return instance
     */
    private function _newInstanceArgs($className, $args)
    {
        $r = new ReflectionClass($className);
        if (is_null($r->getConstructor())) {
            return $r->newInstance();
        }
        return $this->_evaluate($className, $r->newInstanceArgs($args));
    }
    /**
     * Makes new instance when no arguments given
     *
     * @param string $className
     * @return instance
     */
    private function _newInstance($className)
    {
        return $this->_evaluate($className, new $className());
    }

    /**
     * Allows constructions like
     * <code>
     * Factory()->defineDependency()->Lib_Controller_Request($requestString);
     * Factory()->defineDependency('Lib_Db_Adapter_Interface')->Lib_Db_Adapter_Mysqli();
     * </code>
     * @param string $className
     * @param instance $instance
     * @return instance
     */
    public function  _evaluate($className, $instance)
    {        
        if ($this->_dependencyName === false) {
            return $instance;
        }
        $this->_dependencyName = is_null($this->_dependencyName) ? $className : $this->_dependencyName;
        $this->_instances[$this->_dependencyName] = $instance;        
        $this->_dependencyName = false;
        return $instance;
    }
}

The factory uses Lib_Config to read and then access configuration:

Lib_Config

class Lib_Config
{

    /**
     * Contains array of configuration data
     *
     * @var array
     */
    protected $_data = null;

    /**
     * Config provides a property to
     * an array. The data are read-only.
     *
     * @param  string|array   $dataSource
     * @return void
     */
    public function __construct($dataSource = null)
    {
        if (is_null($dataSource)) {
            return $this;
        }
        if (is_string($dataSource)) {
            $dataSource = $this->_readFromFile($dataSource);
        }
        if (!is_array($dataSource)) {
            throw new Exception('Invalid data source');
        }
        $this->_data = array();
        foreach ($dataSource as $key => $value) {
            if (is_array($value)) {
                $this->_data[$key] = new self($value);
            } else {
                $this->_data[$key] = $value;
            }
        }
    }

    /**
     * Load data fro source code
     *
     * @param string $path
     * @return array | string
     */
    private function _loadData($path)
    {
        static $registry = array();
        if (isset ($registry[$path])) {
            return $registry[$path];
        }
        $data = array();
        if (file_exists($path)) {
            ob_start();
            $data = include($path);
            ob_end_clean();
        }
        $registry[$path] = $data;
        return $registry[$path];
    }

    private function _readFromFile($configPath)
    {
        try {
            $configArray = $this->_loadData($configPath);
        } catch (Exception $e) {
            throw new Exception('Cannot open configuration file');
        }
        if (empty($configArray)) {
            throw new Exception('Configuration array is empty');
        }
        return $configArray;
    }

    /**
     * Returns first found element of the key, which the given string starts with
     * 
     * @param string $name
     * @return mixed
     */
     public function getLike($name)
    {
        $res = array();
        foreach ($this->_data as $key => $val) {
            $_key = preg_replace ("/\*$/", "", $key); // allows pattern with *
            if (strpos($name, $_key) === 0) {
                $res[] = $key;
            }
        }
        if (empty ($res)) {
            return new self();
        }        
        @usort($res, "strlen"); // The longer string, the more relevant        
        $key = array_shift($res);
        return $this->get($key);
    }

     /**
     * Retrieve a value and return $default if there is no element set.
     *
     * @param string $name
     * @return mixed
     */
    public function get($name = null)
    {
        if ($name === null) {
            return $this->_data;
        }
        $result = null;
        if (array_key_exists($name, $this->_data)) {
            $result = $this->_data[$name];
        }
        return $result;
    }

    /**
     * Magic function so that $obj->value will work.
     *
     * @param string $name
     * @return mixed
     */
    public function __get($name)
    {
        return $this->get($name);
    }
    /**
     * Return an associative array of the stored data.
     *
     * @return array
     */
    public function toArray()
    {
        if (is_null($this->_data)) {
            return false;
        }
        $array = array();
        $data = $this->_data;
        if (!empty ($data)) {
            foreach ($data as $key => $value) {
                if ($value instanceof Lib_Config) {
                    $array[$key] = $value->toArray();
                } else {
                    $array[$key] = $value;
                }
            }
        }
        return $array;
    }
}


Here the unit-test:

Unit Test

include __DIR__ . "/Di/Classes.php";

class DiTest extends PHPUnit_Framework_TestCase
{

    public function testFactoryMakesInstance()
    {
        $instance = Factory()->TestDi_Class;
        $this->assertTrue( $instance instanceof  TestDi_Class);
    }

    /**
     * @dataProvider providerArgVal
     */
    public function testFactoryMakesInstanceWitArgs($arg)
    {
        $instance = Factory()->TestDi_ClassWithConstructor($arg);
        $this->assertTrue( $instance instanceof  TestDi_ClassWithConstructor);
        $this->assertEquals($arg, $instance->arg);
    }


    public function testFactoryAccessesSingletion()
    {
        $instance = Factory()->getInstance("TestDi_Class");
        $this->assertTrue( $instance instanceof  TestDi_Class);
        $instance->counter ++;
        $instance = Factory()->getInstance("TestDi_Class");
        $this->assertEquals(1, $instance->counter);
    }

    /**
     * @dataProvider providerDependencyMap
     */
    public function testFactoryInjectsDependency($map)
    {
        Factory()->setDependencyMap($map);
        $instance = Factory()->TestDi_ConsumerClass;
        $this->assertTrue( $instance instanceof  TestDi_ConsumerClass);
        $this->assertTrue( $instance->dependency instanceof  TestDi_DependencyClass);
    }

    /**
     *
     * @return array
     */
    public function providerArgVal()
    {
        return array(
            array("argument"),
        );
    }
    /**
     *
     * @return array
     */
    public function providerDependencyMap()
    {
        return array(
            array(
                array(
                  "TestDi_ConsumerClass" => array(
                        "TestDi_DependencyClass",
                   ),
                )
            ),
            array(
                array(
                  "TestDi_Consumer*" => array(
                        "TestDi_DependencyClass",
                   ),
                )
            ),
        );
    }
}

The test is using following test classes:

Di/Classes.php

class TestDi_DependencyClass
{

}

class TestDi_ConsumerClass
{
    public $dependency;
    public function  __construct(TestDi_DependencyClass $instance)
    {
        $this->dependency = $instance;
    }
}

class TestDi_Class
{
    public $counter = 0;
}

class TestDi_ClassWithConstructor
{
    public $arg = 0;
    public function  __construct($arg)
    {
        $this->arg = $arg;
    }
}