diff --git a/src/Definition/ClassDefinition.php b/src/Definition/ClassDefinition.php index ce3f5a06..3c9d833c 100644 --- a/src/Definition/ClassDefinition.php +++ b/src/Definition/ClassDefinition.php @@ -35,6 +35,11 @@ public function addMethod($method, $isRequired = null) $this->methods[$method] = (bool) $isRequired; } + /** + * @param $method + * @param $parameterName + * @param array $parameterInfo (keys: required, type) + */ public function addMethodParameter($method, $parameterName, array $parameterInfo) { if (!array_key_exists($method, $this->methods)) { diff --git a/src/Di.php b/src/Di.php index 93e73461..18e8be3b 100644 --- a/src/Di.php +++ b/src/Di.php @@ -173,10 +173,19 @@ public function newInstance($name, array $params = array(), $isShared = true) if (array_key_exists('__construct', $injectionMethods)) { unset($injectionMethods['__construct']); } - } elseif (is_callable($instantiator)) { + } elseif (is_callable($instantiator, false)) { $object = $this->createInstanceViaCallback($instantiator, $params, $alias); } else { - throw new Exception\RuntimeException('Invalid instantiator'); + if (is_array($instantiator)) { + $msg = sprintf( + 'Invalid instantiator: %s::%s() is not callable.', + isset($instantiator[0]) ? $instantiator[0] : 'NoClassGiven', + isset($instantiator[1]) ? $instantiator[1] : 'NoMethodGiven' + ); + } else { + $msg = 'Invalid instantiator: ' . $instantiator; + } + throw new \RuntimeException($msg); } if ($isShared) { @@ -197,17 +206,35 @@ public function newInstance($name, array $params = array(), $isShared = true) $instanceConfiguration = $instanceManager->getConfiguration($name); if ($instanceConfiguration['injections']) { - $objectsToInject = array(); - foreach ($instanceConfiguration['injections'] as $classAliasToInject) { - $objectsToInject[] = $this->get($classAliasToInject, $params); + $objectsToInject = $methodsToCall = array(); + foreach ($instanceConfiguration['injections'] as $injectName => $injectValue) { + if (is_int($injectName) && is_string($injectValue)) { + $objectsToInject[] = $this->get($injectValue, $params); + } elseif (is_string($injectName) && is_array($injectValue)) { + if (is_string(key($injectValue))) { + $methodsToCall[] = array('method' => $injectName, 'args' => $injectValue); + } else { + foreach ($injectValue as $methodCallArgs) { + $methodsToCall[] = array('method' => $injectName, 'args' => $methodCallArgs); + } + } + } elseif (is_object($injectValue)) { + $objectsToInject[] = $injectValue; + } elseif (is_int($injectName) && is_array($injectValue)) { + // @todo must find method name somehow + throw new Exception\RuntimeException( + 'An injection was provided with a keyed index and an array of data, try using' + . ' the name of a particular method as a key for your injection data.' + ); + } } if ($objectsToInject) { - // @todo this needs to be optimized foreach ($objectsToInject as $objectToInject) { foreach ($injectionMethods as $injectionMethod => $methodIsRequired) { if ($methodParams = $definitions->getMethodParameters($class, $injectionMethod)) { foreach ($methodParams as $methodParam) { - if ($this->isSubclassOf(get_class($objectToInject), $methodParam[1])) { + if (get_class($objectToInject) == $methodParam[1] || + $this->isSubclassOf(get_class($objectToInject), $methodParam[1])) { $callParams = $this->resolveMethodParameters(get_class($object), $injectionMethod, array($methodParam[0] => $objectToInject), false, $alias, true ); @@ -221,6 +248,14 @@ public function newInstance($name, array $params = array(), $isShared = true) } } } + if ($methodsToCall) { + foreach ($methodsToCall as $methodInfo) { + $callParams = $this->resolveMethodParameters(get_class($object), $methodInfo['method'], + $methodInfo['args'], false, $alias, true + ); + call_user_func_array(array($object, $methodInfo['method']), $callParams); + } + } } } @@ -234,21 +269,23 @@ public function newInstance($name, array $params = array(), $isShared = true) * @todo * @param unknown_type $object */ - public function injectObjects($targetObject, array $objects = array()) - { - if ($objects === array()) { - throw new \Exception('Not yet implmeneted'); - } + /* + public function injectObjects($targetObject, array $objects = array()) + { + if ($objects === array()) { + throw new \Exception('Not yet implmeneted'); + } - $targetClass = get_class($targetObject); - if (!$this->definitions()->hasClass($targetClass)) { - throw new Exception\RuntimeException('A definition for this object type cannot be found'); - } + $targetClass = get_class($targetObject); + if (!$this->definitions()->hasClass($targetClass)) { + throw new Exception\RuntimeException('A definition for this object type cannot be found'); + } - foreach ($objects as $objectToInject) { + foreach ($objects as $objectToInject) { - } - } + } + } + */ /** * Retrieve a class instance based on class name @@ -303,6 +340,8 @@ protected function createInstanceViaCallback($callback, $params, $alias) if (is_array($callback)) { $class = (is_object($callback[0])) ? get_class($callback[0]) : $callback[0]; $method = $callback[1]; + } elseif (is_string($callback) && strpos($callback, '::') !== false) { + list($class, $method) = explode('::', $callback, 2); } else { throw new Exception\RuntimeException('Invalid callback type provided to ' . __METHOD__); } @@ -397,29 +436,34 @@ protected function resolveMethodParameters($class, $method, array $callTimeUserP if (isset($callTimeUserParams[$fqName]) || isset($callTimeUserParams[$name])) { // @todo FQ Name in call time params - if (isset($callTimeUserParams[$fqName])) throw \Exception('Implementation incomplete for fq names'); + if (isset($callTimeUserParams[$fqName])) { + $callTimeCurValue =& $callTimeUserParams[$fqName]; + } else { + $callTimeCurValue =& $callTimeUserParams[$name]; + } - if (is_string($callTimeUserParams[$name])) { - if ($this->instanceManager->hasAlias($callTimeUserParams[$name])) { + if (is_string($callTimeCurValue)) { + if ($this->instanceManager->hasAlias($callTimeCurValue)) { // was an alias provided? $computedParams['required'][$fqName] = array( $callTimeUserParams[$name], - $this->instanceManager->getClassFromAlias($callTimeUserParams[$name]) + $this->instanceManager->getClassFromAlias($callTimeCurValue) ); } elseif ($this->definitions->hasClass($callTimeUserParams[$name])) { // was a known class provided? $computedParams['required'][$fqName] = array( - $callTimeUserParams[$name], - $callTimeUserParams[$name] + $callTimeCurValue, + $callTimeCurValue ); } else { // must be a value - $computedParams['value'][$fqName] = $callTimeUserParams[$name]; + $computedParams['value'][$fqName] = $callTimeCurValue; } } else { // int, float, null, object, etc - $computedParams['value'][$fqName] = $callTimeUserParams[$name]; + $computedParams['value'][$fqName] = $callTimeCurValue; } + unset($callTimeCurValue); continue; } @@ -433,26 +477,35 @@ protected function resolveMethodParameters($class, $method, array $callTimeUserP if (isset($iConfig[$thisIndex]['parameters'][$fqName]) || isset($iConfig[$thisIndex]['parameters'][$name])) { // @todo FQ Name in config parameters - if (isset($iConfig[$thisIndex]['parameters'][$fqName])) throw \Exception('Implementation incomplete for fq names'); + if (isset($iConfig[$thisIndex]['parameters'][$fqName])) { + $iConfigCurValue =& $iConfig[$thisIndex]['parameters'][$fqName]; + } else { + $iConfigCurValue =& $iConfig[$thisIndex]['parameters'][$name]; + } - if (is_string($iConfig[$thisIndex]['parameters'][$name]) + if (is_string($iConfigCurValue) && $type === false) { - $computedParams['value'][$fqName] = $iConfig[$thisIndex]['parameters'][$name]; - } elseif (is_string($iConfig[$thisIndex]['parameters'][$name]) - && isset($aliases[$iConfig[$thisIndex]['parameters'][$name]])) { + $computedParams['value'][$fqName] = $iConfigCurValue; + } elseif (is_string($iConfigCurValue) + && isset($aliases[$iConfigCurValue])) { $computedParams['required'][$fqName] = array( $iConfig[$thisIndex]['parameters'][$name], - $this->instanceManager->getClassFromAlias($iConfig[$thisIndex]['parameters'][$name]) + $this->instanceManager->getClassFromAlias($iConfigCurValue) ); - } elseif (is_string($iConfig[$thisIndex]['parameters'][$name]) - && $this->definitions->hasClass($iConfig[$thisIndex]['parameters'][$name])) { + } elseif (is_string($iConfigCurValue) + && $this->definitions->hasClass($iConfigCurValue)) { $computedParams['required'][$fqName] = array( - $iConfig[$thisIndex]['parameters'][$name], - $iConfig[$thisIndex]['parameters'][$name] + $iConfigCurValue, + $iConfigCurValue ); + } elseif (is_object($iConfigCurValue) + && $iConfigCurValue instanceof \Closure + && $type !== 'Closure') { + $computedParams['value'][$fqName] = $iConfigCurValue(); } else { - $computedParams['value'][$fqName] = $iConfig[$thisIndex]['parameters'][$name]; + $computedParams['value'][$fqName] = $iConfigCurValue; } + unset($iConfigCurValue); continue 2; } diff --git a/test/ConfigurationTest.php b/test/ConfigurationTest.php index 50680421..1f94962d 100644 --- a/test/ConfigurationTest.php +++ b/test/ConfigurationTest.php @@ -72,53 +72,6 @@ public function testConfigurationCanConfigureBuilderDefinitionFromIni() } - public function testCanSetInstantiatorToStaticFactory() - { - $config = new Configuration(array( - 'definition' => array( - 'class' => array( - 'ZendTest\Di\TestAsset\DummyParams' => array( - 'instantiator' => array('ZendTest\Di\TestAsset\StaticFactory', 'factory'), - ), - 'ZendTest\Di\TestAsset\StaticFactory' => array( - 'methods' => array( - 'factory' => array( - 'struct' => array( - 'type' => 'ZendTest\Di\TestAsset\Struct', - 'required' => true, - ), - 'params' => array( - 'required' => true, - ), - ), - ), - ), - ), - ), - 'instance' => array( - 'ZendTest\Di\TestAsset\DummyParams' => array( - 'parameters' => array( - 'struct' => 'ZendTest\Di\TestAsset\Struct', - 'params' => array( - 'foo' => 'bar', - ), - ), - ), - 'ZendTest\Di\TestAsset\Struct' => array( - 'parameters' => array( - 'param1' => 'hello', - 'param2' => 'world', - ), - ), - ), - )); - $di = new Di(); - $di->configure($config); - $dummyParams = $di->get('ZendTest\Di\TestAsset\DummyParams'); - $this->assertEquals($dummyParams->params['param1'], 'hello'); - $this->assertEquals($dummyParams->params['param2'], 'world'); - $this->assertEquals($dummyParams->params['foo'], 'bar'); - $this->assertArrayNotHasKey('methods', $di->definitions()->hasMethods('ZendTest\Di\TestAsset\StaticFactory')); - } + } diff --git a/test/DiTest.php b/test/DiTest.php index 3f2d874e..96ce25ae 100644 --- a/test/DiTest.php +++ b/test/DiTest.php @@ -2,15 +2,17 @@ namespace ZendTest\Di; -use Zend\Debug; - use Zend\Di\Di, - PHPUnit_Framework_TestCase as TestCase; + Zend\Di\DefinitionList, + Zend\Di\InstanceManager, + Zend\Di\Configuration, + Zend\Di\Definition; + -class DiTest extends TestCase +class DiTest extends \PHPUnit_Framework_TestCase { - public function testDependencyInjectorHasBuiltInImplementations() + public function testDiHasBuiltInImplementations() { $di = new Di(); $this->assertInstanceOf('Zend\Di\InstanceManager', $di->instanceManager()); @@ -20,7 +22,24 @@ public function testDependencyInjectorHasBuiltInImplementations() $this->assertInstanceOf('Zend\Di\DefinitionList', $definitions); $this->assertInstanceOf('Zend\Di\Definition\RuntimeDefinition', $definitions->top()); } - + + public function testDiConstructorCanTakeDependencies() + { + $dl = new DefinitionList(array()); + $im = new InstanceManager(); + $cg = new Configuration(array()); + $di = new Di($dl, $im, $cg); + + $this->assertSame($dl, $di->definitions()); + $this->assertSame($im, $di->instanceManager()); + + $di->setDefinitionList($dl); + $di->setInstanceManager($im); + + $this->assertSame($dl, $di->definitions()); + $this->assertSame($im, $di->instanceManager()); + } + public function testPassingInvalidDefinitionRaisesException() { $di = new Di(); @@ -91,7 +110,90 @@ public function testNewInstanceReturnsInstanceThatIsNotSharedWithGet() $this->assertInstanceOf('ZendTest\Di\TestAsset\BasicClass', $obj2); $this->assertNotSame($obj1, $obj2); } - + + public function testNewInstanceCanHandleClassesCreatedByCallback() + { + $definitionList = new DefinitionList(array( + $classdef = new Definition\ClassDefinition('ZendTest\Di\TestAsset\CallbackClasses\A'), + new Definition\RuntimeDefinition() + )); + $classdef->setInstantiator('ZendTest\Di\TestAsset\CallbackClasses\A::factory'); + + $di = new Di($definitionList); + $a = $di->get('ZendTest\Di\TestAsset\CallbackClasses\A'); + $this->assertInstanceOf('ZendTest\Di\TestAsset\CallbackClasses\A', $a); + } + + public function testNewInstanceCanHandleComplexCallback() + { + $definitionList = new DefinitionList(array( + $classdefB = new Definition\ClassDefinition('ZendTest\Di\TestAsset\CallbackClasses\B'), + $classdefC = new Definition\ClassDefinition('ZendTest\Di\TestAsset\CallbackClasses\C'), + new Definition\RuntimeDefinition() + )); + + $classdefB->setInstantiator('ZendTest\Di\TestAsset\CallbackClasses\B::factory'); + $classdefB->addMethod('factory', true); + $classdefB->addMethodParameter('factory', 'c', array('type' => 'ZendTest\Di\TestAsset\CallbackClasses\C', 'required' => true)); + $classdefB->addMethodParameter('factory', 'params', array('type' => 'Array', 'required'=>false)); + + $di = new Di($definitionList); + $b = $di->get('ZendTest\Di\TestAsset\CallbackClasses\B', array('params'=>array('foo' => 'bar'))); + $this->assertInstanceOf('ZendTest\Di\TestAsset\CallbackClasses\B', $b); + $this->assertInstanceOf('ZendTest\Di\TestAsset\CallbackClasses\C', $b->c); + $this->assertEquals(array('foo' => 'bar'), $b->params); + } + + +// public function testCanSetInstantiatorToStaticFactory() +// { +// $config = new Configuration(array( +// 'definition' => array( +// 'class' => array( +// 'ZendTest\Di\TestAsset\DummyParams' => array( +// 'instantiator' => array('ZendTest\Di\TestAsset\StaticFactory', 'factory'), +// ), +// 'ZendTest\Di\TestAsset\StaticFactory' => array( +// 'methods' => array( +// 'factory' => array( +// 'struct' => array( +// 'type' => 'ZendTest\Di\TestAsset\Struct', +// 'required' => true, +// ), +// 'params' => array( +// 'required' => true, +// ), +// ), +// ), +// ), +// ), +// ), +// 'instance' => array( +// 'ZendTest\Di\TestAsset\DummyParams' => array( +// 'parameters' => array( +// 'struct' => 'ZendTest\Di\TestAsset\Struct', +// 'params' => array( +// 'foo' => 'bar', +// ), +// ), +// ), +// 'ZendTest\Di\TestAsset\Struct' => array( +// 'parameters' => array( +// 'param1' => 'hello', +// 'param2' => 'world', +// ), +// ), +// ), +// )); +// $di = new Di(); +// $di->configure($config); +// $dummyParams = $di->get('ZendTest\Di\TestAsset\DummyParams'); +// $this->assertEquals($dummyParams->params['param1'], 'hello'); +// $this->assertEquals($dummyParams->params['param2'], 'world'); +// $this->assertEquals($dummyParams->params['foo'], 'bar'); +// $this->assertArrayNotHasKey('methods', $di->definitions()->hasMethods('ZendTest\Di\TestAsset\StaticFactory')); +// } + /** * @group ConstructorInjection */ @@ -300,19 +402,114 @@ public function testNewInstanceWillUsePreferredClassForInterfaceHints() $this->assertSame($a, $d->a); } - /* - public function testNewInstanceWillRunArbitraryMethodsAccordingToConfiguration() + public function testInjectionInstancesCanBeInjectedMultipleTimes() { - $di = new Di(); - $im = $di->instanceManager(); - $im->setMethods('ZendTest\Di\TestAsset\ConfigParameter\A', array( - 'setSomeInt' => array('value' => 5), - 'injectM' => array('m' => 10) + $definitionList = new DefinitionList(array( + $classdef = new Definition\ClassDefinition('ZendTest\Di\TestAsset\InjectionClasses\A'), + new Definition\RuntimeDefinition() + )); + $classdef->addMethod('addB'); + $classdef->addMethodParameter('addB', 'b', array('required' => true, 'type' => 'ZendTest\Di\TestAsset\InjectionClasses\B')); + + $di = new Di($definitionList); + $di->instanceManager()->setInjections( + 'ZendTest\Di\TestAsset\InjectionClasses\A', + array( + 'ZendTest\Di\TestAsset\InjectionClasses\B' + ) + ); + $a = $di->newInstance('ZendTest\Di\TestAsset\InjectionClasses\A'); + $this->assertInstanceOf('ZendTest\Di\TestAsset\InjectionClasses\B', $a->bs[0]); + + $di = new Di($definitionList); + $di->instanceManager()->addAlias('my-b1', 'ZendTest\Di\TestAsset\InjectionClasses\B'); + $di->instanceManager()->addAlias('my-b2', 'ZendTest\Di\TestAsset\InjectionClasses\B'); + + $di->instanceManager()->setInjections( + 'ZendTest\Di\TestAsset\InjectionClasses\A', + array( + 'my-b1', + 'my-b2' + ) + ); + $a = $di->newInstance('ZendTest\Di\TestAsset\InjectionClasses\A'); + + $this->assertInstanceOf('ZendTest\Di\TestAsset\InjectionClasses\B', $a->bs[0]); + $this->assertInstanceOf('ZendTest\Di\TestAsset\InjectionClasses\B', $a->bs[1]); + $this->assertNotSame( + $a->bs[0], + $a->bs[1] + ); + } + + public function testInjectionCanHandleDisambiguation() + { + $definitionList = new DefinitionList(array( + $classdef = new Definition\ClassDefinition('ZendTest\Di\TestAsset\InjectionClasses\A'), + new Definition\RuntimeDefinition() + )); + $classdef->addMethod('injectBOnce'); + $classdef->addMethod('injectBTwice'); + $classdef->addMethodParameter('injectBOnce', 'b', array('required' => true, 'type' => 'ZendTest\Di\TestAsset\InjectionClasses\B')); + $classdef->addMethodParameter('injectBTwice', 'b', array('required' => true, 'type' => 'ZendTest\Di\TestAsset\InjectionClasses\B')); + + $di = new Di($definitionList); + $di->instanceManager()->setInjections( + 'ZendTest\Di\TestAsset\InjectionClasses\A', + array( + 'ZendTest\Di\TestAsset\InjectionClasses\A::injectBOnce:0' => new \ZendTest\Di\TestAsset\InjectionClasses\B('once'), + 'ZendTest\Di\TestAsset\InjectionClasses\A::injectBTwice:0' => new \ZendTest\Di\TestAsset\InjectionClasses\B('twice') + ) + ); + $a = $di->newInstance('ZendTest\Di\TestAsset\InjectionClasses\A'); + $this->assertInstanceOf('ZendTest\Di\TestAsset\InjectionClasses\B', $a->bs[0]); + $this->assertEquals('once', $a->bs[0]->id); + $this->assertInstanceOf('ZendTest\Di\TestAsset\InjectionClasses\B', $a->bs[1]); + $this->assertEquals('twice', $a->bs[1]->id); + } + + public function testInjectionCanHandleMultipleInjectionsWithMultipleArguments() + { + $definitionList = new DefinitionList(array( + $classdef = new Definition\ClassDefinition('ZendTest\Di\TestAsset\InjectionClasses\A'), + new Definition\RuntimeDefinition() )); - $b = $di->newInstance('ZendTest\Di\TestAsset\ConfigParameter\B'); - $this->assertEquals(5, $b->a->someInt); - $this->assertEquals(10, $b->a->m); + $classdef->addMethod('injectSplitDependency'); + $classdef->addMethodParameter('injectSplitDependency', 'b', array('required' => true, 'type' => 'ZendTest\Di\TestAsset\InjectionClasses\B')); + $classdef->addMethodParameter('injectSplitDependency', 'somestring', array('required' => true, 'type' => null)); + + /** + * First test that this works with a single call + */ + $di = new Di($definitionList); + $di->instanceManager()->setInjections( + 'ZendTest\Di\TestAsset\InjectionClasses\A', + array( + 'injectSplitDependency' => array('b' => 'ZendTest\Di\TestAsset\InjectionClasses\B', 'somestring' => 'bs-id') + ) + ); + $a = $di->newInstance('ZendTest\Di\TestAsset\InjectionClasses\A'); + $this->assertInstanceOf('ZendTest\Di\TestAsset\InjectionClasses\B', $a->bs[0]); + $this->assertEquals('bs-id', $a->bs[0]->id); + + /** + * Next test that this works with multiple calls + */ + $di = new Di($definitionList); + $di->instanceManager()->setInjections( + 'ZendTest\Di\TestAsset\InjectionClasses\A', + array( + 'injectSplitDependency' => array( + array('b' => 'ZendTest\Di\TestAsset\InjectionClasses\B', 'somestring' => 'bs-id'), + array('b' => 'ZendTest\Di\TestAsset\InjectionClasses\C', 'somestring' => 'bs-id-for-c') + ) + ) + ); + $a = $di->newInstance('ZendTest\Di\TestAsset\InjectionClasses\A'); + $this->assertInstanceOf('ZendTest\Di\TestAsset\InjectionClasses\B', $a->bs[0]); + $this->assertEquals('bs-id', $a->bs[0]->id); + $this->assertInstanceOf('ZendTest\Di\TestAsset\InjectionClasses\C', $a->bs[1]); + $this->assertEquals('bs-id-for-c', $a->bs[1]->id); } - */ } diff --git a/test/TestAsset/CallbackClasses/A.php b/test/TestAsset/CallbackClasses/A.php new file mode 100644 index 00000000..d55dcb8a --- /dev/null +++ b/test/TestAsset/CallbackClasses/A.php @@ -0,0 +1,14 @@ +c = $c; + $b->params = $params; + return $b; + } + + protected function __construct() + { + // no dice + } +} diff --git a/test/TestAsset/CallbackClasses/C.php b/test/TestAsset/CallbackClasses/C.php new file mode 100644 index 00000000..1538900f --- /dev/null +++ b/test/TestAsset/CallbackClasses/C.php @@ -0,0 +1,8 @@ +someInt = $value; - } - - public function injectM($m) - { - $this->m = $m; - } - -} \ No newline at end of file diff --git a/test/TestAsset/ConfigParameter/B.php b/test/TestAsset/ConfigParameter/B.php deleted file mode 100644 index ecbb182f..00000000 --- a/test/TestAsset/ConfigParameter/B.php +++ /dev/null @@ -1,12 +0,0 @@ -a = $a; - } -} \ No newline at end of file diff --git a/test/TestAsset/InjectionClasses/A.php b/test/TestAsset/InjectionClasses/A.php new file mode 100644 index 00000000..2945f0eb --- /dev/null +++ b/test/TestAsset/InjectionClasses/A.php @@ -0,0 +1,29 @@ +bs[] = $b; + } + + public function injectBOnce(B $b) + { + $this->bs[] = $b; + } + + public function injectBTwice(B $b) + { + $this->bs[] = $b; + } + + public function injectSplitDependency(B $b, $somestring) + { + $b->id = $somestring; + $this->bs[] = $b; + } +} diff --git a/test/TestAsset/InjectionClasses/B.php b/test/TestAsset/InjectionClasses/B.php new file mode 100644 index 00000000..72d7d814 --- /dev/null +++ b/test/TestAsset/InjectionClasses/B.php @@ -0,0 +1,12 @@ +id = $id; + } +} diff --git a/test/TestAsset/InjectionClasses/C.php b/test/TestAsset/InjectionClasses/C.php new file mode 100644 index 00000000..ab6dd516 --- /dev/null +++ b/test/TestAsset/InjectionClasses/C.php @@ -0,0 +1,7 @@ +