Page 1 of 1

Dependency Injection Container

Posted: Mon Nov 05, 2007 9:41 am
by Chris Corbyn
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.

Code: Select all

class Car {
  private $_engine;
  public function __construct() {
    $this->_engine = new SomeEngine();
  }
}

$car = new Car();
We could resolve that by passing the dependency in the constructor (dependency injection):

Code: Select all

class Car {
  private $_engine;
  public function __construct(Engine $engine) {
    $this->_engine = $engine;
  }
}

$car = new Car(new SomeEngine());
Or we could pass it via a public setter method:

Code: Select all

class Car {
  private $engine;
  public function setEngine(Engine $engine) {
    $this->_engine = $engine;
  }
}

$car = new Car();
$car->setEngine($engine);
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):

Image

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>
Here, if we asked for a car we'd get a CustomCar with a FiatEngine using ItalianParts. Something like:

Code: Select all

$car = $factory->create('car');
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

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;
  }
  
}
Swift_ComponentReference.php

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;
  }
  
}
Swift_ClassLocator.php

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);
  
}
Swift_ComponentSpecFinder.php

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);
  
}
Swift_ComponentSpecFinder_XmlSpecFinder.php

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;
    }
  }
  
}
Swift_ComponentFactory.php

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;
  }
  
}
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 ;)

Posted: Mon Nov 05, 2007 9:43 am
by Chris Corbyn
Unit Tests

Swift_ComponentSpecTest.php

Code: Select all

<?php

require_once dirname(__FILE__) . '/../../config.php';
require_once LIB_PATH . '/Swift/ComponentSpec.php';

class Swift_ComponentSpecTest extends UnitTestCase
{
  
  private $_spec;
  
  public function setUp()
  {
    $this->_spec = new Swift_ComponentSpec();
  }
  
  public function testSetAndGetClassName()
  {
    $this->_spec->setClassName('EmptyClass');
    $this->assertEqual('EmptyClass', $this->_spec->getClassName());
  }
  
  public function testSetAndGetConstructorArgs()
  {
    $o = new stdClass();
    $this->_spec->setConstructorArgs(array($o, 'foo', 123));
    $this->assertIdentical(array($o, 'foo', 123),
      $this->_spec->getConstructorArgs());
  }
  
  public function testSetAndGetProperty()
  {
    $this->_spec->setProperty('propName1', 'value');
    $this->assertIdentical('value', $this->_spec->getProperty('propName1'));
    
    $o = new stdClass();
    $o->foo = 'bar';
    
    $this->_spec->setProperty('propName2', $o);
    $this->assertIdentical($o, $this->_spec->getProperty('propName2'));
    
    $this->_spec->setProperty('propName3', array('one', 2, '3'));
    $this->assertIdentical(array('one', 2, '3'),
      $this->_spec->getProperty('propName3'));
  }
  
  public function testGetProperties()
  {
    $this->_spec->setProperty('testProp1', 'x');
    $this->_spec->setProperty('testProp2', 'y');
    $this->_spec->setProperty('testProp3', 'z');
    
    $this->assertEqual(array('testProp1'=>'x', 'testProp2'=>'y', 'testProp3'=>'z'),
      $this->_spec->getProperties());
  }
  
  public function testSetAndGetSingleton()
  {
    $this->assertFalse($this->_spec->isSingleton(),
      'Singletons should be off by default');
    
    $this->_spec->setSingleton(true);
    $this->assertTrue($this->_spec->isSingleton(),
      'Singleton should be turned on');
    
    $this->_spec->setSingleton(false);
    $this->assertFalse($this->_spec->isSingleton(),
      'Singleton should be turned off');
  }
  
}
Swift_ComponentReferenceTest.php

Code: Select all

<?php

require_once dirname(__FILE__) . '/../../config.php';
require_once LIB_PATH . '/Swift/ComponentReference.php';

class Swift_ComponentReferenceTest extends UnitTestCase
{
  
  public function testGetComponentName()
  {
    $ref = new Swift_ComponentReference('test');
    $this->assertEqual('test', $ref->getComponentName());
    
    $ref = new Swift_ComponentReference('other');
    $this->assertEqual('other', $ref->getComponentName());
  }
  
}
Swift_ComponentSpecFinder_AbstractSpecFinderTest.php

Code: Select all

<?php

require_once dirname(__FILE__) . '/../../../config.php';
require_once LIB_PATH . '/Swift/ComponentSpec.php';
require_once LIB_PATH . '/Swift/ComponentFactory.php';

