Initial Horde_Injector dependency injection container, contributed by Blue State...
authorChuck Hagenbuch <chuck@horde.org>
Mon, 21 Sep 2009 04:08:45 +0000 (00:08 -0400)
committerChuck Hagenbuch <chuck@horde.org>
Mon, 21 Sep 2009 04:08:45 +0000 (00:08 -0400)
13 files changed:
framework/Injector/lib/Horde/Injector.php [new file with mode: 0644]
framework/Injector/lib/Horde/Injector/Binder.php [new file with mode: 0644]
framework/Injector/lib/Horde/Injector/Binder/Factory.php [new file with mode: 0644]
framework/Injector/lib/Horde/Injector/Binder/Implementation.php [new file with mode: 0644]
framework/Injector/lib/Horde/Injector/Exception.php [new file with mode: 0644]
framework/Injector/lib/Horde/Injector/Scope.php [new file with mode: 0644]
framework/Injector/lib/Horde/Injector/TopLevel.php [new file with mode: 0644]
framework/Injector/package.xml [new file with mode: 0644]
framework/Injector/test/Horde/Injector/AllTests.php [new file with mode: 0644]
framework/Injector/test/Horde/Injector/Binder/FactoryTest.php [new file with mode: 0644]
framework/Injector/test/Horde/Injector/Binder/ImplementationTest.php [new file with mode: 0644]
framework/Injector/test/Horde/Injector/BinderTest.php [new file with mode: 0644]
framework/Injector/test/Horde/Injector/InjectorTest.php [new file with mode: 0644]

