[DI] Service decoration: autowire the inner service

This commit is contained in:
Kévin Dunglas 2017-12-30 02:28:07 +01:00 committed by Fabien Potencier
parent a5dbc68cd4
commit 24876f2adc
8 changed files with 158 additions and 21 deletions

View File

@ -40,5 +40,7 @@ return PhpCsFixer\Config::create()
->notPath('Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/default.phpt') ->notPath('Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/default.phpt')
->notPath('Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/weak.phpt') ->notPath('Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/weak.phpt')
->notPath('Symfony/Component/Debug/Tests/DebugClassLoaderTest.php') ->notPath('Symfony/Component/Debug/Tests/DebugClassLoaderTest.php')
// invalid annotations on purpose
->notPath('Symfony/Component/DependencyInjection/Tests/Fixtures/includes/autowiring_classes.php')
) )
; ;

View File

@ -6,6 +6,7 @@ CHANGELOG
* added support for variadics in named arguments * added support for variadics in named arguments
* added PSR-11 `ContainerBagInterface` and its `ContainerBag` implementation to access parameters as-a-service * added PSR-11 `ContainerBagInterface` and its `ContainerBag` implementation to access parameters as-a-service
* added support for service's decorators autowiring
4.0.0 4.0.0
----- -----

View File

@ -32,6 +32,12 @@ class AutowirePass extends AbstractRecursivePass
private $autowired = array(); private $autowired = array();
private $lastFailure; private $lastFailure;
private $throwOnAutowiringException; private $throwOnAutowiringException;
private $decoratedClass;
private $decoratedId;
private $methodCalls;
private $getPreviousValue;
private $decoratedMethodIndex;
private $decoratedMethodArgumentIndex;
public function __construct(bool $throwOnAutowireException = true) public function __construct(bool $throwOnAutowireException = true)
{ {
@ -49,6 +55,12 @@ class AutowirePass extends AbstractRecursivePass
$this->types = null; $this->types = null;
$this->ambiguousServiceTypes = array(); $this->ambiguousServiceTypes = array();
$this->autowired = array(); $this->autowired = array();
$this->decoratedClass = null;
$this->decoratedId = null;
$this->methodCalls = null;
$this->getPreviousValue = null;
$this->decoratedMethodIndex = null;
$this->decoratedMethodArgumentIndex = null;
} }
} }
@ -89,7 +101,7 @@ class AutowirePass extends AbstractRecursivePass
return $value; return $value;
} }
$methodCalls = $value->getMethodCalls(); $this->methodCalls = $value->getMethodCalls();
try { try {
$constructor = $this->getConstructor($value, false); $constructor = $this->getConstructor($value, false);
@ -98,21 +110,21 @@ class AutowirePass extends AbstractRecursivePass
} }
if ($constructor) { if ($constructor) {
array_unshift($methodCalls, array($constructor, $value->getArguments())); array_unshift($this->methodCalls, array($constructor, $value->getArguments()));
} }
$methodCalls = $this->autowireCalls($reflectionClass, $methodCalls); $this->methodCalls = $this->autowireCalls($reflectionClass, $isRoot);
if ($constructor) { if ($constructor) {
list(, $arguments) = array_shift($methodCalls); list(, $arguments) = array_shift($this->methodCalls);
if ($arguments !== $value->getArguments()) { if ($arguments !== $value->getArguments()) {
$value->setArguments($arguments); $value->setArguments($arguments);
} }
} }
if ($methodCalls !== $value->getMethodCalls()) { if ($this->methodCalls !== $value->getMethodCalls()) {
$value->setMethodCalls($methodCalls); $value->setMethodCalls($this->methodCalls);
} }
return $value; return $value;
@ -120,13 +132,20 @@ class AutowirePass extends AbstractRecursivePass
/** /**
* @param \ReflectionClass $reflectionClass * @param \ReflectionClass $reflectionClass
* @param array $methodCalls
* *
* @return array * @return array
*/ */
private function autowireCalls(\ReflectionClass $reflectionClass, array $methodCalls) private function autowireCalls(\ReflectionClass $reflectionClass, bool $isRoot): array
{ {
foreach ($methodCalls as $i => $call) { if ($isRoot && ($definition = $this->container->getDefinition($this->currentId)) && $this->container->has($this->decoratedId = $definition->innerServiceId)) {
$this->decoratedClass = $this->container->findDefinition($this->decoratedId)->getClass();
} else {
$this->decoratedId = null;
$this->decoratedClass = null;
}
foreach ($this->methodCalls as $i => $call) {
$this->decoratedMethodIndex = $i;
list($method, $arguments) = $call; list($method, $arguments) = $call;
if ($method instanceof \ReflectionFunctionAbstract) { if ($method instanceof \ReflectionFunctionAbstract) {
@ -138,11 +157,11 @@ class AutowirePass extends AbstractRecursivePass
$arguments = $this->autowireMethod($reflectionMethod, $arguments); $arguments = $this->autowireMethod($reflectionMethod, $arguments);
if ($arguments !== $call[1]) { if ($arguments !== $call[1]) {
$methodCalls[$i][1] = $arguments; $this->methodCalls[$i][1] = $arguments;
} }
} }
return $methodCalls; return $this->methodCalls;
} }
/** /**
@ -190,18 +209,40 @@ class AutowirePass extends AbstractRecursivePass
continue; continue;
} }
if (!$value = $this->getAutowiredReference($ref = new TypedReference($type, $type, !$parameter->isOptional() ? $class : ''), 'for '.sprintf('argument "$%s" of method "%s()"', $parameter->name, $class.'::'.$method))) { $getValue = function () use ($type, $parameter, $class, $method) {
$failureMessage = $this->createTypeNotFoundMessage($ref, sprintf('argument "$%s" of method "%s()"', $parameter->name, $class !== $this->currentId ? $class.'::'.$method : $method)); if (!$value = $this->getAutowiredReference($ref = new TypedReference($type, $type, !$parameter->isOptional() ? $class : ''), 'for '.sprintf('argument "$%s" of method "%s()"', $parameter->name, $class.'::'.$method))) {
$failureMessage = $this->createTypeNotFoundMessage($ref, sprintf('argument "$%s" of method "%s()"', $parameter->name, $class !== $this->currentId ? $class.'::'.$method : $method));
if ($parameter->isDefaultValueAvailable()) { if ($parameter->isDefaultValueAvailable()) {
$value = $parameter->getDefaultValue(); $value = $parameter->getDefaultValue();
} elseif (!$parameter->allowsNull()) { } elseif (!$parameter->allowsNull()) {
throw new AutowiringFailedException($this->currentId, $failureMessage); throw new AutowiringFailedException($this->currentId, $failureMessage);
}
$this->container->log($this, $failureMessage);
}
return $value;
};
if ($this->decoratedClass && $isDecorated = is_a($this->decoratedClass, $type, true)) {
if ($this->getPreviousValue) {
// The inner service is injected only if there is only 1 argument matching the type of the decorated class
// across all arguments of all autowired methods.
// If a second matching argument is found, the default behavior is restored.
$getPreviousValue = $this->getPreviousValue;
$this->methodCalls[$this->decoratedMethodIndex][1][$this->decoratedMethodArgumentIndex] = $getPreviousValue();
$this->decoratedClass = null; // Prevent further checks
} else {
$arguments[$index] = new TypedReference($this->decoratedId, $this->decoratedClass);
$this->getPreviousValue = $getValue;
$this->decoratedMethodArgumentIndex = $index;
continue;
} }
$this->container->log($this, $failureMessage);
} }
$arguments[$index] = $value; $arguments[$index] = $getValue();
} }
if ($parameters && !isset($arguments[++$index])) { if ($parameters && !isset($arguments[++$index])) {

View File

@ -43,6 +43,7 @@ class DecoratorServicePass implements CompilerPassInterface
if (!$renamedId) { if (!$renamedId) {
$renamedId = $id.'.inner'; $renamedId = $id.'.inner';
} }
$definition->innerServiceId = $renamedId;
// we create a new alias/service for the service we are replacing // we create a new alias/service for the service we are replacing
// to be able to reference it in the new one // to be able to reference it in the new one

View File

@ -49,6 +49,13 @@ class Definition
private static $defaultDeprecationTemplate = 'The "%service_id%" service is deprecated. You should stop using it, as it will soon be removed.'; private static $defaultDeprecationTemplate = 'The "%service_id%" service is deprecated. You should stop using it, as it will soon be removed.';
/**
* @internal
*
* Used to store the name of the inner id when using service decoration together with autowiring
*/
public $innerServiceId;
/** /**
* @param string|null $class The service class * @param string|null $class The service class
* @param array $arguments An array of arguments to pass to the service constructor * @param array $arguments An array of arguments to pass to the service constructor

View File

@ -12,9 +12,12 @@
namespace Symfony\Component\DependencyInjection\Tests\Compiler; namespace Symfony\Component\DependencyInjection\Tests\Compiler;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Symfony\Component\Config\FileLocator; use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\Compiler\AutowireRequiredMethodsPass; use Symfony\Component\DependencyInjection\Compiler\AutowireRequiredMethodsPass;
use Symfony\Component\DependencyInjection\Compiler\AutowirePass; use Symfony\Component\DependencyInjection\Compiler\AutowirePass;
use Symfony\Component\DependencyInjection\Compiler\DecoratorServicePass;
use Symfony\Component\DependencyInjection\Compiler\ResolveClassPass; use Symfony\Component\DependencyInjection\Compiler\ResolveClassPass;
use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\RuntimeException; use Symfony\Component\DependencyInjection\Exception\RuntimeException;
@ -787,4 +790,59 @@ class AutowirePassTest extends TestCase
$this->assertSame(array(), $container->getDefinition('autowired')->getArguments()); $this->assertSame(array(), $container->getDefinition('autowired')->getArguments());
} }
public function testAutowireDecorator()
{
$container = new ContainerBuilder();
$container->register(LoggerInterface::class, NullLogger::class);
$container->register(Decorated::class, Decorated::class);
$container
->register(Decorator::class, Decorator::class)
->setDecoratedService(Decorated::class)
->setAutowired(true)
;
(new DecoratorServicePass())->process($container);
(new AutowirePass())->process($container);
$definition = $container->getDefinition(Decorator::class);
$this->assertSame(Decorator::class.'.inner', (string) $definition->getArgument(1));
}
public function testAutowireDecoratorRenamedId()
{
$container = new ContainerBuilder();
$container->register(LoggerInterface::class, NullLogger::class);
$container->register(Decorated::class, Decorated::class);
$container
->register(Decorator::class, Decorator::class)
->setDecoratedService(Decorated::class, 'renamed')
->setAutowired(true)
;
(new DecoratorServicePass())->process($container);
(new AutowirePass())->process($container);
$definition = $container->getDefinition(Decorator::class);
$this->assertSame('renamed', (string) $definition->getArgument(1));
}
/**
* @expectedException \Symfony\Component\DependencyInjection\Exception\AutowiringFailedException
* @expectedExceptionMessage Cannot autowire service "Symfony\Component\DependencyInjection\Tests\Compiler\NonAutowirableDecorator": argument "$decorated1" of method "__construct()" references interface "Symfony\Component\DependencyInjection\Tests\Compiler\DecoratorInterface" but no such service exists. You should maybe alias this interface to one of these existing services: "Symfony\Component\DependencyInjection\Tests\Compiler\NonAutowirableDecorator", "Symfony\Component\DependencyInjection\Tests\Compiler\NonAutowirableDecorator.inner". Did you create a class that implements this interface?
*/
public function testDoNotAutowireDecoratorWhenSeveralArgumentOfTheType()
{
$container = new ContainerBuilder();
$container->register(LoggerInterface::class, NullLogger::class);
$container->register(Decorated::class, Decorated::class);
$container
->register(NonAutowirableDecorator::class, NonAutowirableDecorator::class)
->setDecoratedService(Decorated::class)
->setAutowired(true)
;
(new DecoratorServicePass())->process($container);
(new AutowirePass())->process($container);
}
} }