abstract class Swift_ComponentSpecFinder_AbstractSpecFinderTest
  extends UnitTestCase
{
  
  protected $_finder;
  protected $_factory;
  
  public function setUp()
  {
    $this->_finder = $this->getFinder();
    $this->_factory = $this->getFactory();
  }
  
  abstract public function getFactory();
  
  abstract public function getFinder();
  
  public function testBasicSpecFinding()
  {
    $spec = $this->_finder->findSpecFor('empty', $this->_factory);
    
    $this->assertIsA($spec, 'Swift_ComponentSpec');
    $this->assertEqual('EmptyClass', $spec->getClassName());
  }
  
  public function testSingletonSpecFinding()
  {
    $spec = $this->_finder->findSpecFor('singletonComponent', $this->_factory);
    
    $this->assertIsA($spec, 'Swift_ComponentSpec');
    $this->assertEqual('stdClass', $spec->getClassName());
    $this->assertTrue($spec->isSingleton(),
      'Specification should be for a singleton');
  }
  
  public function testSetterBasedInjectionSpecFinding()
  {
    $spec = $this->_finder->findSpecFor('setterBased', $this->_factory);
    
    $this->assertIsA($spec, 'Swift_ComponentSpec');
    $this->assertEqual('SetterInjectionClass', $spec->getClassName());
    $prop1 = $spec->getProperty('prop1');
    $this->assertTrue(is_array($prop1), 'Property prop1 should be a collection');
    $this->assertIsA($prop1[0], 'Swift_ComponentReference');
    $this->assertEqual('empty', $prop1[0]->getComponentName());
    $this->assertIsA($prop1[1], 'Swift_ComponentReference');
    $this->assertEqual('singletonComponent', $prop1[1]->getComponentName());
    $this->assertEqual('test', $spec->getProperty('prop2'));
  }
  
  public function testConstructorBasedInjectionSpecFinding()
  {
    $spec = $this->_finder->findSpecFor('constructorBased', $this->_factory);
    
    $this->assertIsA($spec, 'Swift_ComponentSpec');
    $this->assertEqual('ConstructorInjectionClass', $spec->getClassName());
    $constructorArgs = $spec->getConstructorArgs();
    $this->assertTrue(is_array($constructorArgs),
      'Constructor arguments should be an array');
    $this->assertEqual('foo', $constructorArgs[0]);
    $this->assertTrue(is_array($constructorArgs[1]),
      'Argument 2 in constructor should be a collection');
    $this->assertEqual('bar', $constructorArgs[1][0]);
    $this->assertEqual('test', $constructorArgs[1][1]);
  }
  
  public function testDefaultTypeIsString()
  {
    $spec = $this->_finder->findSpecFor('constructorBased', $this->_factory);
    
    $this->assertIsA($spec, 'Swift_ComponentSpec');
    $this->assertEqual('ConstructorInjectionClass', $spec->getClassName());
    $constructorArgs = $spec->getConstructorArgs();
    $this->assertTrue(is_array($constructorArgs),
      'Constructor arguments should be an array');
    $this->assertTrue(is_string($constructorArgs[0]),
      'Type should default to string');
    $this->assertTrue(is_array($constructorArgs[1]),
      'Argument 2 in constructor should be a collection');
    $this->assertTrue(is_string($constructorArgs[1][0]),
      'Type should default to string');
    $this->assertTrue(is_string($constructorArgs[1][1]),
      'Type should default to string');
  }
  
  public function testIntegerType()
  {
    $spec = $this->_finder->findSpecFor('constructorBased', $this->_factory);
    
    $this->assertIsA($spec, 'Swift_ComponentSpec');
    $this->assertEqual('ConstructorInjectionClass', $spec->getClassName());
    $constructorArgs = $spec->getConstructorArgs();
    $this->assertTrue(is_array($constructorArgs),
      'Constructor arguments should be an array');
    $this->assertTrue(is_array($constructorArgs[1]),
      'Argument 2 in constructor should be a collection');
    $this->assertTrue(is_integer($constructorArgs[1][2]),
      'Integer value should be honoured');
    $this->assertTrue(is_integer($constructorArgs[1][3]),
      'Integer value should be honoured');
  }
  
  public function testFloatType()
  {
    $spec = $this->_finder->findSpecFor('constructorBased', $this->_factory);
    
    $this->assertIsA($spec, 'Swift_ComponentSpec');
    $this->assertEqual('ConstructorInjectionClass', $spec->getClassName());
    $constructorArgs = $spec->getConstructorArgs();
    $this->assertTrue(is_array($constructorArgs),
      'Constructor arguments should be an array');
    $this->assertTrue(is_array($constructorArgs[1]),
      'Argument 2 in constructor should be a collection');
    $this->assertTrue(is_float($constructorArgs[1][4]),
      'Float value should be honoured');
  }
  
  public function testNullIsReturnedOnFailure()
  {
     $this->assertNull($this->_finder->findSpecFor('nothing', $this->_factory));
  }
  
}
Swift_ComponentSpecFinder_XmlSpecFinderTest.php

