diff --git a/src/Symfony/Component/DependencyInjection/CHANGELOG.md b/src/Symfony/Component/DependencyInjection/CHANGELOG.md index 43f445736f..e006527ce2 100644 --- a/src/Symfony/Component/DependencyInjection/CHANGELOG.md +++ b/src/Symfony/Component/DependencyInjection/CHANGELOG.md @@ -4,6 +4,7 @@ CHANGELOG 3.2.0 ----- + * added support for setter autowiring * allowed to prioritize compiler passes by introducing a third argument to `PassConfig::addPass()`, to `Compiler::addPass` and to `ContainerBuilder::addCompilerPass()` * added support for PHP constants in YAML configuration files * deprecated the ability to set or unset a private service with the `Container::set()` method diff --git a/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php b/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php index fdf0edcfb4..9a2f2ecba1 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php @@ -110,12 +110,45 @@ class AutowirePass implements CompilerPassInterface $this->container->addResource(static::createResourceForClass($reflectionClass)); } - if (!$constructor = $reflectionClass->getConstructor()) { - return; + if ($constructor = $reflectionClass->getConstructor()) { + $this->autowireMethod($id, $definition, $constructor, true); } - $arguments = $definition->getArguments(); - foreach ($constructor->getParameters() as $index => $parameter) { + $methodsCalled = array(); + foreach ($definition->getMethodCalls() as $methodCall) { + $methodsCalled[$methodCall[0]] = true; + } + + foreach ($reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $reflectionMethod) { + $name = $reflectionMethod->getName(); + if (isset($methodsCalled[$name]) || $reflectionMethod->isStatic() || 1 !== $reflectionMethod->getNumberOfParameters() || 0 !== strpos($name, 'set')) { + continue; + } + + $this->autowireMethod($id, $definition, $reflectionMethod, false); + } + } + + /** + * Autowires the constructor or a setter. + * + * @param string $id + * @param Definition $definition + * @param \ReflectionMethod $reflectionMethod + * @param bool $isConstructor + * + * @throws RuntimeException + */ + private function autowireMethod($id, Definition $definition, \ReflectionMethod $reflectionMethod, $isConstructor) + { + if ($isConstructor) { + $arguments = $definition->getArguments(); + } else { + $arguments = array(); + } + + $addMethodCall = false; + foreach ($reflectionMethod->getParameters() as $index => $parameter) { if (array_key_exists($index, $arguments) && '' !== $arguments[$index]) { continue; } @@ -124,7 +157,11 @@ class AutowirePass implements CompilerPassInterface if (!$typeHint = $parameter->getClass()) { // no default value? Then fail if (!$parameter->isOptional()) { - throw new RuntimeException(sprintf('Unable to autowire argument index %d ($%s) for the service "%s". If this is an object, give it a type-hint. Otherwise, specify this argument\'s value explicitly.', $index, $parameter->name, $id)); + if ($isConstructor) { + throw new RuntimeException(sprintf('Unable to autowire argument index %d ($%s) for the service "%s". If this is an object, give it a type-hint. Otherwise, specify this argument\'s value explicitly.', $index, $parameter->name, $id)); + } + + return; } // specifically pass the default value @@ -139,16 +176,23 @@ class AutowirePass implements CompilerPassInterface if (isset($this->types[$typeHint->name])) { $value = new Reference($this->types[$typeHint->name]); + $addMethodCall = true; } else { try { $value = $this->createAutowiredDefinition($typeHint, $id); + $addMethodCall = true; } catch (RuntimeException $e) { if ($parameter->allowsNull()) { $value = null; } elseif ($parameter->isDefaultValueAvailable()) { $value = $parameter->getDefaultValue(); } else { - throw $e; + // The exception code is set to 1 if the exception must be thrown even if it's a setter + if (1 === $e->getCode() || $isConstructor) { + throw $e; + } + + return; } } } @@ -156,7 +200,11 @@ class AutowirePass implements CompilerPassInterface // Typehint against a non-existing class if (!$parameter->isDefaultValueAvailable()) { - throw new RuntimeException(sprintf('Cannot autowire argument %s for %s because the type-hinted class does not exist (%s).', $index + 1, $definition->getClass(), $e->getMessage()), 0, $e); + if ($isConstructor) { + throw new RuntimeException(sprintf('Cannot autowire argument %s for %s because the type-hinted class does not exist (%s).', $index + 1, $definition->getClass(), $e->getMessage()), 0, $e); + } + + return; } $value = $parameter->getDefaultValue(); @@ -168,7 +216,12 @@ class AutowirePass implements CompilerPassInterface // it's possible index 1 was set, then index 0, then 2, etc // make sure that we re-order so they're injected as expected ksort($arguments); - $definition->setArguments($arguments); + + if ($isConstructor) { + $definition->setArguments($arguments); + } elseif ($addMethodCall) { + $definition->addMethodCall($reflectionMethod->name, $arguments); + } } /** @@ -266,7 +319,7 @@ class AutowirePass implements CompilerPassInterface $classOrInterface = $typeHint->isInterface() ? 'interface' : 'class'; $matchingServices = implode(', ', $this->ambiguousServiceTypes[$typeHint->name]); - throw new RuntimeException(sprintf('Unable to autowire argument of type "%s" for the service "%s". Multiple services exist for this %s (%s).', $typeHint->name, $id, $classOrInterface, $matchingServices)); + throw new RuntimeException(sprintf('Unable to autowire argument of type "%s" for the service "%s". Multiple services exist for this %s (%s).', $typeHint->name, $id, $classOrInterface, $matchingServices), 1); } if (!$typeHint->isInstantiable()) { diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php index a8b1be3550..684e99b632 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php @@ -429,6 +429,47 @@ class AutowirePassTest extends \PHPUnit_Framework_TestCase ); } + public function testSetterInjection() + { + $container = new ContainerBuilder(); + $container->register('app_foo', Foo::class); + $container->register('app_a', A::class); + $container->register('app_collision_a', CollisionA::class); + $container->register('app_collision_b', CollisionB::class); + + // manually configure *one* call, to override autowiring + $container + ->register('setter_injection', SetterInjection::class) + ->setAutowired(true) + ->addMethodCall('setWithCallsConfigured', array('manual_arg1', 'manual_arg2')) + ; + + $pass = new AutowirePass(); + $pass->process($container); + + $methodCalls = $container->getDefinition('setter_injection')->getMethodCalls(); + + // grab the call method names + $actualMethodNameCalls = array_map(function ($call) { + return $call[0]; + }, $methodCalls); + $this->assertEquals( + array('setWithCallsConfigured', 'setFoo'), + $actualMethodNameCalls + ); + + // test setWithCallsConfigured args + $this->assertEquals( + array('manual_arg1', 'manual_arg2'), + $methodCalls[0][1] + ); + // test setFoo args + $this->assertEquals( + array(new Reference('app_foo')), + $methodCalls[1][1] + ); + } + /** * @dataProvider getCreateResourceTests */ @@ -476,6 +517,24 @@ class AutowirePassTest extends \PHPUnit_Framework_TestCase $this->assertTrue($container->hasDefinition('bar')); } + + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException + * @expectedExceptionMessage Unable to autowire argument of type "Symfony\Component\DependencyInjection\Tests\Compiler\CollisionInterface" for the service "setter_injection_collision". Multiple services exist for this interface (c1, c2). + * @expectedExceptionCode 1 + */ + public function testSetterInjectionCollisionThrowsException() + { + $container = new ContainerBuilder(); + + $container->register('c1', CollisionA::class); + $container->register('c2', CollisionB::class); + $aDefinition = $container->register('setter_injection_collision', SetterInjectionCollision::class); + $aDefinition->setAutowired(true); + + $pass = new AutowirePass(); + $pass->process($container); + } } class Foo @@ -648,9 +707,69 @@ class ClassForResource class IdenticalClassResource extends ClassForResource { } + class ClassChangedConstructorArgs extends ClassForResource { public function __construct($foo, Bar $bar, $baz) { } } + +class SetterInjection +{ + public function setFoo(Foo $foo) + { + // should be called + } + + public function setDependencies(Foo $foo, A $a) + { + // should be called + } + + public function setBar() + { + // should not be called + } + + public function setNotAutowireable(NotARealClass $n) + { + // should not be called + } + + public function setArgCannotAutowire($foo) + { + // should not be called + } + + public function setOptionalNotAutowireable(NotARealClass $n = null) + { + // should not be called + } + + public function setOptionalNoTypeHint($foo = null) + { + // should not be called + } + + public function setOptionalArgNoAutowireable($other = 'default_val') + { + // should not be called + } + + public function setWithCallsConfigured(A $a) + { + // this method has a calls configured on it + // should not be called + } +} + +class SetterInjectionCollision +{ + public function setMultipleInstancesForOneArg(CollisionInterface $collision) + { + // The CollisionInterface cannot be autowired - there are multiple + + // should throw an exception + } +}