--- /dev/null
+<?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];
+ }
+}
--- /dev/null
+<?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);
+}
--- /dev/null
+<?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);
+ }
+}
--- /dev/null
+<?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)
+ );
+ }
+ }
+}
--- /dev/null
+<?php
+class Horde_Injector_Exception extends Exception
+{}
--- /dev/null
+<?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);
+}
--- /dev/null
+<?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;
+ }
+}
--- /dev/null
+<?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>
--- /dev/null
+<?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();
+}
--- /dev/null
+<?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;
+ }
+}
--- /dev/null
+<?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;
+ }
+}
--- /dev/null
+<?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);
+ }
+}
--- /dev/null
+<?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;
+ }
+}