Code: Select all

<?php

require_once dirname(__FILE__) . '/../../../config.php';
require_once dirname(__FILE__) . '/AbstractSpecFinderTest.php';
require_once LIB_PATH . '/Swift/ComponentSpecFinder/XmlSpecFinder.php';
require_once LIB_PATH . '/Swift/ComponentFactory.php';

class Swift_ComponentSpecFinder_XmlSpecFinderTest
  extends Swift_ComponentSpecFinder_AbstractSpecFinderTest
{
  
  public function getFactory()
  {
    return new Swift_ComponentFactory();
  }
  
  public function getFinder()
  {
    $xml =
    '<?xml version="1.0" ?>' .
    '<components>' .
    
    '  <component>' .
    '    <name>empty</name>' .
    '    <className>EmptyClass</className>' .
    '  </component>' .
    
    '  <component>' .
    '    <name>singletonComponent</name>' .
    '    <className>stdClass</className>' .
    '    <singleton>true</singleton>' .
    '  </component>' .
    
    '  <component>' .
    '    <name>setterBased</name>' .
    '    <className>SetterInjectionClass</className>' .
    '    <properties>' .
    '      <property>' .
    '        <key>prop1</key>' .
    '        <collection>' .
    '          <componentRef>empty</componentRef>' .
    '          <componentRef>singletonComponent</componentRef>' .
    '        </collection>' .
    '      </property>' .
    '      <property>' .
    '        <key>prop2</key>' .
    '        <value>test</value>' .
    '      </property>' .
    '    </properties>' .
    '  </component>' .
    
    '  <component>' .
    '    <name>constructorBased</name>' .
    '    <className>ConstructorInjectionClass</className>' .
    '    <constructor>' .
    '      <arg>' .
    '        <value>foo</value>' .
    '      </arg>' .
    '      <arg>' .
    '        <collection>' .
    '          <value>bar</value>' .
    '          <value>test</value>' .
    '          <value type="integer">100</value>' .
    '          <value type="int">2</value>' .
    '          <value type="float">0.5</value>' .
    '        </collection>' .
    '      </arg>' .
    '    </constructor>' .
    '  </component>' .
    
    '</components>';
    
    return new Swift_ComponentSpecFinder_XmlSpecFinder($xml);
  }
  
}
Swift_ComponentFactoryTest.php

Code: Select all

<?php

require_once dirname(__FILE__) . '/../../config.php';
require_once LIB_PATH . '/Swift/ComponentFactory.php';
require_once LIB_PATH . '/Swift/ComponentSpec.php';
require_once LIB_PATH . '/Swift/ComponentReference.php';
require_once LIB_PATH . '/Swift/ClassLocator.php';
require_once LIB_PATH . '/Swift/ComponentSpecFinder.php';
require_once LIB_PATH . '/Swift/ComponentFactoryException.php';
require_once dirname(__FILE__) . '/../../classes/EmptyClass.php';
require_once dirname(__FILE__) . '/../../classes/EmptyInterface.php';
require_once dirname(__FILE__) . '/../../classes/ConstructorInjectionClass.php';
require_once dirname(__FILE__) . '/../../classes/SetterInjectionClass.php';

Mock::generate('Swift_ClassLocator', 'MockClassLocator');
Mock::generate('Swift_ComponentSpecFinder', 'MockSpecFinder');

class Swift_ComponentFactoryTest extends UnitTestCase
{
  
  private $_factory;
  
  public function setUp()
  {
    $this->_factory = new Swift_ComponentFactory();
  }
  
  public function testNewComponentSpec()
  {
    $spec = $this->_factory->newComponentSpec();
    $this->assertIsA($spec, 'Swift_ComponentSpec');
  }
  
  public function testReferenceFor()
  {
    $ref = $this->_factory->referenceFor('test');
    $this->assertIsA($ref, 'Swift_ComponentReference');
    $this->assertEqual('test', $ref->getComponentName());
  }
  
  public function testSetAndGetComponentSpec()
  {
    $spec = $this->_factory->newComponentSpec();
    $spec->setClassName('stdClass');
    
    $this->_factory->setComponentSpec('testClass', $spec);
    $this->assertIdentical($spec,
      $this->_factory->getComponentSpec('testClass'));
  }
  
