SitePoint Sponsor

User Tag List

Page 1 of 2 12 LastLast
Results 1 to 25 of 34
  1. #1
    Resident Code Monkey Chris Corbyn's Avatar
    Join Date
    Nov 2005
    Location
    Melbourne, Australia
    Posts
    713
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)

    Critique: Dependency Injection Container

    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 php:
    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 php:
    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 php:
    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):



    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 xml:
    <?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 php:
    $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 php:
    <?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 php:
    <?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 php:
    <?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 php:
    <?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 php:
    <?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 php:
    <?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
    Last edited by Chris Corbyn; Nov 5, 2007 at 08:46. Reason: Typo with BBCode tags

  2. #2
    Resident Code Monkey Chris Corbyn's Avatar
    Join Date
    Nov 2005
    Location
    Melbourne, Australia
    Posts
    713
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Unit Tests

    Swift_ComponentSpecTest.php
    Code php:
    <?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 php:
    <?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 php:
    <?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 php:
    <?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 php:
    <?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.

  3. #3
    SitePoint Wizard silver trophy kyberfabrikken's Avatar
    Join Date
    Jun 2004
    Location
    Copenhagen, Denmark
    Posts
    6,157
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Funny. I just finished the last touches on a di container/factory, which I have been nursing for a while now. I'd be interested in hearing, why you didn't go with picophp or phemto? I know why I didn't, but I'd like to hear if your reasons are similar.
    I'd also like to hear, why you ended up with a configuration file? I had that for a while, but ditched it, because I don't think it adds any value and in fact makes the whole construction more opaque.
    Finally - Do you have concrete requirements for setter injection, or is it just a nice-to-have feature?

    Regarding the code (You did ask for critique), I have a few random observations.
    Spec is a bit vague -- Wouldn't ClassFactory be more appropriate?
    Why is it the Spec which knows if it's a singleton or transient, and not the constructor which knows if it needs a singleton instance or a transient?
    Why did you mix a class loader into the library? As I see it, mapping classname => filename is a separate concern all together, and is better handled by the built-in language support (spl_autoload).

  4. #4
    Resident Code Monkey Chris Corbyn's Avatar
    Join Date
    Nov 2005
    Location
    Melbourne, Australia
    Posts
    713
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Thanks kyberfabrikken, I always appreciate the value of your response

    Quote Originally Posted by kyberfabrikken View Post
    Funny. I just finished the last touches on a di container/factory, which I have been nursing for a while now. I'd be interested in hearing, why you didn't go with picophp or phemto? I know why I didn't, but I'd like to hear if your reasons are similar.
    Honestly, because:

    a) I thought it would be a fun side-project.
    b) I looked briefly at Phemto just through the SVN Browse link on SF and for amount of code in it (not much) it didn't look like enough to disuade me from writing my own. I hadn't seen picophp before, i'll have to check it out

    I'd also like to hear, why you ended up with a configuration file? I had that for a while, but ditched it, because I don't think it adds any value and in fact makes the whole construction more opaque.
    It's optional really. Without the config file you simply load in Specifications for dependencies programmatically. I figured as part of something like a framework or a large library it would be:

    a) Less memory intensive to look up only requested dependencies as needed
    b) Easier to maintain specifications as a separate file.

    Like I say, it's optional though. The tests just do it programmatically.

    Finally - Do you have concrete requirements for setter injection, or is it just a nice-to-have feature?
    I didn't strictly want classes to know anything about the DI container (i.e. it wasn't taken as granted that a class wil ask the DI container for a dependency itself). Instead, dependencies should be able to be injected with the factory and it gets a point where the number of constructor args gets a bit silly. Granted, you would pass the factory to the classes and have them use it but then you lose the transparency of the injection container (something I actually quite like) Maybe I've been writing too many JavaBeans lately.


    Spec is a bit vague -- Wouldn't ClassFactory be more appropriate?
    I agree that 'Spec' could be improved, but ClassFactory isn't really the right word neither I don't think since Spec only provides information and doesn't actually create anything. 'SpecFinder' is even more obscure because not only does it look for specs, it also creates them. Maybe I should renamed SpecFinder to SpecFactory or SpecLookup.

    Note that Spec's are only looked up when nothing is already loaded. If you add a spec programmatically, or it's already been looked up then that process wont be repeated.

    Why is it the Spec which knows if it's a singleton or transient, and not the constructor which knows if it needs a singleton instance or a transient?
    Two good points Thinking out loud here. Singleton is the wrong term really because you can have multiple components of the same class (i.e. the same type, but configured differently), you just can't have multiple instances of the same configuration... hmmm. If only the classes which depend on it know they need a singleton then suddenly the component isn't really a singleton, it's just 'registered' in the factory. This is probably a more desirable term too. It would be good to allow the dependant class to specify how it wants the dependency Thanks.

    Why did you mix a class loader into the library? As I see it, mapping classname => filename is a separate concern all together, and is better handled by the built-in language support (spl_autoload).
    I'm not a big fan of autoloaders, especially in the context of a library where end-users may also be opposed to using autoloaders. I just feel a bit like I'm treading on egg shells when something at a language-level is going to start including files at will (i.e. when something like class_exists() is used).

    The ClassLoader stuff is a strategy and is optional. If no strategies are loaded then they're simply not used (like in the tests).

    Great feedback, thanks

  5. #5
    SitePoint Wizard silver trophy kyberfabrikken's Avatar
    Join Date
    Jun 2004
    Location
    Copenhagen, Denmark
    Posts
    6,157
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Quote Originally Posted by d11wtq View Post
    I didn't strictly want classes to know anything about the DI container (i.e. it wasn't taken as granted that a class wil ask the DI container for a dependency itself). Instead, dependencies should be able to be injected with the factory and it gets a point where the number of constructor args gets a bit silly. Granted, you would pass the factory to the classes and have them use it but then you lose the transparency of the injection container (something I actually quite like) Maybe I've been writing too many JavaBeans lately.
    That makes sense. And no, you definitely don't want to pass in the container, if you can help it.

    Quote Originally Posted by d11wtq View Post
    I agree that 'Spec' could be improved, but ClassFactory isn't really the right word neither I don't think since Spec only provides information and doesn't actually create anything.
    Okay. I missed the finer details then. Spec makes a bit more sense in that context, but at least drop the abbrev. and call it Specification?

    Quote Originally Posted by d11wtq View Post
    Singleton is the wrong term really because you can have multiple components of the same class (...)
    My sentiments exactly. What's even worse is, that Singleton is the name of a very specific pattern and implementation hereof. I used the term shared instead, and transient for the opposite.

    Quote Originally Posted by d11wtq View Post
    I'm not a big fan of autoloaders, especially in the context of a library where end-users may also be opposed to using autoloaders. I just feel a bit like I'm treading on egg shells when something at a language-level is going to start including files at will (i.e. when something like class_exists() is used).
    Funny you say that after mentioning Java? Although I agree that class_exists() has very undesirable behaviour.

  6. #6
    Resident Code Monkey Chris Corbyn's Avatar
    Join Date
    Nov 2005
    Location
    Melbourne, Australia
    Posts
    713
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Quote Originally Posted by kyberfabrikken View Post
    My sentiments exactly. What's even worse is, that Singleton is the name of a very specific pattern and implementation hereof. I used the term shared instead, and transient for the opposite.
    Ahh, better words

    Funny you say that after mentioning Java? Although I agree that class_exists() has very undesirable behaviour.
    Java's more rigid about what it will and will not load. You also generally know it's loading a class because you'll import it or you'll load it from Class.forName(). More to the point, Java won't just execute arbitrary code when a class is loaded and it won't load something that doesn't *really* contain a class/interface of the correct name. It's just more foolproof. With PHP's autoloader stuff it will happily load in a procedural script without even throwing a warning and you could happily go without noticing that if all you'd done was something like a class_exists(). The other thing is that I can't be 100% sure *what* exactly invokes PHP's autoloaders... class_exists() is just one thing, are there other buried functions or constructs that woukd invoke autoloading without telling you? (Let's not get too sidetracked on that topic though).

    So so far we've got:

    * Some naming issues
    * Dependant's specifying if they want new or shared instanced (to be added)

    I'm inclined to think that reflection is a bit slow but it's so much more elegant than using dynamic method calls/eval().

    Gotta head out so can't reply til later. It's Melbourne cup day and I'm missing all the fun!

  7. #7
    ********* Victim lastcraft's Avatar
    Join Date
    Apr 2003
    Location
    London
    Posts
    2,423
    Mentioned
    2 Post(s)
    Tagged
    0 Thread(s)
    Hi...

    Quote Originally Posted by d11wtq View Post
    I looked briefly at Phemto just through the SVN Browse link on SF and for amount of code in it (not much) it didn't look like enough to disuade me from writing my own.
    Could you elaborate on this? I am curious why not having much code stopped you from using it.

    yours, Marcus
    Marcus Baker
    Testing: SimpleTest, Cgreen, Fakemail
    Other: Phemto dependency injector
    Books: PHP in Action, 97 things

  8. #8
    SitePoint Guru 33degrees's Avatar
    Join Date
    May 2005
    Posts
    707
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Quote Originally Posted by lastcraft View Post
    Could you elaborate on this? I am curious why not having much code stopped you from using it.
    He looked at the code and said, "I can do that"?

    Personally, I only use 3rd party code if the time saved outweighs the learning experience. In the case of phemto, I'd much rather figure it out on my own.

  9. #9
    Resident Code Monkey Chris Corbyn's Avatar
    Join Date
    Nov 2005
    Location
    Melbourne, Australia
    Posts
    713
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Quote Originally Posted by 33degrees View Post
    He looked at the code and said, "I can do that"?

    Personally, I only use 3rd party code if the time saved outweighs the learning experience. In the case of phemto, I'd much rather figure it out on my own.
    Correct. I was already thinking of writing my own purely for pleasure so looking at Phemto didn't put me off going ahead and doing it. Nothing against Phemto, I just wanted to give it a stab. This is the first bit of code I've written for myself in 2 months. It's yeilded a different end-result too.

  10. #10
    SitePoint Addict
    Join Date
    Sep 2006
    Posts
    232
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    The way I'm doing it is, whenever I want to force an injection, I add it as a required argument in the constructor. While optional injections can be passed using a setter method. But that's just a stupid rule I created of course.

  11. #11
    simple tester McGruff's Avatar
    Join Date
    Sep 2003
    Location
    Glasgow
    Posts
    1,690
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    If the purpose of a DI container is to pull out all the wiring into one, accessible location, the key thing is how easy are they to configure.

    Basic, hard-coded factory classes are probably the simplest to start off with. The class is the configuration. Changes either mean editing the class or writing a new one. That isn't terribly flexible but would be OK if there isn't any real rate of change. It can be a good solution.

    XML is often used for DI containers but it's a raw data format which I'd rather not have to look at. One thing the "simple data file" approach can do is allow a non-programmer to choose application behaviours. A program could present some options and then save the choices. You can't really do that with a coded configuration.

    With Phemto, the configuration file is just a few $phemto->register(...) calls. Locator decorators add some flexibility. Reading dependency information from type hints saves some work getting things set up and therefore should probably be a feature of any DIC.

    So how does Swift compare? Is it as easy to use? Does it have extra capabilities not available in Phemto?

  12. #12
    Resident Code Monkey Chris Corbyn's Avatar
    Join Date
    Nov 2005
    Location
    Melbourne, Australia
    Posts
    713
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Just to save any confusion about what this project is, the DI container is not called Swift Swift Mailer is the original project which gave birth to this since version 4 will make use of it. I'm thinking of forking off a new project now however.

    Reading type hints for interfaces was a consideration, but not something I'd want to force because annoyingly PHP's type-hinting doesn't always do what one might want it to do. The ArrayAccess type-hint for example (last I checked) wont checkout if you pass it an array.

    Type hinting on class names is a place where it would work ok, but then some people see that as a bit of a smell. If you type hint on interfaces then I'm not sure how you'd go about determining which implementation of the interface to use.

    I'll take a closer look at Phemto I promise

    By the way, the XML thing is just one format you can use. Here's the YAML equivalent of the XML I posted before.

    Code yaml:
    components:
      frenchParts:
        className: Parts_French
      
      renaultEngine:
        className: Engines_RenaultEngine
        properties:
          parts: { componentRef: frenchParts }
          year:  { value: 2002 }
      
      italianParts:
        className: Parts_Italian
        
      fiatEngine:
        className: Engines_FiatEngine
        properties:
          parts: { componentRef: italianParts }
          year:  { value: 1998 }
      
      car:
        className: CustomCar
        constructor:
          - { componentRef: fiatEngine }

    You always could programmatically register components too, but it was a process which involved creating ComponentSpec objects then registering them. I've changed this to allow one-liners now.

    Code php:
    $di = new Swift_ComponentFactory();
     
    $di->registerComponentSpec('frenchParts', $di->newComponentSpec('Parts_French'));
     
    $di->registerComponentSpec('renaultEngine', $di->newComponentSpec('Engines_RenaultEngine',
      array(), array('parts' => $di->referenceFor('frenchParts'), 'year' => 2002)));
     
    $di->registerComponentSpec('italianParts', $di->newComponentSpec('Parts_Italian'));
     
    $di->registerComponentSpec('fiatEngine', $di->newComponentSpec('Engines_FiatEngine',
      array(), array('parts' => $di->referenceFor('italianParts'), 'year' => 1998)));
     
    $di->registerComponentSpec('car', $di->newComponentSpec('CustomCar',
      array($di->referenceFor('fiatEngine')));

    I'm sure I could simplify that process a bit further.

    The gain from using a config file is purely that you only instantiate specification objects for components which are currently being used.
    Last edited by Chris Corbyn; Nov 6, 2007 at 20:53. Reason: Added example of programmatic specification loading

  13. #13
    SitePoint Addict
    Join Date
    Sep 2006
    Posts
    232
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Chris, I like this lightweight DI container you created, looks like you followed every step mentioned in Fowler's DI article

    The only critique I can make is from a user point of view (Swift Mailer user). Keep this project as a separate project and don't add extra complexity to Swift Mailer. The reason why I'm telling you this is because the component is already to big, and also, if you keep adding extra complexity to it, it will make Swift Mailer very difficult to maintain and very expensive to use.

    Don't punish your users. Don't forget that you are creating the system, but they are maintaining it. KISS

  14. #14
    Resident Code Monkey Chris Corbyn's Avatar
    Join Date
    Nov 2005
    Location
    Melbourne, Australia
    Posts
    713
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Quote Originally Posted by phpimpact View Post
    Chris, I like this lightweight DI container you created, looks like you followed every step mentioned in Fowler's DI article

    The only critique I can make is from a user point of view (Swift Mailer user). Keep this project as a separate project and don't add extra complexity to Swift Mailer. The reason why I'm telling you this is because the component is already to big, and also, if you keep adding extra complexity to it, it will make Swift Mailer very difficult to maintain and very expensive to use.

    Don't punish your users. Don't forget that you are creating the system, but they are maintaining it. KISS
    Thanks

    I've honestly never read the infamous DI article you referred to but it's reassuring to know Most of my DI knowledge comes from Java.

    I'm already thinking up a name for this as a new project. Swift v4 will use it but the end-user doesn't need to be aware of it's usage. v4 isn't out until March (ish) so this is all very much an experimental time right now. If things are fiddly or complex they won't end up in the library. Generally speaking, v4 is going simpler than v3 and will be massively easier to unit test (the Connections and their SMTP-like requirements make Swift very difficult to test right now which is just one of its downfalls... the other being too tight coupling). By 'easier to test' I'm referring the portion of Swift's user-base who are agile developers themselves.

  15. #15
    Resident Code Monkey Chris Corbyn's Avatar
    Join Date
    Nov 2005
    Location
    Melbourne, Australia
    Posts
    713
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    After reading the Phemto README (I agree it's nicely trimmed and simple... kudos!) it has a considerably different approach to injection over the method I've taken. I can't afford to have the restrictions which Phemto has. For one thing I need some way of creating several components which *all* implement the same interface. It seems (unless I've misread the README) that Phemto makes the assumption you'll only ever be using one class per interface. Swift Mailer has multiple implementations of things like Authenticator, Connection, Mime etc and they all get used in the same script alongside each other. Defining 'components' was a good way to allow this in my case.

  16. #16
    SitePoint Wizard silver trophy kyberfabrikken's Avatar
    Join Date
    Jun 2004
    Location
    Copenhagen, Denmark
    Posts
    6,157
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Quote Originally Posted by Chris Corbyn View Post
    For one thing I need some way of creating several components which *all* implement the same interface. It seems (unless I've misread the README) that Phemto makes the assumption you'll only ever be using one class per interface.
    I think the assumption is, that if you do in fact need several classes for an interface, you should create multiple interfaces. Logically, it makes sense, but I agree that it feels a bit heavy.

    Quote Originally Posted by Chris Corbyn View Post
    The gain from using a config file is purely that you only instantiate specification objects for components which are currently being used.
    It just occurred to me; Do you load+parse a spec configuration for each spec, on the first use? That sounds like a serious performance problem to me. I'd rather register all potential components in one go, than having to break out to I/O multiple times.

  17. #17
    Resident Code Monkey Chris Corbyn's Avatar
    Join Date
    Nov 2005
    Location
    Melbourne, Australia
    Posts
    713
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Quote Originally Posted by kyberfabrikken View Post
    It just occurred to me; Do you load+parse a spec configuration for each spec, on the first use? That sounds like a serious performance problem to me. I'd rather register all potential components in one go, than having to break out to I/O multiple times.
    Nope, it's a single XML file right now (no reason why it couldn't be multiple files if desired however). The file is loaded and parsed once, then a handful of Xpath queries are made for each component. It's arguable what the trade-off is between instantiating all specification objects at the start, or looking up only needed specifications at runtime (smart lazy-loading effectively).

  18. #18
    Resident Code Monkey Chris Corbyn's Avatar
    Join Date
    Nov 2005
    Location
    Melbourne, Australia
    Posts
    713
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    I've added a little decorated reflection class returning method to the API, so for things like the following:

    Code php:
    class Car {
      private $_engine;
      private $_registrationNo;
      private $_year;
     
      public function __construct($registrationNo, $year) {
        $this->_registrationNo = $registrationNo;
        $this->_year = $year;
      }
     
      public function setEngine(Engine $engine) {
        $this->_engine;
      }
    }

    You can use setter based injection for the dependency, the constructor args for the specific object configuration so it's more streamlined:

    Code php:
    <?php
     
    $car = $di->classOf('car')->newInstance('R110 ADC', 1998);

  19. #19
    SitePoint Guru 33degrees's Avatar
    Join Date
    May 2005
    Posts
    707
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Quote Originally Posted by kyberfabrikken View Post
    I think the assumption is, that if you do in fact need several classes for an interface, you should create multiple interfaces. Logically, it makes sense, but I agree that it feels a bit heavy.
    I really think that, if you're going to use typehints and reflection, it should be bypassable in some way. It strikes me as quite wrong to have to change my class hierarchies to conform to a 3rd party tool, especially in the context of using a framework that already dictates a certain structure. Another point is that not everything a DI container might inject will be objects, you might want to inject string or arrays of configuration data...

  20. #20
    SitePoint Addict
    Join Date
    Sep 2006
    Posts
    232
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Cool, nice example. It would be great if you could document this a little bit more, like you did with Swift in your wiki. It's a nice and simple approach, and I like the idea of this being a separate project. Remember, whatever you do, do it outdoors. You are in Australia for god sake!

  21. #21
    simple tester McGruff's Avatar
    Join Date
    Sep 2003
    Location
    Glasgow
    Posts
    1,690
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Quote Originally Posted by phpimpact
    Don't forget that you are creating the system, but they are maintaining it.
    A configurable DI container can actually be a big plus for users if you want to provide an easy way to tweak behaviour (assuming an underlying design which is amenable to altering behaviour simply by wiring up different types of objects). Rather than having to dig through the code looking for "new Foo", configuration gets pulled out to a single location. An author can provide a few pre-written options and users can write classes of their own to create whole new behaviours.

    Suppose a default spam policy is provided...
    PHP Code:
    // a line in a DIC configuration file - Phemto style 
    // here but same sort of deal for YAML & etc
    $this->injector->register('DefaultSpamPolicy'); 
    ..it's easy to change - just write a new one and register it instead:
    PHP Code:
    $this->injector->register('SpiffyNewSpamPolicy'); 
    The real power of a DI container like Phemto is that, inside the app, the code doesn't depend on any concrete implementation:

    PHP Code:
    class Foo {
        function 
    __construct(SpamPolicy $policy) {
            ...
            ...
        }
        ...
        ...

    I've assumed that Default~ and Spiffy~ either implement a SpamPolicy interface or extend a SpamPolicy class.

    Chris: with Phemto the hinted type in a Foo constructor just has to be somewhere in the class hierarchy, not necessarily a formally declared interface. So:
    (1) you can have multiple instances of any class
    (2) you can have many instances of different classes which implement the same interface provided you don't type hint the interface, but somewhere higher up

    Of course hinting towards the bottom of the hierarchy is more flexible since there will be more options with which the dependency can be filled. Maybe that's something which java DIC's insist upon?

    YAML - looks much nicer than XML The php option looks nice and simple
    PHP Code:
    $car $di->classOf('car')->newInstance('R110 ADC'1998

  22. #22
    Resident Code Monkey Chris Corbyn's Avatar
    Join Date
    Nov 2005
    Location
    Melbourne, Australia
    Posts
    713
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Quote Originally Posted by McGruff View Post
    A configurable DI container can actually be a big plus for users if you want to provide an easy way to tweak behaviour (assuming an underlying design which is amenable to altering behaviour simply by wiring up different types of objects). Rather than having to dig through the code looking for "new Foo", configuration gets pulled out to a single location. An author can provide a few pre-written options and users can write classes of their own to create whole new behaviours.
    This is one of the motivations for doing this. I've had a small subset of enthusiastic users asking for various ways to do funky things that really can't be done with the level of coupling right now (unless you want to modify Swift itself ... ).

    Suppose a default spam policy is provided...
    PHP Code:
    // a line in a DIC configuration file - Phemto style 
    // here but same sort of deal for YAML & etc
    $this->injector->register('DefaultSpamPolicy'); 
    ..it's easy to change - just write a new one and register it instead:
    PHP Code:
    $this->injector->register('SpiffyNewSpamPolicy'); 
    The real power of a DI container like Phemto is that, inside the app, the code doesn't depend on any concrete implementation:

    PHP Code:
    class Foo {
        function 
    __construct(SpamPolicy $policy) {
            ...
            ...
        }
        ...
        ...

    I've assumed that Default~ and Spiffy~ either implement a SpamPolicy interface or extend a SpamPolicy class.

    Chris: with Phemto the hinted type in a Foo constructor just has to be somewhere in the class hierarchy, not necessarily a formally declared interface. So:
    (1) you can have multiple instances of any class
    (2) you can have many instances of different classes which implement the same interface provided you don't type hint the interface, but somewhere higher up

    Of course hinting towards the bottom of the hierarchy is more flexible since there will be more options with which the dependency can be filled. Maybe that's something which java DIC's insist upon?

    YAML - looks much nicer than XML The php option looks nice and simple
    PHP Code:
    $car $di->classOf('car')->newInstance('R110 ADC'1998
    OK, so take this layout:

    PHP Code:
    interface DeliveryDriver {
      public function 
    send(Message $message);
    }

    class 
    SmtpDeliveryDriver implements DeliveryDriver {
    }

    class 
    SendmailDeliveryDriver implements DeliveryDriver {
    }

    class 
    Mailer {
      public function 
    __construct(DeliveryDriver $driver) {
      }

    How does Phemto know which implementation to use?

    I also need to do things like this:

    PHP Code:
    interface Header {
      public function 
    getName();
      public function 
    getValue();
    }

    class 
    Message {
      private 
    $_headers = array(); //Of Header instances
      
    public function setHeaders($headers = array()) {
        
    $this->_headers $headers;
      }

    Could Phemto handle that?

    Code php:
    <components>
     
      <component>
        <name>toHeader</name>
        <className>BasicHeader</className>
        <constructor>
          <arg>
            <value>To</value>
          </arg>
        </constructor>
      </component>
     
      <component>
        <name>fromHeader</name>
        <className>BasicHeader</className>
        <constructor>
          <arg>
            <value>From</value>
          </arg>
        </constructor>
      </component>
     
      <component>
        <name>message</name>
        <className>Message</className>
        <properties>
          <property>
            <key>headers</key>
            <collection>
              <componentRef>toHeader</componentRef>
              <componentRef>fromHeader</componentRef>
            </collection>
          </property>
        </properties>
      </component>
     
    </components>

    Granted I could perhaps simplify this a bit, but conceptually it's permitted.

  23. #23
    SitePoint Addict
    Join Date
    Sep 2006
    Posts
    232
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    After carefully analysing Phemto, I can see what Phemto tried to achieve with its implementation of the DI pattern, simplicity. And simplicity is one of the most difficult things to achieve. Pawel (a great guy and programmer) did the same 2 years ago, brought some of the complexity from java to php, and people turned to other solutions, simple ones.

    In my opinion, you need to get rid of all the XML, YAML and INI stuff. Create support for namespaces, get rid of some classes and methods, and simplify the implementation.

    (When you add it to Swift of course, no more than 2 files would be great and will do the job).

    Then, when you release it as a separate project, yeah, go for it. XML, YAML, newInstance(), cloneInstane() and everything you can think of.
    Last edited by Chris Corbyn; Nov 8, 2007 at 15:55.

  24. #24
    SitePoint Addict
    Join Date
    Sep 2006
    Posts
    232
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    I tried to join 2 messages and I lost them both :/

  25. #25
    Resident Code Monkey Chris Corbyn's Avatar
    Join Date
    Nov 2005
    Location
    Melbourne, Australia
    Posts
    713
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Quote Originally Posted by phpimpact View Post
    I tried to join 2 messages and I lost them both :/
    Odd, I'm not sure how that happened.... it seemed to delete the posts but leave them dead. I've undone the change and merged the posts for you


Bookmarks

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •