Page 1 of 2
dependency injection container
Posted: Tue May 20, 2008 4:48 pm
by koen.h
I don't get this pattern. After reading about it for 3 hours sometimes it seems like using a setter method is enough, sometimes it looks like a registry, and other times there's a whole construct that takes a complete manual to understand.
Let's take a simple case:
class Car {
private $spoiler;
public method getSpoiler() {
if (!is_object($this->spoiler)) { $this->spoiler = new SimpleSpoiler(); }
return $this->spoiler;
}
}
I want to refactor this to get rid of the instantiation dependency, using dependency injection. How would this be done?
edit: bad example
Re: dependency injection container
Posted: Tue May 20, 2008 4:55 pm
by koen.h
Re: dependency injection container
Posted: Tue May 20, 2008 5:41 pm
by Christopher
What you are showing is just compositing in a sort of Factory style way. With DI, something needs to know how to inject.
Code: Select all
class Car {
private $spoiler;
public method getSpoiler() {
return $this->spoiler;
}
}
class CarDI {
public method inject($car) {
if (!is_object($car->spoiler)) {
$car->spoiler = new SimpleSpoiler();
}
}
}
$car = new Car();
CarDI::($car);
Re: dependency injection container
Posted: Wed May 21, 2008 2:56 am
by webaddict
Here are some links that certainly helped me understanding the pattern:
Also the documentation of the Spring framework (in Java) was rather helpful.
Re: dependency injection container
Posted: Wed May 21, 2008 3:13 am
by Maugrim_The_Reaper
Understanding a pattern means knowing the problem, and how its solved.
Take the example of Zend_Config_Ini. You decide one day to use ini files exclusively and litter 40+ classes with references to Zend_Config_Ini. There are a few ways of fixing this obviously. Use a registry, use a setter, or option #3, use a DI container.
Option #3, as the theory in ZF goes, is closely related to immediate referencing of class names in controller actions - which is overwhelmingly common. But therein is the problem - what if tomorrow you decide to use Zend_Config_Xml??
With Registry/Setter - it's a quick change. With immediate class referencing in an action, it calls for editing 40+ files.
Now replicate the problem to a large application, using numerous custom or component level classes, directly referencing them in action methods. See the problem? The cost of change has now increased exponentially. Using a DI container solves the problem, since the class names causing the pain are externalised into a configuration file. It let's you reconfigure how DI builds a specific object. Granted, it's use in PHP is very limited, but it is a valid solution to a common problem in PHP programming today.
The other reason, besides making maintenance and change far easier, is that it allows testing libraries divide Controller from View. This is a vastly underestimated approach. In PHP it is rare to find anyone testing Controllers, without relying on the View as the final result. This works great - for acceptance testing - but it's not actual unit testing. The controller class is not being isolated (you are also dragging in the whole View!) and it's final result (calls to View, independent output, and Model updates) is not examined directly.
I consider this almost purely a PHP problem. With testing still the exception in PHP, PHP developers are often blind to the fact they are not unit testing when they rely on Views to tell them if Controller work. Rails on the other hand can take advantage of the fact Ruby's version of "new" is a method (who needs a DI container when you can mock the instantiation of any object

).
Does that shed a little light on why Zend_Di has so many comments attached to its proposal?
Re: dependency injection container
Posted: Wed May 21, 2008 6:31 am
by koen.h
Suppose you're enterprise application has hundreds of classes each with dependencies. In that case maintaining an xml configuration with all those dependencies doesn't seem like a scalable approach since the file will become quite large. It does seem to be awesome in solving dependency problems.
Another thing is that once you use it you can't go back. Unless you change every class that uses it. It's pretty pervasive.
Re: dependency injection container
Posted: Wed May 21, 2008 7:06 am
by Maugrim_The_Reaper
Depends on the form of configuration though

. Kick up a configuration object, serialise it, and keep a memcached daemon running and it's not half as bad as people think. In a ZF application either way, configuration objects are almost always cached in some way since it's an obvious performance issue according to xdebug.
Re: dependency injection container
Posted: Wed May 21, 2008 7:17 am
by koen.h
ZF can't rely on people having memcaching available though. If a pattern needs caching to make it worth using, isn't it a bit overkill to use it in the first place? A registry has its global problem, but at the moment I think it's the better option.
Re: dependency injection container
Posted: Wed May 21, 2008 7:47 am
by Maugrim_The_Reaper
Some people would argue PHP itself needs APC in order to work properly