  public function testClassLocatorStrategy()
  {
    $locator1 = new MockClassLocator();
    $locator1->expectOnce('classExists');
    $locator1->setReturnValue('classExists', false);
    $locator1->expectNever('includeClass');
    
    $locator2 = new MockClassLocator();
    $locator2->expectOnce('classExists');
    $locator2->setReturnValue('classExists', true);
    $locator2->expectOnce('includeClass');
    
    $spec = $this->_factory->newComponentSpec();
    $spec->setClassName('stdClass');
    
    $this->_factory->setComponentSpec('testClass', $spec);
    
    $this->_factory->registerClassLocator('one', $locator1);
    $this->_factory->registerClassLocator('two', $locator2);
    
    $o = $this->_factory->create('testClass');
  }
  
  public function testCreateReturnsCorrectType()
  {
    $spec = $this->_factory->newComponentSpec();
    $spec->setClassName('EmptyClass');
    
    $this->_factory->setComponentSpec('testClass', $spec);
    
    $o = $this->_factory->create('testClass');
    
    $this->assertIsA($o, 'EmptyClass');
    $this->assertIsA($o, 'EmptyInterface');
  }
  
  public function testConstructorBasedInjectionByValue()
  {
    $spec = $this->_factory->newComponentSpec();
    $spec->setClassName('ConstructorInjectionClass');
    $spec->setConstructorArgs(array('foo', 'bar'));
    
    $this->_factory->setComponentSpec('constructorClass', $spec);
    
    $o = $this->_factory->create('constructorClass');
    
    $this->assertIsA($o, 'ConstructorInjectionClass');
    $this->assertEqual('foo', $o->getProp1());
    $this->assertEqual('bar', $o->getProp2());
  }
  
  public function testSetterBasedInjectionByValue()
  {
    $spec = $this->_factory->newComponentSpec();
    $spec->setClassName('SetterInjectionClass');
    $spec->setProperty('prop1', 'foo');
    $spec->setProperty('prop2', 'bar');
    
    $this->_factory->setComponentSpec('setterClass', $spec);
    
    $o = $this->_factory->create('setterClass');
    
    $this->assertIsA($o, 'SetterInjectionClass');
    $this->assertEqual('foo', $o->getProp1());
    $this->assertEqual('bar', $o->getProp2());
  }
  
  public function testConstructorBasedDependencyInjection()
  {
    $emptyClassSpec = $this->_factory->newComponentSpec();
    $emptyClassSpec->setClassName('EmptyClass');
    
    $setterClassSpec = $this->_factory->newComponentSpec();
    $setterClassSpec->setClassName('SetterInjectionClass');
    $setterClassSpec->setProperty('prop1', 'one');
    $setterClassSpec->setProperty('prop2', 'two');
    
    $diSpec = $this->_factory->newComponentSpec();
    $diSpec->setClassName('ConstructorInjectionClass');
    $diSpec->setConstructorArgs(array(
      $this->_factory->referenceFor('emptyClass'),
      $this->_factory->referenceFor('setterClass')
    ));
    
    $this->_factory->setComponentSpec('emptyClass', $emptyClassSpec);
    $this->_factory->setComponentSpec('setterClass', $setterClassSpec);
    $this->_factory->setComponentSpec('testClass', $diSpec);
    
    $o = $this->_factory->create('testClass');
    
    $this->assertIsA($o, 'ConstructorInjectionClass');
    $this->assertIsA($o->getProp1(), 'EmptyClass');
    $this->assertIsA($o->getProp2(), 'SetterInjectionClass');
    
    $prop2 = $o->getProp2();
    
    $this->assertEqual('one', $prop2->getProp1());
    $this->assertEqual('two', $prop2->getProp2());
  }
  
  public function testSetterBasedDependencyInjection()
  {
    $emptyClassSpec = $this->_factory->newComponentSpec();
    $emptyClassSpec->setClassName('EmptyClass');
    
    $constructorClassSpec = $this->_factory->newComponentSpec();
    $constructorClassSpec->setClassName('ConstructorInjectionClass');
    $constructorClassSpec->setConstructorArgs(array(123, 456));
    
    $diSpec = $this->_factory->newComponentSpec();
    $diSpec->setClassName('SetterInjectionClass');
    $diSpec->setProperty('prop1', $this->_factory->referenceFor('emptyClass'));
    $diSpec->setProperty('prop2', $this->_factory
      ->referenceFor('constructorClass'));
    
    $this->_factory->setComponentSpec('emptyClass', $emptyClassSpec);
    $this->_factory->setComponentSpec('constructorClass', $constructorClassSpec);
    $this->_factory->setComponentSpec('testClass', $diSpec);
    
    $o = $this->_factory->create('testClass');
    
    $this->assertIsA($o, 'SetterInjectionClass');
    $this->assertIsA($o->getProp1(), 'EmptyClass');
    $this->assertIsA($o->getProp2(), 'ConstructorInjectionClass');
    
    $prop2 = $o->getProp2();
    
    $this->assertEqual(123, $prop2->getProp1());
    $this->assertEqual(456, $prop2->getProp2());
  }
  
