Dependency Injection Container
Posted: Mon Nov 05, 2007 9:41 am
Excuse the verbosity, I hoped this might be a slightly educational thread a side-effect.
I was looking for some critique on this code base before I put it to full-use inside a library of mine. Driving forces to developing this were:
* To minimize coupling between groups of classes as much as possible
* To make unit testing much simpler
* To easily re-configure a library to use custom implementations of components
Classes usually have classes they depend upon and in order for those dependencies to be resolved we often end up with undesirable tight coupling between classes.
We could resolve that by passing the dependency in the constructor (dependency injection):
Or we could pass it via a public setter method:
These two approaches are what I would term "constructor-based dependency injection" and "setter-based dependency injection". Each is extremely useful and intuitive but they also start to get awkward when the number of dependencies increases.
Throw in the factory, with a sprinkle of salt:
Some UML to give a rough overview of the code-base (click to enlarge):

Imagine you could map out specifications for objects (components) and their dependencies (other components) in XML or YAML, then generate the objects based on that specification. One day you could develop a better version of a class you were using the satisfy a dependency, update your XML to refer to your new class and voila; system-wide dependency updated. If it doesn't work out, change your XML back and nothing is lost.
Dependency Injection also makes it easier to test atomic units of code in isolation, and to mock dependencies which one component may refer to.
An example XML file (not a real-world example):
Here, if we asked for a car we'd get a CustomCar with a FiatEngine using ItalianParts. Something like:
As you can see from the XML, dependencies can be basic scalar values or they can be other components. They can also be collections of values or components.
Dependencies can be injected via the constructor, or via setters like was discussed earlier.
Source code:
Swift_ComponentSpec.php
Swift_ComponentReference.php
Swift_ClassLocator.php
Swift_ComponentSpecFinder.php
Swift_ComponentSpecFinder_XmlSpecFinder.php
Swift_ComponentFactory.php
The code doesn't look for class properties themselves; in fact they don't even need to exist. What matter is whether a public setter method exists following a naming convention like a JavaBean setter (i.e setFooBar where the property is named fooBar).
I'll follow with Unit Tests
I was looking for some critique on this code base before I put it to full-use inside a library of mine. Driving forces to developing this were:
* To minimize coupling between groups of classes as much as possible
* To make unit testing much simpler
* To easily re-configure a library to use custom implementations of components
Classes usually have classes they depend upon and in order for those dependencies to be resolved we often end up with undesirable tight coupling between classes.
Code: Select all
class Car {
private $_engine;
public function __construct() {
$this->_engine = new SomeEngine();
}
}
$car = new Car();Code: Select all
class Car {
private $_engine;
public function __construct(Engine $engine) {
$this->_engine = $engine;
}
}
$car = new Car(new SomeEngine());
Code: Select all
class Car {
private $engine;
public function setEngine(Engine $engine) {
$this->_engine = $engine;
}
}
$car = new Car();
$car->setEngine($engine);Throw in the factory, with a sprinkle of salt:
Some UML to give a rough overview of the code-base (click to enlarge):