.
Not all servers or hosts will have memcached or APC, but if you are building a sufficiently large application to attract visitors I think it's safe to assume you will have APC and memcached (or at least similar alternatives which bypass the actual cost caching is trying to avoid) to improve performance.
Re: dependency injection container
Posted: Wed May 21, 2008 7:50 am
by Chris Corbyn
Maybe off-topic, but here's one I wrote for Swift Mailer which supports constructor injection only for now. It's easy enough to add setter-injection though (just a few calls to setXYZ() after instantiating the object).
Code: Select all
<?php
/*
Dependency Injection container class Swift Mailer.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
//@require 'Swift/DependencyException.php';
/**
* Dependency Injection container.
* @package Swift
* @author Chris Corbyn
*/
class Swift_DependencyContainer
{
/** Constant for literal value types */
const TYPE_VALUE = 0x0001;
/** Constant for new instance types */
const TYPE_INSTANCE = 0x0010;
/** Constant for shared instance types */
const TYPE_SHARED = 0x0100;
/** Constant for aliases */
const TYPE_ALIAS = 0x1000;
/** Singleton instance */
private static $_instance = null;
/** The data container */
private $_store = array();
/** The current endpoint in the data container */
private $_endPoint;
/**
* Constructor should not be used.
* Use {@link getInstance()} instead.
*/
public function __construct() { }
/**
* Returns a singleton of the DependencyContainer.
* @return Swift_DependencyContainer
*/
public static function getInstance()
{
if (!isset(self::$_instance))
{
self::$_instance = new self();
}
return self::$_instance;
}
/**
* List the names of all items stored in the Container.
* @return array
*/
public function listItems()
{
return array_keys($this->_store);
}
/**
* Test if an item is registered in this container with the given name.
* @param string $itemName
* @return boolean
* @see register()
*/
public function has($itemName)
{
return array_key_exists($itemName, $this->_store)
&& isset($this->_store[$itemName]['lookupType']);
}
/**
* Lookup the item with the given $itemName.
* @param string $itemName
* @return mixed
* @throws Swift_DependencyException If the dependency is not found
* @see register()
*/
public function lookup($itemName)
{
if (!$this->has($itemName))
{
throw new Swift_DependencyException(
'Cannot lookup dependency "' . $itemName . '" since it is not registered.'
);
}
switch ($this->_store[$itemName]['lookupType'])
{
case self::TYPE_ALIAS:
return $this->_createAlias($itemName);
case self::TYPE_VALUE:
return $this->_getValue($itemName);
case self::TYPE_INSTANCE:
return $this->_createNewInstance($itemName);
case self::TYPE_SHARED:
return $this->_createSharedInstance($itemName);
}
}
/**
* Create an array of arguments passed to the constructor of $itemName.
* @param string $itemName
* @return array
*/
public function createDependenciesFor($itemName)
{
$args = array();
if (isset($this->_store[$itemName]['args']))
{
$args = $this->_resolveArgs($this->_store[$itemName]['args']);
}
return $args;
}
/**
* Register a new dependency with $itemName.
* This method returns the current DependencyContainer instance because it
* requires the use of the fluid interface to set the specific details for the
* dependency.
*
* @param string $itemName
* @return Swift_DependencyContainer
* @see asNewInstanceOf(), asSharedInstanceOf(), asValue()
*/
public function register($itemName)
{
$this->_store[$itemName] = array();
$this->_endPoint =& $this->_store[$itemName];
return $this;
}
/**
* Specify the previously registered item as a literal value.
* {@link register()} must be called before this will work.
*
* @param mixed $value
* @return Swift_DependencyContainer
*/
public function asValue($value)
{
$endPoint =& $this->_getEndPoint();
$endPoint['lookupType'] = self::TYPE_VALUE;
$endPoint['value'] = $value;
return $this;
}
/**
* Specify the previously registered item as an alias of another item.
* @param string $lookup
* @return Swift_DependencyContainer
*/
public function asAliasOf($lookup)
{
$endPoint =& $this->_getEndPoint();
$endPoint['lookupType'] = self::TYPE_ALIAS;
$endPoint['ref'] = $lookup;
return $this;
}
/**
* Specify the previously registered item as a new instance of $className.
* {@link register()} must be called before this will work.
* Any arguments can be set with {@link withDependencies()},
* {@link addConstructorValue()} or {@link addConstructorLookup()}.
*
* @param string $className
* @return Swift_DependencyContainer
* @see withDependencies(), addConstructorValue(), addConstructorLookup()
*/
public function asNewInstanceOf($className)
{
$endPoint =& $this->_getEndPoint();
$endPoint['lookupType'] = self::TYPE_INSTANCE;
$endPoint['className'] = $className;
return $this;
}
/**
* Specify the previously registered item as a shared instance of $className.
* {@link register()} must be called before this will work.
* @param string $className
* @return Swift_DependencyContainer
*/
public function asSharedInstanceOf($className)
{
$endPoint =& $this->_getEndPoint();
$endPoint['lookupType'] = self::TYPE_SHARED;
$endPoint['className'] = $className;
return $this;
}
/**
* Specify a list of injected dependencies for the previously registered item.
* This method takes an array of lookup names.
*
* @param array $lookups
* @return Swift_DependencyContainer
* @see addConstructorValue(), addConstructorLookup()
*/
public function withDependencies(array $lookups)
{
$endPoint =& $this->_getEndPoint();
$endPoint['args'] = array();
foreach ($lookups as $lookup)
{
$this->addConstructorLookup($lookup);
}
return $this;
}
/**
* Specify a literal (non looked up) value for the constructor of the
* previously registered item.
*
* @param mixed $value
* @return Swift_DependencyContainer
* @see withDependencies(), addConstructorLookup()
*/
public function addConstructorValue($value)
{
$endPoint =& $this->_getEndPoint();
if (!isset($endPoint['args']))
{
$endPoint['args'] = array();
}
$endPoint['args'][] = array('type' => 'value', 'item' => $value);
return $this;
}
/**
* Specify a dependency lookup for the constructor of the previously
* registered item.
*
* @param string $lookup
* @return Swift_DependencyContainer
* @see withDependencies(), addConstructorValue()
*/
public function addConstructorLookup($lookup)
{
$endPoint =& $this->_getEndPoint();
if (!isset($this->_endPoint['args']))
{
$endPoint['args'] = array();
}
$endPoint['args'][] = array('type' => 'lookup', 'item' => $lookup);
return $this;
}
// -- Private methods
/** Get the literal value with $itemName */
private function _getValue($itemName)
{
return $this->_store[$itemName]['value'];
}
/** Resolve an alias to another item */
private function _createAlias($itemName)
{
return $this->lookup($this->_store[$itemName]['ref']);
}
/** Create a fresh instance of $itemName */
private function _createNewInstance($itemName)
{
$reflector = new ReflectionClass($this->_store[$itemName]['className']);
if ($reflector->getConstructor())
{
return $reflector->newInstanceArgs(
$this->createDependenciesFor($itemName)
);
}
else
{
return $reflector->newInstance();
}
}
/** Create and register a shared instance of $itemName */
private function _createSharedInstance($itemName)
{
if (!isset($this->_store[$itemName]['instance']))
{
$this->_store[$itemName]['instance'] = $this->_createNewInstance($itemName);
}
return $this->_store[$itemName]['instance'];
}
/** Get the current endpoint in the store */
private function &_getEndPoint()
{
if (!isset($this->_endPoint))
{
throw new BadMethodCallException(
'Component must first be registered by calling register()'
);
}
return $this->_endPoint;
}
/** Get an argument list with dependencies resolved */
private function _resolveArgs(array $args)
{
$resolved = array();
foreach ($args as $argDefinition)
{
switch ($argDefinition['type'])
{
case 'lookup':
$resolved[] = $this->_lookupRecursive($argDefinition['item']);
break;
case 'value':
$resolved[] = $argDefinition['item'];
break;
}
}
return $resolved;
}
/** Resolve a single dependency with an collections */
private function _lookupRecursive($item)
{
if (is_array($item))
{
$collection = array();
foreach ($item as $k => $v)
{
$collection[$k] = $this->_lookupRecursive($v);
}
return $collection;
}
else
{
return $this->lookup($item);
}
}
}
Dependency maps are specified like this:
Code: Select all
<?php
Swift_DependencyContainer::getInstance()
-> register('cache')
-> asAliasOf('cache.array')
-> register('cache.array')
-> asSharedInstanceOf('Swift_KeyCache_ArrayKeyCache')
-> withDependencies(array('cache.inputstream'))
-> register('cache.inputstream')
-> asNewInstanceOf('Swift_KeyCache_SimpleKeyCacheInputStream')
;
And instances are created like this:
Code: Select all
$cache = Swift_DependencyContainer::getInstance()->lookup('cache');
The main methods to look at are:
- register() (and its friends asValue(), asAliasOf(), asNewInstanceOf(), asSharedInstanceOf() and withDependencies())
- lookup()
- createDependenciesFor()
EDIT | Unit test for the above:
Code: Select all
<?php
require_once 'Swift/Tests/SwiftUnitTestCase.php';
require_once 'Swift/DependencyContainer.php';
class One {
public $arg1, $arg2;
public function __construct($arg1 = null, $arg2 = null) {
$this->arg1 = $arg1;
$this->arg2 = $arg2;
}
}
class Swift_DependencyContainerTest extends Swift_Tests_SwiftUnitTestCase
{
private $_container;
public function setUp()
{
$this->_container = new Swift_DependencyContainer();
}
public function testRegisterAndLookupValue()
{
$this->_container->register('foo')->asValue('bar');
$this->assertIdentical('bar', $this->_container->lookup('foo'));
}
public function testHasReturnsTrueForRegisteredValue()
{
$this->_container->register('foo')->asValue('bar');
$this->assertTrue($this->_container->has('foo'));
}
public function testHasReturnsFalseForUnregisteredValue()
{
$this->assertFalse($this->_container->has('foo'));
}
public function testRegisterAndLookupNewInstance()
{
$this->_container->register('one')->asNewInstanceOf('One');
$this->assertIsA($this->_container->lookup('one'), 'One');
}
public function testHasReturnsTrueForRegisteredInstance()
{
$this->_container->register('one')->asNewInstanceOf('One');
$this->assertTrue($this->_container->has('one'));
}
public function testNewInstanceIsAlwaysNew()
{
$this->_container->register('one')->asNewInstanceOf('One');
$a = $this->_container->lookup('one');
$b = $this->_container->lookup('one');
$this->assertClone($a, $b);
}
public function testRegisterAndLookupSharedInstance()
{
$this->_container->register('one')->asSharedInstanceOf('One');
$this->assertIsA($this->_container->lookup('one'), 'One');
}
public function testHasReturnsTrueForSharedInstance()
{
$this->_container->register('one')->asSharedInstanceOf('One');
$this->assertTrue($this->_container->has('one'));
}
public function testMultipleSharedInstancesAreSameInstance()
{
$this->_container->register('one')->asSharedInstanceOf('One');
$a = $this->_container->lookup('one');
$b = $this->_container->lookup('one');
$this->assertSame($a, $b);
}
public function testNewInstanceWithDependencies()
{
$this->_container->register('foo')->asValue('FOO');
$this->_container->register('one')->asNewInstanceOf('One')
->withDependencies(array('foo'));
$obj = $this->_container->lookup('one');
$this->assertIdentical('FOO', $obj->arg1);
}
public function testNewInstanceWithMultipleDependencies()
{
$this->_container->register('foo')->asValue('FOO');
$this->_container->register('bar')->asValue(42);
$this->_container->register('one')->asNewInstanceOf('One')
->withDependencies(array('foo', 'bar'));
$obj = $this->_container->lookup('one');
$this->assertIdentical('FOO', $obj->arg1);
$this->assertIdentical(42, $obj->arg2);
}
public function testNewInstanceWithInjectedObjects()
{
$this->_container->register('foo')->asValue('FOO');
$this->_container->register('one')->asNewInstanceOf('One');
$this->_container->register('two')->asNewInstanceOf('One')
->withDependencies(array('one', 'foo'));
$obj = $this->_container->lookup('two');
$this->assertClone($this->_container->lookup('one'), $obj->arg1);
$this->assertIdentical('FOO', $obj->arg2);
}
public function testNewInstanceWithAddConstructorValue()
{
$this->_container->register('one')->asNewInstanceOf('One')
->addConstructorValue('x')
->addConstructorValue(99);
$obj = $this->_container->lookup('one');
$this->assertIdentical('x', $obj->arg1);
$this->assertIdentical(99, $obj->arg2);
}
public function testNewInstanceWithAddConstructorLookup()
{
$this->_container->register('foo')->asValue('FOO');
$this->_container->register('bar')->asValue(42);
$this->_container->register('one')->asNewInstanceOf('One')
->addConstructorLookup('foo')
->addConstructorLookup('bar');
$obj = $this->_container->lookup('one');
$this->assertIdentical('FOO', $obj->arg1);
$this->assertIdentical(42, $obj->arg2);
}
public function testResolvedDependenciesCanBeLookedUp()
{
$this->_container->register('foo')->asValue('FOO');
$this->_container->register('one')->asNewInstanceOf('One');
$this->_container->register('two')->asNewInstanceOf('One')
->withDependencies(array('one', 'foo'));
$deps = $this->_container->createDependenciesFor('two');
$this->assertEqual(
array($this->_container->lookup('one'), 'FOO'), $deps
);
}
public function testArrayOfDependenciesCanBeSpecified()
{
$this->_container->register('foo')->asValue('FOO');
$this->_container->register('one')->asNewInstanceOf('One');
$this->_container->register('two')->asNewInstanceOf('One')
->withDependencies(array(array('one', 'foo'), 'foo'));
$obj = $this->_container->lookup('two');
$this->assertEqual(array($this->_container->lookup('one'), 'FOO'), $obj->arg1);
$this->assertIdentical('FOO', $obj->arg2);
}
public function testAliasCanBeSet()
{
$this->_container->register('foo')->asValue('FOO');
$this->_container->register('bar')->asAliasOf('foo');
$this->assertIdentical('FOO', $this->_container->lookup('bar'));
}
public function testAliasOfAliasCanBeSet()
{
$this->_container->register('foo')->asValue('FOO');
$this->_container->register('bar')->asAliasOf('foo');
$this->_container->register('zip')->asAliasOf('bar');
$this->_container->register('button')->asAliasOf('zip');
$this->assertIdentical('FOO', $this->_container->lookup('button'));
}
}
Re: dependency injection container
Posted: Wed May 21, 2008 9:36 am
by koen.h
Too bad apc is not enabled by default in php6.
Chris, I saw your work in another forum while researching. I'll look over it again. Maybe the whole thing must sink in some more before I see a brighter light of this pattern.
Re: dependency injection container
Posted: Wed May 21, 2008 11:09 am
by Christopher
koen.h wrote:Suppose you're enterprise application has hundreds of classes each with dependencies. In that case maintaining an xml configuration with all those dependencies doesn't seem like a scalable approach since the file will become quite large. It does seem to be awesome in solving dependency problems.
You jumped a little too quickly to optimization. The whole point of things like Front Controllers is to be able to centrally manage problems like this. With PHP, an increase in application size does not simply mean that "the file" gets bigger. There are lots of solutions to break up the problem into manageable pieces -- that is exactly what an Action Controller does.
koen.h wrote:Another thing is that once you use it you can't go back. Unless you change every class that uses it. It's pretty pervasive.
Because DI requires no code inside classes to do the injection, you can always go back. Doing the configuration manually within the class is always an option. Conversely, if you don't use DI then configuration code does become pervasive.
Re: dependency injection container
Posted: Thu May 22, 2008 9:37 pm
by Ambush Commander
Hey Chris, I noticed you were using:
Code: Select all
/** Constant for literal value types */
const TYPE_VALUE = 0x0001;
/** Constant for new instance types */
const TYPE_INSTANCE = 0x0010;
/** Constant for shared instance types */
const TYPE_SHARED = 0x0100;
/** Constant for aliases */
const TYPE_ALIAS = 0x1000;
Shouldn't it be 0x01, 0x02, 0x04, 0x08, 0x10, etc...?
To be more on topic, I've been thinking of using dependency injectors more extensively in HTML Purifier, so I'll be paying close attention this topic.

Re: dependency injection container
Posted: Fri May 23, 2008 1:22 am
by Chris Corbyn
Ambush Commander wrote:Hey Chris, I noticed you were using:
Code: Select all
/** Constant for literal value types */
const TYPE_VALUE = 0x0001;
/** Constant for new instance types */
const TYPE_INSTANCE = 0x0010;
/** Constant for shared instance types */
const TYPE_SHARED = 0x0100;
/** Constant for aliases */
const TYPE_ALIAS = 0x1000;
Shouldn't it be 0x01, 0x02, 0x04, 0x08, 0x10, etc...?
I just figured it's easy to read, but yeah the increments are larger than they need to be. It doesn't hurt anything here though. I seem to have gotten into a habit of doing this lately. I used to just do 1, 2, 4, 8, 16, 32 etc etc.
Re: dependency injection container
Posted: Fri May 23, 2008 2:16 am
by Christopher
Chris Corbyn wrote:I just figured it's easy to read,
Well you're all that matters ... you're the only one who should ever read the values ... that's why they are constants!