View File

@ -2,6 +2,8 @@
namespace Symfony\Component\DependencyInjection\Tests\Compiler; namespace Symfony\Component\DependencyInjection\Tests\Compiler;
use Psr\Log\LoggerInterface;
class Foo class Foo
{ {
} }
@ -352,3 +354,28 @@ class ScalarSetter
{ {
} }
} }
interface DecoratorInterface
{
}
class Decorated implements DecoratorInterface
{
public function __construct($quz = null, \NonExistent $nonExistent = null, DecoratorInterface $decorated = null, array $foo = array())
{
}
}
class Decorator implements DecoratorInterface
{
public function __construct(LoggerInterface $logger, DecoratorInterface $decorated)
{
}
}
class NonAutowirableDecorator implements DecoratorInterface
{
public function __construct(LoggerInterface $logger, DecoratorInterface $decorated1, DecoratorInterface $decorated2)
{
}
}

View File

@ -57,8 +57,8 @@ class ProjectServiceContainer extends Container
'Psr\\Container\\ContainerInterface' => true, 'Psr\\Container\\ContainerInterface' => true,
'Symfony\\Component\\DependencyInjection\\ContainerInterface' => true, 'Symfony\\Component\\DependencyInjection\\ContainerInterface' => true,
'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CustomDefinition' => true, 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CustomDefinition' => true,
'service_locator.MtGsMEd' => true, 'service_locator.KT3jhJ7' => true,
'service_locator.MtGsMEd.foo_service' => true, 'service_locator.KT3jhJ7.foo_service' => true,
); );
} }