diff --git a/framework/Injector/lib/Horde/Injector.php b/framework/Injector/lib/Horde/Injector.php
new file mode 100644 (file)
index 0000000..7383e5f
--- /dev/null
@@ -0,0 +1,197 @@
+<?php
+/**
+ * Injector class for injecting dependencies of objects
+ *
+ * This class is responsible for injecting dependencies of objects.  It is inspired
+ * by the bucket_Container's concept of child scopes, but written to support many different
+ * types of bindings as well as allowing for setter injection bindings.
+ *
+ * @author Bob Mckee <bmckee@bywires.com>
+ * @author James Pepin <james@jamespepin.com>
+ */
+class Horde_Injector implements Horde_Injector_Scope
+{
+    private $_parentInjector;
+    private $_bindings;
+    private $_instances;
+
+    /**
+     * create a new injector object.
+     *
+     * Every injector object has a parent scope.  For the very first Horde_Injector, you should pass it a
+     * Horde_Injector_TopLevel
+     *
+     * @param Horde_Injector_Scope The parent scope
+     */
+    public function __construct(Horde_Injector_Scope $injector)
+    {
+        $this->_parentInjector = $injector;
+        $this->_bindings = array();
+        $this->_instances = array(__CLASS__ => $this);
+    }
+
+    /**
+     * create a child injector that inherits this injector's scope.
+     *
+     * All child injectors inherit the parent scope.  Any objects that were created using
+     * getInstance, will be available to the child container.  The child container can set bindings
+     * to override the parent, and none of those bindings will leak to the parent.
+     *
+     * @return Horde_Injector A child injector with $this as its parent
+     */
+    public function createChildInjector()
+    {
+        return new self($this);
+    }
+
+    public function __call($name, $args)
+    {
+        if (substr($name, 0, 4) == 'bind') {
+            return $this->_bind(substr($name, 4), $args);
+        }
+        throw new BadMethodCallException('Call to undefined method ' . __CLASS__ .
+            '::' . $name . '()');
+    }
+
+    /**
+     * method that creates binders to send to addBinder. This is called by the magic method __call
+     * whenever a function is called that starts with bind
+     *
+     * @param string $type The type of Horde_Injector_Binder_ to be created. Matches ^Horde_Injector_Binder_(\w+)$
+     * @param array $args The constructor arguments for the binder object
+     *
+     * @return Horde_Injector_Binder The binder object created. Useful for method chaining
+     */
+    private function _bind($type, $args)
+    {
+        $interface = array_shift($args);
+
+        if (!$interface) {
+            throw new BadMethodCallException('First paremeter for "bind' . $type .
+                '" must be the name of an interface or class');
+        }
+
+        $reflectionClass = new ReflectionClass('Horde_Injector_Binder_' . $type);
+
+        if ($reflectionClass->getConstructor()) {
+            $this->_addBinder($interface, $reflectionClass->newInstanceArgs($args));
+        }
+        $this->_addBinder($interface, $reflectionClass->newInstance());
+
+        return $this->_getBinder($interface);
+    }
+
+    /**
+     * Add a Horde_Injector_Binder to an interface
+     *
+     * This is the method by which we bind an interface to a concrete implentation,
+     * or factory.  For convenience, binders may be added by bind[BinderType].
+     * bindFactory - creates a Horde_Injector_Binder_Factory
+     * bindImplementation - creates a Horde_Injector_Binder_Implementation
+     * All subsequent arguments are passed to the constructor of the Horde_Injector_Binder object
+     * Any Horde_Injector_Binder_ Object may be created this way.
+     *
+     * @param string $interface The interface to bind to
+     * @param Horde_Injector_Binder $binder The binder to be bound to the specified $interface
+     * @return Horde_Injector $this  a reference to itself for method chaining
+     */
+    public function addBinder($interface, Horde_Injector_Binder $binder)
+    {
+        $this->_addBinder($interface, $binder);
+        return $this;
+    }
+
+    /**
+     * Get the Binder associated with the specified instance.
+     *
+     * Binders are objects responsible for binding a particular interface
+     * with a class. If no binding is set for this object, the parent scope is consulted.
+     *
+     * @param string $interface The interface to retrieve binding information for
+     * @return Horde_Injector_Binder The binding set for the specified interface
+     */
+    public function getBinder($interface)
+    {
+        if (isset($this->_bindings[$interface])) {
+            return $this->_bindings[$interface];
+        }
+        return $this->_parentInjector->getBinder($interface);
+    }
+
+    /**
+     * Set the object instance to be retrieved by getInstance the next time the specified
+     * interface is requested.
+     *
+     * This method allows you to set the cached object instance so that all subsequent getInstance
+     * calls return the object you have specified
+     *
+     * @param string $interface The interface to bind the instance to
+     * @param mixed $instance The object instance to be bound to the specified instance
+     * @return Horde_Injector $this  a reference to itself for method chaining
+     */
+    public function setInstance($interface, $instance)
+    {
+        $this->_instances[$interface] = $instance;
+        return $this;
+    }
+
+    /**
+     * Create a new instance of the specified object/interface
+     *
+     * This method creates a new instance of the specified object/interface.
+     * NOTE: it does not save that instance for later retrieval. If your object should
+     * be re-used elsewhere, you should be using getInstance
+     *
+     * @param string $interface The interface name, or object class to be created.
+     * @return mixed A new object that implements $interface
+     */
+    public function createInstance($interface)
+    {
+        return $this->getBinder($interface)->create($this);
+    }
+
+    /**
+     * Retrieve an instance of the specified object/interface.
+     *
+     * This method gets you an instance, and saves a reference to that instance for later requests.
+     * Interfaces must be bound to a concrete class to be created this way.
+     * Concrete instances may be created through reflection.
+     * It does not gaurantee that it is a new instance of the object.  For a new
+     * instance see createInstance
+     *
+     * @param string $interface The interface name, or object class to be created.
+     * @return mixed An object that implements $interface, not necessarily a new one.
+     */
+    public function getInstance($interface)
+    {
+        // do we have an instance?
+        if (!isset($this->_instances[$interface])) {
+            // do we have a binding for this interface? if so then we don't ask our parent
+            if (!isset($this->_bindings[$interface])) {
+                // does our parent have an instance?
+                if ($instance = $this->_parentInjector->getInstance($interface)) {
+                    return $instance;
+                }
+            }
+
+            // we have to make our own instance
+            $this->setInstance($interface, $this->createInstance($interface));
+        }
+
+        return $this->_instances[$interface];
+    }
+
+    private function _addBinder($interface, Horde_Injector_Binder $binder)
+    {
+        // first we check to see if our parent already has an equal binder set.
+        // if so we don't need to do anything
+        if (!$binder->equals($this->_parentInjector->getBinder($interface))) {
+            $this->_bindings[$interface] = $binder;
+        }
+    }
+
+    private function _getBinder($interface)
+    {
+        return $this->_bindings[$interface];
+    }
+}
diff --git a/framework/Injector/lib/Horde/Injector/Binder.php b/framework/Injector/lib/Horde/Injector/Binder.php
new file mode 100644 (file)
index 0000000..fafa479
--- /dev/null
@@ -0,0 +1,13 @@
+<?php
+interface Horde_Injector_Binder
+{
+    public function create(Horde_Injector $injector);
+
+    /**
+     * determine if one binder equals another binder
+     *
+     * @param Horde_Injector_Binder $binder The binder to compare against $this
+     * @return bool true if they are equal, or false if they are not equal
+     */
+    public function equals(Horde_Injector_Binder $binder);
+}
diff --git a/framework/Injector/lib/Horde/Injector/Binder/Factory.php b/framework/Injector/lib/Horde/Injector/Binder/Factory.php
new file mode 100644 (file)
index 0000000..7a395c4
--- /dev/null
@@ -0,0 +1,92 @@
+<?php
+/**
+ * A binder object for binding an interface to a factory class and method
+ *
+ * An interface may be bound to a factory class.  That factory class must
+ * provide a method or methods that accept a Horde_Injector, and return an
+ * object that satisfies the instance requirement. ie:
+ *   class MyFactory {
+ *      ...
+ *      public function create(Horde_Injector $injector) {
+ *         return new MyClass($injector->getInstance('Collaborator'), new MyOtherClass(17));
+ *      }
+ *      ...
+ *
+ * @author Bob Mckee <bmckee@bywires.com>
+ * @author James Pepin <james@jamespepin.com>
+ */
+class Horde_Injector_Binder_Factory implements Horde_Injector_Binder
+{
+    private $_factory;
+    private $_method;
+
+    /**
+     * create a new Horde_Injector_Binder_Factory instance
+     *
+     * @param string $factory The factory class to use for creating objects
+     * @param string $method The method on that factory to use for creating objects
+     */
+    public function __construct($factory, $method)
+    {
+        $this->_factory = $factory;
+        $this->_method = $method;
+    }
+
+    public function equals(Horde_Injector_Binder $otherBinder)
+    {
+        if (!$otherBinder instanceof Horde_Injector_Binder_Factory) {
+            return false;
+        }
+
+        if ($otherBinder->getFactory() != $this->_factory) {
+            return false;
+        }
+
+        if ($otherBinder->getMethod() != $this->_method) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * get the factory classname that this binder was bound to
+     *
+     * @return string The factory classname this binder is bound to
+     */
+    public function getFactory()
+    {
+        return $this->_factory;
+    }
+
+    /**
+     * get the method that this binder was bound to
+     *
+     * @return string The method this binder is bound to
+     */
+    public function getMethod()
+    {
+        return $this->_method;
+    }
+
+    /**
+     * Create instance using a factory method
+     *
+     * If the factory depends on a Horde_Injector we want to limit its scope so
+     * it cannot change anything that effects any higher-level scope.  A factory
+     * should not have the responsibility of making a higher-level scope change.
+     * To enforce this we create a new child Horde_Injector.  When a
+     * Horde_Injector is requested from a Horde_Injector it will return itself.
+     * This means that the factory will only ever be able to work on the child
+     * Horde_Injector we give it now.
+     *
+     * @param Horde_Injector $injector
+     */
+    public function create(Horde_Injector $injector)
+    {
+        $childInjector = $injector->createChildInjector();
+        // we use getInstance here because we don't want to have to create this
+        // factory more than one time to create more objects of this type.
+        return $childInjector->getInstance($this->_factory)->{$this->_method}($childInjector);
+    }
+}
diff --git a/framework/Injector/lib/Horde/Injector/Binder/Implementation.php b/framework/Injector/lib/Horde/Injector/Binder/Implementation.php
new file mode 100644 (file)
index 0000000..7a65351
--- /dev/null
@@ -0,0 +1,97 @@
+<?php
+class Horde_Injector_Binder_Implementation implements Horde_Injector_Binder
+{
+    private $_implementation;
+    private $_setters;
+
+    public function __construct($implementation)
+    {
+        $this->_implementation = $implementation;
+        $this->_setters = array();
+    }
+
+    public function getImplementation()
+    {
+        return $this->_implementation;
+    }
+
+    public function bindSetter($method)
+    {
+        $this->_setters[] = $method;
+        return $this;
+    }
+
+    public function equals(Horde_Injector_Binder $otherBinder)
+    {
+        if (!$otherBinder instanceof Horde_Injector_Binder_Implementation) {
+            return false;
+        }
+
+        if ($otherBinder->getImplementation() != $this->_implementation) {
+            return false;
+        }
+
+        return true;
+    }
+
+    public function create(Horde_Injector $injector)
+    {
+        $reflectionClass = new ReflectionClass($this->_implementation);
+        $this->_validateImplementation($reflectionClass);
+        $instance = $this->_getInstance($injector, $reflectionClass);
+        $this->_callSetters($injector, $instance);
+        return $instance;
+    }
+
+    private function _validateImplementation(ReflectionClass $reflectionClass)
+    {
+        if ($reflectionClass->isAbstract() || $reflectionClass->isInterface()) {
+            throw new Horde_Injector_Exception('Cannot bind interfaces or abstract classes "' .
+                $this->_implementation . '" to an interface.');
+        }
+    }
+
+    private function _getInstance(Horde_Injector $injector, ReflectionClass $class)
+    {
+        if ($class->getConstructor()) {
+            return $class->newInstanceArgs(
+                $this->_getMethodDependencies($injector, $class->getConstructor())
+            );
+        }
+        return $class->newInstance();
+    }
+
+    private function _getMethodDependencies(Horde_Injector $injector, ReflectionMethod $method)
+    {
+        $dependencies = array();
+        foreach ($method->getParameters() as $parameter) {
+            $dependencies[] = $this->_getParameterDependency($injector, $parameter);
+        }
+        return $dependencies;
+    }
+
+    private function _getParameterDependency(Horde_Injector $injector, ReflectionParameter $parameter)
+    {
+        if ($parameter->getClass()) {
+            $dependency = $injector->getInstance($parameter->getClass()->getName());
+        } elseif ($parameter->isOptional()) {
+            $dependency = $parameter->getDefaultValue();
+        } else {
+            throw new Horde_Injector_Exception('Unable to instantiate class "' . $this->_implementation .
+                '" because a value could not be determined untyped parameter "$' .
+                $parameter->getName() . '"');
+        }
+        return $dependency;
+    }
+
+    private function _callSetters(Horde_Injector $injector, $instance)
+    {
+        foreach ($this->_setters as $setter) {
+            $reflectionMethod = new ReflectionMethod($instance, $setter);
+            $reflectionMethod->invokeArgs(
+                $instance,
+                $this->_getMethodDependencies($injector, $reflectionMethod)
+            );
+        }
+    }
+}
diff --git a/framework/Injector/lib/Horde/Injector/Exception.php b/framework/Injector/lib/Horde/Injector/Exception.php
new file mode 100644 (file)
index 0000000..7facb86
--- /dev/null
@@ -0,0 +1,3 @@
+<?php
+class Horde_Injector_Exception extends Exception
+{}
diff --git a/framework/Injector/lib/Horde/Injector/Scope.php b/framework/Injector/lib/Horde/Injector/Scope.php
new file mode 100644 (file)
index 0000000..5c0e5e9
--- /dev/null
@@ -0,0 +1,26 @@
+<?php
+/**
+ * Interface for injector scopes
+ *
+ * Injectors implement a Chain of Responsibility pattern.  This is the required
+ * interface for injectors to pass on responsibility to parent objects in the chain.
+ */
+interface Horde_Injector_Scope
+{
+    /**
+     * Returns the Horde_Injector_Binder object mapped to the request interface if such a
+     * mapping exists
+     *
+     * @return Horde_Injector_Binder|null
+     * @param string $interface interface name of object whose binding if being retrieved
+     */
+    public function getBinder($interface);
+
+    /**
+     * Returns instance of requested object if proper configuration has been provided.
+     *
+     * @return Object
+     * @param string $interface interface name of object which is being requested
+     */
+    public function getInstance($interface);
+}
diff --git a/framework/Injector/lib/Horde/Injector/TopLevel.php b/framework/Injector/lib/Horde/Injector/TopLevel.php
new file mode 100644 (file)
index 0000000..0050a35
--- /dev/null
@@ -0,0 +1,40 @@
+<?php
+/**
+ * Top level injector class for returning the default binding for an object
+ *
+ * This class returns a Horde_Injector_Binder_Implementation with the requested
+ * $interface mapped to itself.  This is the default case, and for conrete
+ * classes should work all the time so long as you constructor parameters are
+ * typed
+ *
+ * @author Bob Mckee <bmckee@bywires.com>
+ * @author James Pepin <james@jamespepin.com>
+ */
+class Horde_Injector_TopLevel implements Horde_Injector_Scope
+{
+    /**
+     * Get an Implementation Binder that maps the $interface to itself
+     *
+     * @param string $interface The interface to retrieve binding information for
+     * @return Horde_Injector_Binder_Implementation a new binding object that maps the interface to itself
+     */
+    public function getBinder($interface)
+    {
+        return new Horde_Injector_Binder_Implementation($interface, $interface);
+    }
+
+    /**
+     * Always return null.  Object doesn't keep instance references
+     *
+     * Method is necessary because this object is the default parent Injector.
+     * The child of this injector will ask it for instances in the case where no
+     * bindings are set on the child.  This should always return null.
+     *
+     * @param string $interface The interface in question
+     * @return null
+     */
+    public function getInstance($interface)
+    {
+        return null;
+    }
+}
diff --git a/framework/Injector/package.xml b/framework/Injector/package.xml
new file mode 100644 (file)
index 0000000..3d9ecf9
--- /dev/null
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package packagerversion="1.4.9" version="2.0" xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
+http://pear.php.net/dtd/tasks-1.0.xsd
+http://pear.php.net/dtd/package-2.0
+http://pear.php.net/dtd/package-2.0.xsd">
+ <name>Injector</name>
+ <channel>pear.horde.org</channel>
+ <summary>Horde dependency injection container</summary>
+ <description>A depedency injection container for Horde.</description>
+ <lead>
+  <name>Chuck Hagenbuch</name>
+  <user>chuck</user>
+  <email>chuck@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <date>2009-09-20</date>
+ <version>
+  <release>0.1.0</release>
+  <api>0.1.0</api>
+ </version>
+ <stability>
+  <release>beta</release>
+  <api>beta</api>
+ </stability>
+ <license uri="http://opensource.org/licenses/bsd-license.php">BSD</license>
+ <notes>
+   * Initial release, contributed by Blue State Digital
+ </notes>
+ <contents>
+  <dir name="/">
+   <dir name="lib">
+    <dir name="Horde">
+     <dir name="Injector">
+      <dir name="Binder">
+       <file name="Factory.php" role="php" />
+       <file name="Implementation.php" role="php" />
+      </dir> <!-- /lib/Horde/Injector/Binder -->
+      <file name="Binder.php" role="php" />
+      <file name="Exception.php" role="php" />
+      <file name="Scope.php" role="php" />
+      <file name="TopLevel.php" role="php" />
+     </dir> <!-- /lib/Horde/Injector -->
+     <file name="Injector.php" role="php" />
+    </dir> <!-- /lib/Horde -->
+   </dir> <!-- /lib -->
+  </dir> <!-- / -->
+ </contents>
+ <dependencies>
+  <required>
+   <php>
+    <min>5.2.0</min>
+   </php>
+   <pearinstaller>
+    <min>1.7.0</min>
+   </pearinstaller>
+  </required>
+ </dependencies>
+ <phprelease>
+  <filelist>
+   <install name="lib/Horde/Injector/Binder/Factory.php" as="Horde/Injector/Binder/Factory.php" />
+   <install name="lib/Horde/Injector/Binder/Implementation.php" as="Horde/Injector/Binder/Implementation.php" />
+   <install name="lib/Horde/Injector/Binder.php" as="Horde/Injector/Binder.php" />
+   <install name="lib/Horde/Injector/Exception.php" as="Horde/Injector/Exception.php" />
+   <install name="lib/Horde/Injector/Scope.php" as="Horde/Injector/Scope.php" />
+   <install name="lib/Horde/Injector/TopLevel.php" as="Horde/Injector/TopLevel.php" />
+   <install name="lib/Horde/Injector.php" as="Horde/Injector.php" />
+  </filelist>
+ </phprelease>
+</package>
diff --git a/framework/Injector/test/Horde/Injector/AllTests.php b/framework/Injector/test/Horde/Injector/AllTests.php
new file mode 100644 (file)
index 0000000..e0306e9
--- /dev/null
@@ -0,0 +1,45 @@
+<?php
+/**
+ * @package    Horde_Injector
+ * @subpackage UnitTests
+ */
+
+if (!defined('PHPUnit_MAIN_METHOD')) {
+    define('PHPUnit_MAIN_METHOD', 'Horde_Injector_AllTests::main');
+}
+
+require 'Horde/Autoloader.php';
+
+class Horde_Injector_AllTests {
+
+    public static function main()
+    {
+        PHPUnit_TextUI_TestRunner::run(self::suite());
+    }
+
+    public static function suite()
+    {
+        $suite = new PHPUnit_Framework_TestSuite('Horde Framework - Horde_Injector');
+
+        $basedir = dirname(__FILE__);
+        $baseregexp = preg_quote($basedir . DIRECTORY_SEPARATOR, '/');
+
+        foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($basedir)) as $file) {
+            if ($file->isFile() && preg_match('/Test.php$/', $file->getFilename())) {
+                $pathname = $file->getPathname();
+                require $pathname;
+
+                $class = str_replace(DIRECTORY_SEPARATOR, '_',
+                                     preg_replace("/^$baseregexp(.*)\.php/", '\\1', $pathname));
+                $suite->addTestSuite('Horde_Injector_' . $class);
+            }
+        }
+
+        return $suite;
+    }
+
+}
+
+if (PHPUnit_MAIN_METHOD == 'Horde_Injector_AllTests::main') {
+    Horde_Injector_AllTests::main();
+}
diff --git a/framework/Injector/test/Horde/Injector/Binder/FactoryTest.php b/framework/Injector/test/Horde/Injector/Binder/FactoryTest.php
new file mode 100644 (file)
index 0000000..43188f1
--- /dev/null
@@ -0,0 +1,101 @@
+<?php
+class Horde_Injector_Binder_FactoryTest extends Horde_Test_Case
+{
+    public function testShouldCallFactoryMethod()
+    {
+        $factory = $this->getMock('Factory', array('create'));
+        $factory->expects($this->once())
+            ->method('create')
+            ->with()
+            ->will($this->returnValue('INSTANCE'));
+        $factoryClassName = get_class($factory);
+
+        $childInjector = $this->getMockSkipConstructor('Horde_Injector', array('createInstance', 'getInstance'));
+        $childInjector->expects($this->once())
+            ->method('getInstance')
+            ->with($this->equalTo($factoryClassName))
+            ->will($this->returnValue($factory));
+
+        $injector = $this->getMockSkipConstructor('Horde_Injector', array('createChildInjector'));
+        $injector->expects($this->once())
+            ->method('createChildInjector')
+            ->with()
+            ->will($this->returnValue($childInjector));
+
+        $factoryBinder = new Horde_Injector_Binder_Factory(
+            $factoryClassName,
+            'create'
+        );
+
+        $this->assertEquals('INSTANCE', $factoryBinder->create($injector));
+    }
+
+    /**
+     * the factory binder should pass a child injector object to the factory, so that
+     * any configuration that happens in the factory will not bleed into global scope
+     */
+    public function testShouldPassChildInjectorToFactoryMethod()
+    {
+        $factory = new InjectorFactoryTestMockFactory();
+
+        $binder = new Horde_Injector_Binder_Factory('InjectorFactoryTestMockFactory', 'create');
+
+        $injector = new InjectorMockTestAccess(new Horde_Injector_TopLevel());
+        $injector->TEST_ID = "PARENTINJECTOR";
+
+        // set the instance so we know we'll get our factory object from the injector
+        $injector->setInstance('InjectorFactoryTestMockFactory', $factory);
+
+        // calling create should pass a child injector to the factory
+        $binder->create($injector);
+
+        // now the factory should have a reference to a child injector
+        $this->assertEquals($injector->TEST_ID . "->CHILD", $factory->getInjector()->TEST_ID, "Incorrect Injector passed to factory method");
+    }
+
+    /**
+     * this test guarantees that our mock factory stores the injector that was given to it,
+     * so that we may inspect it later and prove what injector is actually given to it
+     */
+    public function testMockFactoryStoresPassedInjector()
+    {
+        $factory = new InjectorFactoryTestMockFactory();
+        $injector = new InjectorMockTestAccess(new Horde_Injector_TopLevel());
+        $injector->TEST_ID = "INJECTOR";
+        $factory->create($injector);
+
+        $this->assertEquals($injector, $factory->getInjector());
+    }
+
+    public function testShouldReturnBindingDetails()
+    {
+        $factoryBinder = new Horde_Injector_Binder_Factory(
+            'FACTORY',
+            'METHOD'
+        );
+
+        $this->assertEquals('FACTORY', $factoryBinder->getFactory());
+        $this->assertEquals('METHOD', $factoryBinder->getMethod());
+    }
+}
+
+class InjectorFactoryTestMockFactory
+{
+    public function getInjector()
+    {
+        return $this->_injector;
+    }
+    public function create(Horde_Injector $injector)
+    {
+        $this->_injector = $injector;
+    }
+}
+class InjectorMockTestAccess extends Horde_Injector
+{
+    public function createChildInjector()
+    {
+        $child = new self($this);
+        $child->TEST_ID = $this->TEST_ID . "->CHILD";
+        return $child;
+    }
+}
diff --git a/framework/Injector/test/Horde/Injector/Binder/ImplementationTest.php b/framework/Injector/test/Horde/Injector/Binder/ImplementationTest.php
new file mode 100644 (file)
index 0000000..6a60077
--- /dev/null
@@ -0,0 +1,215 @@
+<?php
+class Horde_Injector_Binder_ImplementationTest extends Horde_Test_Case
+{
+    public function testShouldReturnBindingDetails()
+    {
+        $implBinder = new Horde_Injector_Binder_Implementation(
+            'IMPLEMENTATION'
+        );
+
+        $this->assertEquals('IMPLEMENTATION', $implBinder->getImplementation());
+    }
+
+    public function testShouldCreateInstanceOfClassWithNoDependencies()
+    {
+        $implBinder = new Horde_Injector_Binder_Implementation(
+            'Horde_Injector_Binder_ImplementationTest__NoDependencies'
+        );
+
+        $this->assertType(
+            'Horde_Injector_Binder_ImplementationTest__NoDependencies',
+            $implBinder->create($this->_getInjectorNeverCallMock())
+        );
+    }
+
+    public function testShouldCreateInstanceOfClassWithTypedDependencies()
+    {
+        $implBinder = new Horde_Injector_Binder_Implementation(
+            'Horde_Injector_Binder_ImplementationTest__TypedDependency'
+        );
+
+        $createdInstance = $implBinder->create($this->_getInjectorReturnsNoDependencyObject());
+
+        $this->assertType(
+            'Horde_Injector_Binder_ImplementationTest__TypedDependency',
+            $createdInstance
+        );
+
+        $this->assertType(
+            'Horde_Injector_Binder_ImplementationTest__NoDependencies',
+            $createdInstance->dep
+        );
+    }
+
+    /**
+     * @expectedException Horde_Injector_Exception
+     */
+    public function testShouldThrowExceptionWhenTryingToCreateInstanceOfClassWithUntypedDependencies()
+    {
+        $implBinder = new Horde_Injector_Binder_Implementation(
+            'Horde_Injector_Binder_ImplementationTest__UntypedDependency'
+        );
+
+        $implBinder->create($this->_getInjectorNeverCallMock());
+    }
+
+    public function testShouldUseDefaultValuesFromUntypedOptionalParameters()
+    {
+        $implBinder = new Horde_Injector_Binder_Implementation(
+            'Horde_Injector_Binder_ImplementationTest__UntypedOptionalDependency'
+        );
+
+        $createdInstance = $implBinder->create($this->_getInjectorNeverCallMock());
+
+        $this->assertEquals('DEPENDENCY', $createdInstance->dep);
+    }
+
+    /**
+     * @expectedException ReflectionException
+     */
+    public function testShouldThrowExceptionIfRequestedClassIsNotDefined()
+    {
+        $implBinder = new Horde_Injector_Binder_Implementation(
+            'CLASS_DOES_NOT_EXIST'
+        );
+
+        $implBinder->create($this->_getInjectorNeverCallMock());
+    }
+
+    /**
+     * @expectedException Horde_Injector_Exception
+     */
+    public function testShouldThrowExcpetionIfImplementationIsAnInterface()
+    {
+        $implBinder = new Horde_Injector_Binder_Implementation(
+            'Horde_Injector_Binder_ImplementationTest__Interface'
+        );
+
+        $implBinder->create($this->_getInjectorNeverCallMock());
+    }
+
+    /**
+     * @expectedException Horde_Injector_Exception
+     */
+    public function testShouldThrowExcpetionIfImplementationIsAnAbstractClass()
+    {
+        $implBinder = new Horde_Injector_Binder_Implementation(
+            'Horde_Injector_Binder_ImplementationTest__AbstractClass'
+        );
+
+        $implBinder->create($this->_getInjectorNeverCallMock());
+    }
+
+    public function testShouldCallSetterMethodsWithNoDependenciesIfRequested()
+    {
+        $implBinder = new Horde_Injector_Binder_Implementation(
+            'Horde_Injector_Binder_ImplementationTest__SetterNoDependencies'
+        );
+        $implBinder->bindSetter('setDependency');
+
+        $instance = $implBinder->create($this->_getInjectorNeverCallMock());
+
+        $this->assertType(
+            'Horde_Injector_Binder_ImplementationTest__SetterNoDependencies',
+            $instance
+        );
+
+        $this->assertEquals('CALLED', $instance->setterDep);
+    }
+
+    public function testShouldCallSetterMethodsWithDependenciesIfRequested()
+    {
+        $implBinder = new Horde_Injector_Binder_Implementation(
+            'Horde_Injector_Binder_ImplementationTest__SetterHasDependencies'
+        );
+        $implBinder->bindSetter('setDependency');
+
+        $instance = $implBinder->create($this->_getInjectorReturnsNoDependencyObject());
+
+        $this->assertType(
+            'Horde_Injector_Binder_ImplementationTest__SetterHasDependencies',
+            $instance
+        );
+
+        $this->assertType(
+            'Horde_Injector_Binder_ImplementationTest__NoDependencies',
+            $instance->setterDep
+        );
+    }
+
+    private function _getInjectorNeverCallMock()
+    {
+        $injector = $this->getMockSkipConstructor('Horde_Injector', array('getInstance'));
+        $injector->expects($this->never())
+            ->method('getInstance');
+        return $injector;
+    }
+
+    private function _getInjectorReturnsNoDependencyObject()
+    {
+        $injector = $this->getMockSkipConstructor('Horde_Injector', array('getInstance'));
+        $injector->expects($this->once())
+            ->method('getInstance')
+            ->with($this->equalTo('Horde_Injector_Binder_ImplementationTest__NoDependencies'))
+            ->will($this->returnValue(new Horde_Injector_Binder_ImplementationTest__NoDependencies()));
+        return $injector;
+    }
+}
+
+/**
+ * Used by preceeding tests!!!
+ */
+
+class Horde_Injector_Binder_ImplementationTest__NoDependencies
+{}
+
+class Horde_Injector_Binder_ImplementationTest__TypedDependency
+{
+    public $dep;
+
+    public function __construct(Horde_Injector_Binder_ImplementationTest__NoDependencies $dep)
+    {
+        $this->dep = $dep;
+    }
+}
+
+class Horde_Injector_Binder_ImplementationTest__UntypedDependency
+{
+    public function __construct($dep) {}
+}
+
+class Horde_Injector_Binder_ImplementationTest__UntypedOptionalDependency
+{
+    public $dep;
+
+    public function __construct($dep = 'DEPENDENCY')
+    {
+        $this->dep = $dep;
+    }
+}
+
+interface Horde_Injector_Binder_ImplementationTest__Interface
+{}
+
+abstract class Horde_Injector_Binder_ImplementationTest__AbstractClass
+{}
+
+class Horde_Injector_Binder_ImplementationTest__SetterNoDependencies
+{
+    public $setterDep;
+
+    public function setDependency()
+    {
+        $this->setterDep = 'CALLED';
+    }
+}
+
+class Horde_Injector_Binder_ImplementationTest__SetterHasDependencies
+{
+    public $setterDep;
+
+    public function setDependency(Horde_Injector_Binder_ImplementationTest__NoDependencies $setterDep)
+    {
+        $this->setterDep = $setterDep;
+    }
+}
diff --git a/framework/Injector/test/Horde/Injector/BinderTest.php b/framework/Injector/test/Horde/Injector/BinderTest.php
new file mode 100644 (file)
index 0000000..1564e30
--- /dev/null
@@ -0,0 +1,55 @@
+<?php
+class Horde_Injector_BinderTest extends Horde_Test_Case
+{
+    /**
+     * provider returns binder1, binder2, shouldEqual, errmesg
+     */
+    public function binderIsEqualProvider()
+    {
+        return array(
+            array(
+                new Horde_Injector_Binder_Implementation('foobar'),
+                new Horde_Injector_Binder_Factory('factory', 'method'),
+                false, "Implementation_Binder should not equal Factory binder"
+            ),
+            array(
+                new Horde_Injector_Binder_Implementation('foobar'),
+                new Horde_Injector_Binder_Implementation('foobar'),
+                true, "Implementation Binders both reference concrete class foobar"
+            ),
+            array(
+                new Horde_Injector_Binder_Implementation('foobar'),
+                new Horde_Injector_Binder_Implementation('otherimpl'),
+                false, "Implementation Binders do not have same implementation set"
+            ),
+            array(
+                new Horde_Injector_Binder_Factory('factory', 'method'),
+                new Horde_Injector_Binder_Implementation('foobar'),
+                false, "Implementation_Binder should not equal Factory binder"
+            ),
+            array(
+                new Horde_Injector_Binder_Factory('foobar', 'create'),
+                new Horde_Injector_Binder_Factory('foobar', 'create'),
+                true, "Factory Binders both reference factory class foobar::create"
+            ),
+            array(
+                new Horde_Injector_Binder_Factory('foobar', 'create'),
+                new Horde_Injector_Binder_Factory('otherimpl', 'create'),
+                false, "Factory Binders do not have same factory class set, so they should not be equal"
+            ),
+            array(
+                new Horde_Injector_Binder_Factory('foobar', 'create'),
+                new Horde_Injector_Binder_Factory('foobar', 'otherMethod'),
+                false, "Factory Binders are set to the same class but different methods. They should not be equal"
+            ),
+        );
+    }
+
+    /**
+     * @dataProvider binderIsEqualProvider
+     */
+    public function testBinderEqualFunction($binderA, $binderB, $shouldEqual, $message)
+    {
+        $this->assertEquals($shouldEqual, $binderA->equals($binderB), $message);
+    }
+}
diff --git a/framework/Injector/test/Horde/Injector/InjectorTest.php b/framework/Injector/test/Horde/Injector/InjectorTest.php
new file mode 100644 (file)
index 0000000..14ba179
--- /dev/null
@@ -0,0 +1,243 @@
+<?php
+class Horde_Injector_InjectorTest extends PHPUnit_Framework_TestCase
+{
+    public function testShouldGetDefaultImplementationBinder()
+    {
+        $topLevel = $this->getMock('Horde_Injector_TopLevel', array('getBinder'));
+        $topLevel->expects($this->once())
+            ->method('getBinder')
+            ->with($this->equalTo('UNBOUND_INTERFACE'))
+            ->will($this->returnValue('RETURNED_BINDING'));
+
+        $injector = new Horde_Injector($topLevel);
+
+        $this->assertEquals('RETURNED_BINDING', $injector->getBinder('UNBOUND_INTERFACE'));
+    }
+
+    public function testShouldGetManuallyBoundBinder()
+    {
+        $injector = new Horde_Injector(new Horde_Injector_TopLevel());
+        $binder = new Horde_Injector_Binder_Mock();
+        $injector->addBinder('BOUND_INTERFACE', $binder);
+        $this->assertSame($binder, $injector->getBinder('BOUND_INTERFACE'));
+    }
+
+    public function testShouldProvideMagicFactoryMethodForBinderAddition()
+    {
+        $injector = new Horde_Injector(new Horde_Injector_TopLevel());
+
+        // binds a Horde_Injector_Binder_Mock object
+        $this->assertType('Horde_Injector_Binder_Mock', $injector->bindMock('BOUND_INTERFACE'));
+        $this->assertType('Horde_Injector_Binder_Mock', $injector->getBinder('BOUND_INTERFACE'));
+    }
+
+    /**
+     * @expectedException BadMethodCallException
+     */
+    public function testShouldThrowExceptionIfInterfaceNameIsNotPassedToMagicFactoryMethodForBinderAddition()
+    {
+        $injector = new Horde_Injector($this->_getTopLevelNeverCalledMock());
+        $injector->bindMock();
+    }
+
+    public function testShouldReturnItselfWhenInjectorRequested()
+    {
+        $injector = new Horde_Injector($this->_getTopLevelNeverCalledMock());
+        $this->assertSame($injector, $injector->getInstance('Horde_Injector'));
+    }
+
+    /**
+     * Would love to use PHPUnit's mock object here istead of Horde_Injector_Binder_Mock but you
+     * can't be sure the expected resulting object is the same object you told the mock to return.
+     * This is because Mock clone objects passed to mocked methods.
+     *
+     * http://www.phpunit.de/ticket/120
+     *
+     * @author Bob McKee <bmckee@bywires.com>
+     */
+    public function testCreateInstancePassesCurrentInjectorScopeToBinderForCreation()
+    {
+        $injector = new Horde_Injector(new Horde_Injector_TopLevel());
+        $injector->addBinder('BOUND_INTERFACE', new Horde_Injector_Binder_Mock());
+
+        // normally you wouldn't get an injector back; the binder would create something and return
+        // it to you.  here we are just confirming that the proper injector was passed to the
+        // binder's create method.
+        $this->assertEquals($injector, $injector->createInstance('BOUND_INTERFACE'));
+    }
+
+    public function testShouldNotReturnSharedObjectOnCreate()
+    {
+        $injector = $this->_createInjector();
+        //this call will cache this class on the injector
+        $stdclass = $injector->getInstance('StdClass');
+
+        $this->assertNotSame($stdclass, $injector->createInstance('StdClass'));
+    }
+
+    public function testShouldNotShareObjectCreatedUsingCreate()
+    {
+        $injector = $this->_createInjector();
+
+        // this call should not store the instance on the injector
+        $stdclass = $injector->createInstance('StdClass');
+
+        $this->assertNotSame($stdclass, $injector->getInstance('StdClass'));
+    }
+
+    public function testChildSharesInstancesOfParent()
+    {
+        $injector = $this->_createInjector();
+
+        //this call will store the created instance on $injector
+        $stdclass = $injector->getInstance('StdClass');
+
+        // create a child injector and ensure that the stdclass returned is the same
+        $child = $injector->createChildInjector();
+        $this->assertSame($stdclass, $child->getInstance('StdClass'));
+    }
+
+    private function _createInjector()
+    {
+        return new Horde_Injector(new Horde_Injector_TopLevel());
+    }
+
+    public function testShouldReturnSharedInstanceIfRequested()
+    {
+        $injector = new Horde_Injector($this->_getTopLevelNeverCalledMock());
+        $instance = new StdClass();
+        $injector->setInstance('INSTANCE_INTERFACE', $instance);
+        $this->assertSame($instance, $injector->getInstance('INSTANCE_INTERFACE'));
+    }
+
+    /**
+     * this test should test that when you override a binding in a child injector,
+     * that the child does not create a new version of the object if the binding has not changed
+     */
+    public function testChildInjectorDoNotSaveBindingLocallyWhenBinderIsSameAsParent()
+    {
+        // we need to set a class for an instance on the parent
+        $injector = new Horde_Injector(new Horde_Injector_TopLevel());
+        $injector->addBinder('FooBarInterface', new Horde_Injector_Binder_Implementation('StdClass'));
+
+        // getInstance will save $returnedObject and return it again later when FooBarInterface is requested
+        $returnedObject = $injector->getInstance('FooBarInterface');
+
+        $childInjector = $injector->createChildInjector();
+        // add same binding again to child
+        $childInjector->addBinder('FooBarInterface', new Horde_Injector_Binder_Implementation('StdClass'));
+
+        $this->assertSame($returnedObject, $childInjector->getInstance('FooBarInterface'),
+            "Child should have returned object refrence from parnet because added binder was identical to the parent binder");
+    }
+
+    /**
+     * this test should test that when you override a binding in a child injector,
+     * that the child creates a new version of the object, and not the parent's cached version
+     * if the binding is changed
+     */
+    public function testChildInjectorsDoNotAskParentForInstanceIfBindingIsSet()
+    {
+        $mockTopLevel = $this->getMock('Horde_Injector_TopLevel', array('getInstance'));
+        $mockTopLevel->expects($this->never())->method('getInstance');
+        $injector = new Horde_Injector($mockTopLevel);
+
+        $injector->addBinder('StdClass', new Horde_Injector_Binder_Mock());
+        $injector->getInstance('StdClass');
+    }
+
+    public function testChildInjectorAsksParentForInstance()
+    {
+        $topLevelMock = $this->getMock('Horde_Injector_TopLevel', array('getInstance'));
+
+        $topLevelMock->expects($this->once())
+            ->method('getInstance')
+            ->with('StdClass');
+
+        $injector = new Horde_Injector($topLevelMock);
+
+        $injector->getInstance('StdClass');
+    }
+
+    /**
+     * Would love to use PHPUnit's mock object here istead of Horde_Injector_Binder_Mock but you
+     * can't be sure the expected resulting object is the same object you told the mock to return.
+     * This is because Mock clone objects passed to mocked methods.
+     *
+     * http://www.phpunit.de/ticket/120
+     *
+     * @author Bob McKee <bmckee@bywires.com>
+     */
+    public function testShouldCreateAndStoreSharedObjectIfOneDoesNotAlreadyExist()
+    {
+        $injector = new Horde_Injector(new Horde_Injector_TopLevel());
+        $injector->addBinder('BOUND_INTERFACE', new Horde_Injector_Binder_Mock());
+
+        // should call "createInstance" and then "setInstance" on the result
+        // normally you wouldn't get an injector back; the binder would create something and return
+        // it to you.  here we are just confirming that the proper injector was passed to the
+        // binder's create method.
+        $this->assertSame($injector, $injector->getInstance('BOUND_INTERFACE'));
+
+        // should just return stored instance
+        // the injector sent to the "create" method noted above should also be returned here.
+        $this->assertSame($injector, $injector->getInstance('BOUND_INTERFACE'));
+    }
+
+    public function testShouldCreateAndStoreSharedObjectInstanceIfDefaultTopLevelBinderIsUsed()
+    {
+        $injector = new Horde_Injector(new Horde_Injector_TopLevel());
+
+        $class  = $injector->getInstance('StdClass');
+        $class2 = $injector->getInstance('StdClass');
+
+        $this->assertSame($class, $class2, "Injector did not return same object on consecutive getInstance calls");
+    }
+
+    public function testCreateChildInjectorReturnsDifferentInjector()
+    {
+        $injector = new Horde_Injector($this->_getTopLevelNeverCalledMock());
+        $childInjector = $injector->createChildInjector();
+        $this->assertType('Horde_Injector', $childInjector);
+        $this->assertNotSame($injector, $childInjector);
+    }
+
+    public function testShouldAllowChildInjectorsAccessToParentInjectorBindings()
+    {
+        $mockInjector = $this->getMock('Horde_Injector_TopLevel', array('getBinder'));
+        $mockInjector->expects($this->any()) // this gets called once in addBinder
+            ->method('getBinder')
+            ->with('BOUND_INTERFACE')
+            ->will($this->returnValue(new Horde_Injector_Binder_Mock()));
+
+        $injector = new Horde_Injector($mockInjector);
+        $binder = new Horde_Injector_Binder_Mock();
+        $injector->addBinder('BOUND_INTERFACE', $binder);
+        $childInjector = $injector->createChildInjector();
+        $this->assertSame($binder, $childInjector->getBinder('BOUND_INTERFACE'));
+    }
+
+    private function _getTopLevelNeverCalledMock()
+    {
+        $topLevel = $this->getMock('Horde_Injector_TopLevel', array('getBinder', 'getInstance'));
+        $topLevel->expects($this->never())->method('getBinder');
+        return $topLevel;
+    }
+}
+
+/**
+ * Used by preceeding tests!!!
+ */
+class Horde_Injector_Binder_Mock implements Horde_Injector_Binder
+{
+    private $_interface;
+    public function create(Horde_Injector $injector)
+    {
+        return $injector;
+    }
+
+    public function equals(Horde_Injector_Binder $otherBinder)
+    {
+        return $otherBinder === $this;
+    }
+}