[DI] Service decoration: autowire the inner service
This commit is contained in:
parent
a5dbc68cd4
commit
24876f2adc
@ -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')
|
||||||
)
|
)
|
||||||
;
|
;
|
||||||
|
@ -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
|
||||||
-----
|
-----
|
||||||
|
@ -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,6 +209,7 @@ class AutowirePass extends AbstractRecursivePass
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$getValue = function () use ($type, $parameter, $class, $method) {
|
||||||
if (!$value = $this->getAutowiredReference($ref = new TypedReference($type, $type, !$parameter->isOptional() ? $class : ''), 'for '.sprintf('argument "$%s" of method "%s()"', $parameter->name, $class.'::'.$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));
|
$failureMessage = $this->createTypeNotFoundMessage($ref, sprintf('argument "$%s" of method "%s()"', $parameter->name, $class !== $this->currentId ? $class.'::'.$method : $method));
|
||||||
|
|
||||||
@ -201,7 +221,28 @@ class AutowirePass extends AbstractRecursivePass
|
|||||||
$this->container->log($this, $failureMessage);
|
$this->container->log($this, $failureMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
$arguments[$index] = $value;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$arguments[$index] = $getValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($parameters && !isset($arguments[++$index])) {
|
if ($parameters && !isset($arguments[++$index])) {
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user