Dependency Injection via Factory

Refactoring

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 doesn’t belong to the model. We can pass to the model constructor a settings array with DB configuration data.

namespace Model;
class User
{
    private $_db;

    public function  __construct($settings)
    {
        $this->_db = new \Lib\Db\Adapter\Mysqli($settings['dbConfig']);
    }
}

Usage:

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

namespace Model;
class User
{
    private $_db;

    public function  __construct()
    {
        $this->_db = \Lib\Registry::getInstance()->db;
    }
}

Usage:

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

namespace Model;
class User
{
    private $_db;

    public function  __construct(\Lib\Db\Adapter\Interface $db)
    {
        $this->_db = $db;
    }
}

Usage:

$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 Tsertkov 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;
}

By means the factory we can create instances like that:

// Assignation
$instance = Factory()->build("\Model\User");
// Call a method
Factory()->build("\Model\User")->method();

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

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

When we need a singleton it will be like that:

Factory()->getInstance("\Model\User");

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

namespace Lib;
class Factory
{
    private $_instances = array();
    private $_dependencyMap = null;
    private $_dependencyName = false;

    /**
     *
     * @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 singleton
     *
     * @param string $className
     * @return instance
     */
    public function  getInstance($className)
    {
        $className = self::getClassNameTranslated($className);
        if (isset ($this->_instances[$className])){
            return $this->_instances[$className];
        } else {
            return ($this->_instances[$className] = $this->_build($className));
        }
    }

    /**
     * Build an instance by namespaced class name
     *
     * <code>
     * Factory()->build("\Model\User");
     * </code>
     * @param string $className
     * Optional:
     * @param mixed $arg1
     * @param mixed $argN
     */
    public function  build()
    {
        $args = func_get_args();
        $className = array_shift($args);
        return $this->_build($className,  $args);
    }
    /**
     * Build an instance of class which name specified as undefined factory method
     *
     * @param string $className
     * @param array $args
     * @return instance
     */
    public function  __call($className,  $args)
    {
        return $this->_build(self::getClassNameTranslated($className),  $args);

    }
    /**
     * Build an instance of class which name specified as undefined factory property
     *
     * @param string $className
     * @return instance
     */
    public function  __get($className)
    {
        return $this->_build(self::getClassNameTranslated($className));
    }

    /**
     * Translate variable name into namespaced classname
     *
     * @param type $className
     * @return type
     */
    public static function getClassNameTranslated($className)
    {
        return "\\" . ltrim(str_replace("_", "\\", $className), "\\");
    }

    /**
     * Build instance by the given class name
     *
     * @param string $className
     * @param array $args
     * @return instance
     */
    private function  _build($className,  array $args = array())
    {
        if (class_exists($className)) {
            // getLike method of \Lib\config object matches $classname to patterns
            if ($this->_dependencyMap
                %26%26 $dependedClasses = $this->_dependencyMap->getLike($className)->toArray()) {
                return $this->_makeInstanceWithDependencies($className, $dependedClasses, $args);
            }
            // When arguments given we use more time consuming reflection
            if (empty($args)) {
                return $this->_makeInstanceWithoutArgs($className);
            } else {
                return $this->_makeInstanceWithArgs($className, $args);
            }
        } else {
            throw new \Lib\Factory\Exception($className . ' class not found');
        }
    }

    /**
     * Inject dependecies while creating a new instance
     *
     * @param string $className
     * @param array $dependedClasses
     * @return instance
     */
    private function _makeInstanceWithDependencies($className, array $dependedClasses,
        array $otherArgs = array())
    {
        $args = array();
        foreach ($dependedClasses as $depClassName) {
            $args[] = $this->getInstance($depClassName);
        }
        return $this->_makeInstanceWithArgs($className, array_merge($args, $otherArgs));
    }

    /**
     * Make new instance with arguments
     *
     * @param string $className
     * @param array $args
     * @return instance
     */
    private function _makeInstanceWithArgs($className, $args)
    {
        $r = new \ReflectionClass($className);
        if (is_null($r->getConstructor())) {
            return $r->newInstance();
        }
        return $this->_evaluate($className, $r->newInstanceArgs($args));
    }
    /**
     * Make new instance with no arguments
     *
     * @param string $className
     * @return instance
     */
    private function _makeInstanceWithoutArgs($className)
    {
        return $this->_evaluate($className, new $className());
    }

    /**
     * Makes the trick to assign just created object to the dependency, defined previously
     * in the chain
     * <code>
     * Factory()->defineDependency()->Lib_Controller_Request($requestString);
     * Factory()->defineDependency('DbAdapterInterface')->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

namespace Lib;

class 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 \Lib\Config\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)
    {
        $configArray = array();
        try {
            $configArray = $this->_loadData($configPath);
        } catch (Exception $e) {
            throw new \Lib\Config\Exception('Cannot open configuration file');
        }
        if (empty($configArray)) {
            throw new \Lib\Config\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 (is_null($name) || is_null($this->_data)) {
            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;
    }
}

Unit Tests

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

class DiTest extends PHPUnit_Framework_TestCase
{

    public function testFactoryMakesInstance()
    {
        $instance = \Factory()->build("\TestDi\SimpleClass");
        $this->assertTrue($instance instanceof \TestDi\SimpleClass);
        // via magic method
        $instance = \Factory()->TestDi_SimpleClass;
        $this->assertTrue($instance instanceof \TestDi\SimpleClass);
    }

    /**
     * @dataProvider providerAnArgument
     */
    public function testFactoryMakesInstanceWitArgs($arg)
    {
        $instance = \Factory()->build("\TestDi\ClassWithConstructor", $arg);
        $this->assertTrue($instance instanceof  \TestDi\ClassWithConstructor);
        $this->assertEquals($arg, $instance->arg);
        // via magic method
        $instance = \Factory()->TestDi_ClassWithConstructor($arg);
        $this->assertTrue($instance instanceof  \TestDi\ClassWithConstructor);
        $this->assertEquals($arg, $instance->arg);
    }


    public function testFactoryAccessesSingletion()
    {
        $instance = \Factory()->getInstance("\TestDi\SimpleClass");
        $this->assertTrue($instance instanceof  \TestDi\SimpleClass);
        $instance->counter ++;
        $instance = \Factory()->getInstance("\TestDi\SimpleClass");
        $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 providerAnArgument()
    {
        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

namespace TestDi;

class DependencyClass
{
}

class ConsumerClass
{
    public $dependency;
    public function  __construct(\TestDi\DependencyClass $instance)
    {
        $this->dependency = $instance;
    }
}

class SimpleClass
{
    public $counter = 0;
}

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