  public function testRuntimeConstructorArgInjection()
  {
    $spec = $this->_factory->newComponentSpec();
    $spec->setClassName('ConstructorInjectionClass');
    $spec->setConstructorArgs(array('foo', 'bar'));
    
    $this->_factory->setComponentSpec('test', $spec);
    
    $o = $this->_factory->create('test', array('x', 'y'));
    
    $this->assertIsA($o, 'ConstructorInjectionClass');
    $this->assertEqual('x', $o->getProp1());
    $this->assertEqual('y', $o->getProp2());
  }
  
  public function testRuntimeSetterInjection()
  {
    $spec = $this->_factory->newComponentSpec();
    $spec->setClassName('SetterInjectionClass');
    $spec->setProperty('prop1', 'foo');
    $spec->setProperty('prop2', 'bar');
    
    $this->_factory->setComponentSpec('test', $spec);
    
    $o = $this->_factory->create('test', null, array('prop1'=>'x', 'prop2'=>'y'));
    
    $this->assertIsA($o, 'SetterInjectionClass');
    $this->assertEqual('x', $o->getProp1());
    $this->assertEqual('y', $o->getProp2());
  }
  
  public function testSingleton()
  {
    $spec = $this->_factory->newComponentSpec();
    $spec->setClassName('stdClass');
    $spec->setSingleton(true);
    
    $this->_factory->setComponentSpec('test', $spec);
    
    $o1 = $this->_factory->create('test');
    $o2 = $this->_factory->create('test');
    
    $this->assertReference($o1, $o2);
  }
  
  public function testExceptionThrownForBadComponentName()
  {
    try
    {
      $o = $this->_factory->create('noSuchComponent');
      $this->fail('An exception should have been thrown because a component ' .
        'named noSuchComponent is not registered.');
    }
    catch (Swift_ComponentFactoryException $e)
    {
      $this->pass();
    }
  }
  
  public function testSpecFinderStrategy()
  {
    $spec = $this->_factory->newComponentSpec();
    $spec->setClassName('stdClass');
    
    $finder1 = new MockSpecFinder();
    $finder1->setReturnValue('findSpecFor', null);
    $finder1->expectOnce('findSpecFor', array(
      'testComponent', new ReferenceExpectation($this->_factory)
    ));
    
    $finder2 = new MockSpecFinder();
    $finder2->setReturnValue('findSpecFor', $spec);
    $finder2->expectOnce('findSpecFor', array(
      'testComponent', new ReferenceExpectation($this->_factory)
    ));
    
    //Strategy should already have loaded $spec
    $finder3 = new MockSpecFinder();
    $finder3->setReturnValue('findSpecFor', null);
    $finder3->expectNever('findSpecFor');
    
    $this->_factory->registerSpecFinder('finder1', $finder1);
    $this->_factory->registerSpecFinder('finder2', $finder2);
    $this->_factory->registerSpecFinder('finder3', $finder3);
    
    $o = $this->_factory->create('testComponent');
    
    $this->assertIsA($o, 'stdClass');
  }

}
I have the classes for YAML (using syck) and standard PHP array spec-finder implementations too but the format is less readable so I need to work on them a bit.

Posted: Mon Nov 05, 2007 12:18 pm
by Jenk
This not of any help?

viewtopic.php?t=55387

Posted: Mon Nov 05, 2007 1:23 pm
by Christopher
A couple of questions. Why is this part of Swift and not a standalone library? It seems like it could be. Also, have you checked the speed difference between reflection and eval. Using reflection is cleaner, but as I recall there used to be a hefty penalty for using it.

It is interesting, I use XML very similar to yours for a minimal ORM library I have. It gives me the idea to split it into an Injection library and build the ORM on top of that to fill in the objects.

Posted: Mon Nov 05, 2007 3:51 pm
by Chris Corbyn
Thanks for the feedback. It started out as something Swift is going to use in the next version but as things have progressed it is starting to look like I'll fork it as a separate project. This is the initial draft of the code, I went with the most intuitive implementaion and haven't optimized for speed yet. I'm almost sure changing the variable-method names instead of RefelctionMethod->invoke() for starters would speed things up.