Imagine you could map out specifications for objects (components) and their dependencies (other components) in XML or YAML, then generate the objects based on that specification. One day you could develop a better version of a class you were using the satisfy a dependency, update your XML to refer to your new class and voila; system-wide dependency updated. If it doesn't work out, change your XML back and nothing is lost.
Dependency Injection also makes it easier to test atomic units of code in isolation, and to mock dependencies which one component may refer to.
An example XML file (not a real-world example):
Code: Select all
<?xml version="1.0" ?>
<components>
<component>
<name>frenchParts</name>
<className>Parts_French</className>
</component>
<component>
<name>renaultEngine</name>
<className>Engines_RenaultEngine</className>
<properties>
<property>
<key>parts</key>
<componentRef>frenchParts</componentRef>
</property>
<property>
<key>year</key>
<value type="integer">2002</value>
</property>
</properties>
</component>
<component>
<name>italianParts</name>
<className>Parts_Italian</className>
</component>
<component>
<name>fiatEngine</name>
<className>Engines_FiatEngine</className>
<properties>
<property>
<key>parts</key>
<componentRef>italianParts</componentRef>
</property>
<property>
<key>year</key>
<value type="integer">1998</value>
</property>
</properties>
</component>
<component>
<name>car</name>
<className>CustomCar</className>
<constructor>
<arg>
<componentRef>fiatEngine</componentRef>
</arg>
</constructor>
</component>
</components>Code: Select all
$car = $factory->create('car');Dependencies can be injected via the constructor, or via setters like was discussed earlier.
Source code:
Swift_ComponentSpec.php
Code: Select all
<?php
/**
* Spefication container for the Dependency Injection factory to operate.
* @author Chris Corbyn
* @package Swift
* @subpackage DI
*/
class Swift_ComponentSpec
{
/**
* The class name.
* @var string
*/
private $_className;
/**
* Arguments to be passed to the constructor.
* @var mixed[]
*/
private $_constructorArgs = array();
/**
* Properties of the object.
* @var mixed[]
*/
private $_properties = array();
/**
* True if the object should only be created once.
* @var boolean
*/
private $_singleton = false;
/**
* Set the class name to $className.
* @param string $className
*/
public function setClassName($className)
{
$this->_className = $className;
}
/**
* Get the class name.
* @return string
*/
public function getClassName()
{
return $this->_className;
}
/**
* Set the arguments to be passed into the constructor.
* @param mixed[] Constructor arguments
*/
public function setConstructorArgs(array $constructorArgs)
{
$this->_constructorArgs = $constructorArgs;
}
/**
* Get arguments to be passed to the constructor.
* @return mixed[]
*/
public function getConstructorArgs()
{
return $this->_constructorArgs;
}
/**
* Set the property with name $key to value $value.
* A public setter named setPropName() is expected where $key is propName.
* @param string $key
* @param mixed $value
*/
public function setProperty($key, $value)
{
$this->_properties[$key] = $value;
}
/**
* Get the value of the property named $key.
* @param string $key
* @return mixed
*/
public function getProperty($key)
{
return $this->_properties[$key];
}
/**
* Get all properties as an associative array.
* @return mixed[]
*/
public function getProperties()
{
return $this->_properties;
}
/**
* Make this component a singleton, or turn singleton off.
* @param boolean $singleton
*/
public function setSingleton($singleton)
{
$this->_singleton = $singleton;
}
/**
* Returns true if this component is a singleton.
* @return boolean
*/
public function isSingleton()
{
return $this->_singleton;
}
}
Code: Select all
<?php
/**
* Provides information about a component referenced within the DI container.
* @author Chris Corbyn
* @package Swift
* @subpackage DI
*/
class Swift_ComponentReference
{
/**
* The name of the component being referenced.
* @var string
*/
private $_componentName;
/**
* Create a new ComponentReference for $componentName.
* @param string $componentName
*/
public function __construct($componentName)
{
$this->_componentName = $componentName;
}
/**
* Get the name of the component referenced.
* @return string
*/
public function getComponentName()
{
return $this->_componentName;
}
}
Code: Select all
<?php
/**
* ClassLocator interface for searching for and including class files.
* @author Chris Corbyn
* @package Swift
* @subpackage DI
*/
interface Swift_ClassLocator
{
/**
* Returns true if the class exists from the ClassLocator point of view.
* @param string $className
* @return boolean
*/
public function classExists($className);
/**
* Include the class with the name $className.
* @param string $className
*/
public function includeClass($className);
}
Code: Select all
<?php
require_once dirname(__FILE__) . '/ComponentFactory.php';
/**
* A ComponentSpec finding interface when no such component is registered.
* @author Chris Corbyn
* @package Swift
* @subpackage DI
*/
interface Swift_ComponentSpecFinder
{
/**
* Try to find and create a specification for $componentName.
* Returns NULL on failure.
* @param string $componentName
* @param Swift_ComponentFactory The factory currently instantiated
* @return Swift_ComponentSpec
*/
public function findSpecFor($componentName, Swift_ComponentFactory $factory);
}
Code: Select all
<?php
require_once dirname(__FILE__) . '/../ComponentFactory.php';
require_once dirname(__FILE__) . '/../ComponentFactoryException.php';
require_once dirname(__FILE__) . '/../ComponentSpecFinder.php';
/**
* A ComponentSpecFinder which reads from a XML file or markup.
* @author Chris Corbyn
* @package Swift
* @subpackage DI
*/
class Swift_ComponentSpecFinder_XmlSpecFinder
implements Swift_ComponentSpecFinder
{
/**
* SimpleXMLElement instance.
* @var SimpleXMLElement
*/
private $_xml;
/**
* Creates a new YamlSpecFinder with the given YAML file or source.
* @param string $yaml
*/
public function __construct($xml)
{
if (is_file($xml))
{
$this->_xml = simplexml_load_file($xml);
}
else
{
$this->_xml = simplexml_load_string($xml);
}
}
/**
* Get the value of an XML node reading its type attribute, if any.
* @param SimpleXMLElement $element
* @return mixed
*/
private function _valueOf(SimpleXMLElement $element)
{
$strValue = (string) $element;
switch (strtolower((string) array_shift($element->xpath('./@type'))))
{
case 'int':
case 'integer':
return (int) $strValue;
case 'float':
return (float) $strValue;
case 'str':
case 'string':
default:
return $strValue;
}
}
/**
* Find component references, values, or collections in an element and set
* into a variable $v passed by-reference.
* Returns true if anything is set, or false if not.
* @param SimpleXMLElement $element
* @param Swift_ComponentFactory $factory
* @param mixed &$v
* @return boolean
*/
private function _setValueByReference(SimpleXMLElement $element,
Swift_ComponentFactory $factory, &$v)
{
//Element contains a collection of values
if ($collection = array_shift($element->xpath("./collection")))
{
$v = array();
foreach ($collection->children() as $child)
{
switch($child->getName())
{
case 'value':
$v[] = $this->_valueOf($child);
break;
case 'componentRef':
$v[] = $factory->referenceFor((string) $child);
break;
}
}
return true;
}
// Element is a single value
elseif ($value = $this->_valueOf(array_shift(
$element->xpath("./value"))))
{
$v = $value;
return true;
}
//Element references another component
elseif ($componentRef = (string) array_shift(
$element->xpath("./componentRef")))
{
$v = $factory->referenceFor($componentRef);
return true;
}
//Nothing found
else
{
return false;
}
}
/**
* Try create the ComponentSpec for $componentName.
* Returns NULL on failure.
* @param string $componentName
* @param Swift_ComponentFactory $factory
* @return Swift_ComponentSpec
*/
public function findSpecFor($componentName, Swift_ComponentFactory $factory)
{
//If a <component> element with this name is found
if ($component = array_shift($this->_xml->xpath(
"/components/component[name='" . $componentName . "']")))
{
//Cannot make a spec with no className
if (!$className = (string) array_shift($component->xpath("./className")))
{
return null;
}
$spec = $factory->newComponentSpec();
$spec->setClassName($className);
//Loop over all <property> elements (possibly none)
foreach ($component->xpath("./properties/property") as $i => $property)
{
if ($key = (string) array_shift($property->xpath("./key")))
{
//Set property key and value where possible
if ($this->_setValueByReference($property, $factory, $valueRef))
{
$spec->setProperty($key, $valueRef);
}
else
{
throw new Swift_ComponentFactoryException(
'Missing value(s) for property ' . $key . ' in component ' .
$componentName);
}
}
else
{
throw new Swift_ComponentFactoryException(
'Missing <key> for property ' . $i . ' in component ' .
$componentName);
}
}
$constructorArgs = array();
//Loop over all constructor arguments (possibly none)
foreach ($component->xpath("./constructor/arg") as $i => $arg)
{
//Get value were possible
if ($this->_setValueByReference($arg, $factory, $valueRef))
{
$constructorArgs[] = $valueRef;
}
else //Throw an Exception because it's not possible to know what to do
{
throw new Swift_ComponentFactoryException(
'Failed getting value of constructor arg ' . $i . ' in component ' .
$componentName);
}
}
$spec->setConstructorArgs($constructorArgs);
//Determine if component should be a singleton
if ($singleton = (string) array_shift($component->xpath("./singleton")))
{
if (in_array(strtolower($singleton), array('true', 'yes', 'on', '1')))
{
$spec->setSingleton(true);
}
}
return $spec;
}
else
{
return null;
}
}
}
Code: Select all
<?php
require_once dirname(__FILE__) . '/ClassLocator.php';
require_once dirname(__FILE__) . '/ComponentReference.php';
require_once dirname(__FILE__) . '/ComponentSpec.php';
require_once dirname(__FILE__) . '/ComponentSpecFinder.php';
require_once dirname(__FILE__) . '/ComponentFactoryException.php';
/**
* A factory class for the dependency injection container.
* Reads from specifications for components and creates configured instances
* based upon them.
* @author Chris Corbyn
* @package Swift
* @subpackage DI
*/
class Swift_ComponentFactory
{
/**
* ComponentSpec collection.
* @var Swift_ComponentSpec[]
*/
private $_specs = array();
/**
* ClassLocator collection.
* @var Swift_ClassLocator[]
*/
private $_classLocators = array();
/**
* ComponentSpecFinder collection.
* @var Swift_ComponentSpecFinder[]
*/
private $_specFinders = array();
/**
* Registered instances (pseudo-singletons)
* @var mixed[]
*/
private $_singletons = array();
/**
* Creates a new instance of the ComponentSpec class.
* @return Swift_ComponentSpec
*/
public function newComponentSpec()
{
return new Swift_ComponentSpec();
}
/**
* Creates a new ComponentReference for the given $componentName.
* @param string $componentName
* @return Swift_ComponentReference
*/
public function referenceFor($componentName)
{
return new Swift_ComponentReference($componentName);
}
/**
* Sets the specification for the given $componentName.
* @param string $componentName
* @param Swift_ComponentSpec The specification for $componentName
*/
public function setComponentSpec($componentName, Swift_ComponentSpec $spec)
{
$this->_specs[$componentName] = $spec;
}
/**
* Gets the specification for the given $componentName.
* @param string $componentName
* @return Swift_ComponentSpec
* @throws Swift_ComponentFactoryException If spec is not found
*/
public function getComponentSpec($componentName)
{
if (!isset($this->_specs[$componentName]))
{
$spec = null;
foreach ($this->_specFinders as $finder)
{
if ($spec = $finder->findSpecFor($componentName, $this))
{
$this->_specs[$componentName] = $spec;
break;
}
}
if (!$spec)
{
throw new Swift_ComponentFactoryException(
$componentName . ' does not exist');
}
}
return $this->_specs[$componentName];
}
/**
* Register a new ClassLocator for finding and loading class files.
* @param string $key
* @param Swift_ClassLocator The ClassLocator to register
*/
public function registerClassLocator($key, Swift_ClassLocator $locator)
{
$this->_classLocators[$key] = $locator;
}
/**
* Registers a new ComponentSpec finder in this factory.
* @param string $key
* @param Swift_ComponentSpecFinder The spec finder instance
*/
public function registerSpecFinder($key, Swift_ComponentSpecFinder $finder)
{
$this->_specFinders[$key] = $finder;
}
/**
* Test if the given parameter is a dependency to be resolved.
* @param mixed $input
* @return boolean
* @access private
*/
private function _isDependency($input)
{
return ($input instanceof Swift_ComponentReference);
}
/**
* Resolve all dependencies from ComponentReference objects into their
* appropriate instances.
* @param mixed $input
* @return mixed
* @access private
*/
private function _resolveDependencies($input)
{
if (is_array($input))
{
$ret = array();
foreach ($input as $value)
{
$ret[] = $this->_resolveDependencies($value);
}
return $ret;
}
else
{
if ($this->_isDependency($input))
{
$componentName = $input->getComponentName();
return $this->create($componentName);
}
else
{
return $input;
}
}
}
/**
* Create an instance of the given component.
* @param string $componentName
* @param mixed[] $constructorArgs, optional
* @param mixed[] Associative array of properties, optional
* @return mixed
*/
public function create($componentName, $constructorArgs = null,
$properties = null)
{
$spec = $this->getComponentSpec($componentName);
//If a pseudo-singleton is used, try to return a registered instance
// if not, reference it now
if ($spec->isSingleton())
{
if (isset($this->_singletons[$componentName]))
{
return $this->_singletons[$componentName];
}
else
{
$o = null;
$this->_singletons[$componentName] =& $o;
}
}
$className = $spec->getClassName();
//Load the class file
foreach ($this->_classLocators as $locator)
{
if ($locator->classExists($className))
{
$locator->includeClass($className);
break;
}
}
$class = new ReflectionClass($className);
//If the class has a constructor, use the constructor aguments,
// otherwise instantiate with no arguments
if ($class->getConstructor())
{
//Allow arguments to be given at runtime
if (!is_array($constructorArgs))
{
$injectedArgs = $this->_resolveDependencies(
$spec->getConstructorArgs());
}
else
{
$injectedArgs = $this->_resolveDependencies($constructorArgs);
}
$o = $class->newInstanceArgs($injectedArgs);
}
else
{
$o = $class->newInstance();
}
//Allow runtime injection of properties
if (!is_array($properties))
{
$properties = $spec->getProperties();
}
//Run setter-based injection
foreach ($properties as $key => $value)
{
$setter = 'set' . ucfirst($key);
if ($class->hasMethod($setter))
{
$injectedValue = $this->_resolveDependencies($value);
$class->getMethod($setter)->invoke($o, $injectedValue);
}
}
return $o;
}
}
I'll follow with Unit Tests