feature #17608 [DependencyInjection] Autowiring: add setter injection support (dunglas)
This PR was merged into the 3.2-dev branch.
Discussion
----------
[DependencyInjection] Autowiring: add setter injection support
| Q | A
| ------------- | ---
| Bug fix? | no
| New feature? | yes
| BC breaks? | no
| Deprecations? | no
| Tests pass? | yes
| Fixed tickets | n/a
| License | MIT
| Doc PR | todo
Add support for setter injection in the Dependency Injection Component.
Setter injection should be avoided when possible. However, there is some legitimate use cases for it. This PR follows a proposal of @weaverryan to ease using [DunglasActionBundle](https://github.com/dunglas/DunglasActionBundle) with #16863. The basic idea is to include a setter for the required service in the trait and let the DependencyInjection Component autowire the dependency using the setter.
This way, a newcomer can use the trait without having to create or modify the constructor of the action.
/cc @derrabus
Commits
-------
a0d7cbe
[DependencyInjection] Autowiring: add setter injection support
This commit is contained in:
commit
7eab6b9204
@ -4,6 +4,7 @@ CHANGELOG
|
|||||||
3.2.0
|
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()`
|
* 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
|
* added support for PHP constants in YAML configuration files
|
||||||
* deprecated the ability to set or unset a private service with the `Container::set()` method
|
* deprecated the ability to set or unset a private service with the `Container::set()` method
|
||||||
|
@ -110,12 +110,45 @@ class AutowirePass implements CompilerPassInterface
|
|||||||
$this->container->addResource(static::createResourceForClass($reflectionClass));
|
$this->container->addResource(static::createResourceForClass($reflectionClass));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$constructor = $reflectionClass->getConstructor()) {
|
if ($constructor = $reflectionClass->getConstructor()) {
|
||||||
return;
|
$this->autowireMethod($id, $definition, $constructor, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
$arguments = $definition->getArguments();
|
$methodsCalled = array();
|
||||||
foreach ($constructor->getParameters() as $index => $parameter) {
|
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]) {
|
if (array_key_exists($index, $arguments) && '' !== $arguments[$index]) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -124,7 +157,11 @@ class AutowirePass implements CompilerPassInterface
|
|||||||
if (!$typeHint = $parameter->getClass()) {
|
if (!$typeHint = $parameter->getClass()) {
|
||||||
// no default value? Then fail
|
// no default value? Then fail
|
||||||
if (!$parameter->isOptional()) {
|
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
|
// specifically pass the default value
|
||||||
@ -139,16 +176,23 @@ class AutowirePass implements CompilerPassInterface
|
|||||||
|
|
||||||
if (isset($this->types[$typeHint->name])) {
|
if (isset($this->types[$typeHint->name])) {
|
||||||
$value = new Reference($this->types[$typeHint->name]);
|
$value = new Reference($this->types[$typeHint->name]);
|
||||||
|
$addMethodCall = true;
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
$value = $this->createAutowiredDefinition($typeHint, $id);
|
$value = $this->createAutowiredDefinition($typeHint, $id);
|
||||||
|
$addMethodCall = true;
|
||||||
} catch (RuntimeException $e) {
|
} catch (RuntimeException $e) {
|
||||||
if ($parameter->allowsNull()) {
|
if ($parameter->allowsNull()) {
|
||||||
$value = null;
|
$value = null;
|
||||||
} elseif ($parameter->isDefaultValueAvailable()) {
|
} elseif ($parameter->isDefaultValueAvailable()) {
|
||||||
$value = $parameter->getDefaultValue();
|
$value = $parameter->getDefaultValue();
|
||||||
} else {
|
} 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
|
// Typehint against a non-existing class
|
||||||
|
|
||||||
if (!$parameter->isDefaultValueAvailable()) {
|
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();
|
$value = $parameter->getDefaultValue();
|
||||||
@ -168,7 +216,12 @@ class AutowirePass implements CompilerPassInterface
|
|||||||
// it's possible index 1 was set, then index 0, then 2, etc
|
// 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
|
// make sure that we re-order so they're injected as expected
|
||||||
ksort($arguments);
|
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';
|
$classOrInterface = $typeHint->isInterface() ? 'interface' : 'class';
|
||||||
$matchingServices = implode(', ', $this->ambiguousServiceTypes[$typeHint->name]);
|
$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()) {
|
if (!$typeHint->isInstantiable()) {
|
||||||
|
@ -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
|
* @dataProvider getCreateResourceTests
|
||||||
*/
|
*/
|
||||||
@ -476,6 +517,24 @@ class AutowirePassTest extends \PHPUnit_Framework_TestCase
|
|||||||
|
|
||||||
$this->assertTrue($container->hasDefinition('bar'));
|
$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
|
class Foo
|
||||||
@ -648,9 +707,69 @@ class ClassForResource
|
|||||||
class IdenticalClassResource extends ClassForResource
|
class IdenticalClassResource extends ClassForResource
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
class ClassChangedConstructorArgs extends ClassForResource
|
class ClassChangedConstructorArgs extends ClassForResource
|
||||||
{
|
{
|
||||||
public function __construct($foo, Bar $bar, $baz)
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user