From 079d550cbef5e7b08743d7917e1efb23b5f2d488 Mon Sep 17 00:00:00 2001 From: Chuck Hagenbuch Date: Mon, 21 Sep 2009 00:08:45 -0400 Subject: [PATCH] Initial Horde_Injector dependency injection container, contributed by Blue State Digital. --- framework/Injector/lib/Horde/Injector.php | 197 +++++++++++++++++ framework/Injector/lib/Horde/Injector/Binder.php | 13 ++ .../Injector/lib/Horde/Injector/Binder/Factory.php | 92 ++++++++ .../lib/Horde/Injector/Binder/Implementation.php | 97 ++++++++ .../Injector/lib/Horde/Injector/Exception.php | 3 + framework/Injector/lib/Horde/Injector/Scope.php | 26 +++ framework/Injector/lib/Horde/Injector/TopLevel.php | 40 ++++ framework/Injector/package.xml | 69 ++++++ .../Injector/test/Horde/Injector/AllTests.php | 45 ++++ .../test/Horde/Injector/Binder/FactoryTest.php | 101 +++++++++ .../Horde/Injector/Binder/ImplementationTest.php | 215 ++++++++++++++++++ .../Injector/test/Horde/Injector/BinderTest.php | 55 +++++ .../Injector/test/Horde/Injector/InjectorTest.php | 243 +++++++++++++++++++++ 13 files changed, 1196 insertions(+) create mode 100644 framework/Injector/lib/Horde/Injector.php create mode 100644 framework/Injector/lib/Horde/Injector/Binder.php create mode 100644 framework/Injector/lib/Horde/Injector/Binder/Factory.php create mode 100644 framework/Injector/lib/Horde/Injector/Binder/Implementation.php create mode 100644 framework/Injector/lib/Horde/Injector/Exception.php create mode 100644 framework/Injector/lib/Horde/Injector/Scope.php create mode 100644 framework/Injector/lib/Horde/Injector/TopLevel.php create mode 100644 framework/Injector/package.xml create mode 100644 framework/Injector/test/Horde/Injector/AllTests.php create mode 100644 framework/Injector/test/Horde/Injector/Binder/FactoryTest.php create mode 100644 framework/Injector/test/Horde/Injector/Binder/ImplementationTest.php create mode 100644 framework/Injector/test/Horde/Injector/BinderTest.php create mode 100644 framework/Injector/test/Horde/Injector/InjectorTest.php diff --git a/framework/Injector/lib/Horde/Injector.php b/framework/Injector/lib/Horde/Injector.php new file mode 100644 index 000000000..7383e5f8b --- /dev/null +++ b/framework/Injector/lib/Horde/Injector.php @@ -0,0 +1,197 @@ + + * @author James Pepin + */ +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 index 000000000..fafa479c5 --- /dev/null +++ b/framework/Injector/lib/Horde/Injector/Binder.php @@ -0,0 +1,13 @@ +getInstance('Collaborator'), new MyOtherClass(17)); + * } + * ... + * + * @author Bob Mckee + * @author James Pepin + */ +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 index 000000000..7a65351ef --- /dev/null +++ b/framework/Injector/lib/Horde/Injector/Binder/Implementation.php @@ -0,0 +1,97 @@ +_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 index 000000000..7facb86cb --- /dev/null +++ b/framework/Injector/lib/Horde/Injector/Exception.php @@ -0,0 +1,3 @@ + + * @author James Pepin + */ +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 index 000000000..3d9ecf938 --- /dev/null +++ b/framework/Injector/package.xml @@ -0,0 +1,69 @@ + + + Injector + pear.horde.org + Horde dependency injection container + A depedency injection container for Horde. + + Chuck Hagenbuch + chuck + chuck@horde.org + yes + + 2009-09-20 + + 0.1.0 + 0.1.0 + + + beta + beta + + BSD + + * Initial release, contributed by Blue State Digital + + + + + + + + + + + + + + + + + + + + + + + + 5.2.0 + + + 1.7.0 + + + + + + + + + + + + + + + diff --git a/framework/Injector/test/Horde/Injector/AllTests.php b/framework/Injector/test/Horde/Injector/AllTests.php new file mode 100644 index 000000000..e0306e9b0 --- /dev/null +++ b/framework/Injector/test/Horde/Injector/AllTests.php @@ -0,0 +1,45 @@ +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 index 000000000..43188f18a --- /dev/null +++ b/framework/Injector/test/Horde/Injector/Binder/FactoryTest.php @@ -0,0 +1,101 @@ +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 index 000000000..6a600774c --- /dev/null +++ b/framework/Injector/test/Horde/Injector/Binder/ImplementationTest.php @@ -0,0 +1,215 @@ +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 index 000000000..1564e30a5 --- /dev/null +++ b/framework/Injector/test/Horde/Injector/BinderTest.php @@ -0,0 +1,55 @@ +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 index 000000000..14ba17958 --- /dev/null +++ b/framework/Injector/test/Horde/Injector/InjectorTest.php @@ -0,0 +1,243 @@ +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 + */ + 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 + */ + 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; + } +} -- 2.11.0