Skip to content

Commit 530e4c3

Browse files
dunglasfabpot
authored andcommitted
[DI] Service decoration: autowire the inner service
1 parent e003f18 commit 530e4c3

File tree

7 files changed

+156
-21
lines changed

7 files changed

+156
-21
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ CHANGELOG
66

77
* added support for variadics in named arguments
88
* added PSR-11 `ContainerBagInterface` and its `ContainerBag` implementation to access parameters as-a-service
9+
* added support for service's decorators autowiring
910

1011
4.0.0
1112
-----

Compiler/AutowirePass.php

Lines changed: 60 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ class AutowirePass extends AbstractRecursivePass
3232
private $autowired = array();
3333
private $lastFailure;
3434
private $throwOnAutowiringException;
35+
private $decoratedClass;
36+
private $decoratedId;
37+
private $methodCalls;
38+
private $getPreviousValue;
39+
private $decoratedMethodIndex;
40+
private $decoratedMethodArgumentIndex;
3541

3642
public function __construct(bool $throwOnAutowireException = true)
3743
{
@@ -49,6 +55,12 @@ public function process(ContainerBuilder $container)
4955
$this->types = null;
5056
$this->ambiguousServiceTypes = array();
5157
$this->autowired = array();
58+
$this->decoratedClass = null;
59+
$this->decoratedId = null;
60+
$this->methodCalls = null;
61+
$this->getPreviousValue = null;
62+
$this->decoratedMethodIndex = null;
63+
$this->decoratedMethodArgumentIndex = null;
5264
}
5365
}
5466

@@ -89,7 +101,7 @@ private function doProcessValue($value, $isRoot = false)
89101
return $value;
90102
}
91103

92-
$methodCalls = $value->getMethodCalls();
104+
$this->methodCalls = $value->getMethodCalls();
93105

94106
try {
95107
$constructor = $this->getConstructor($value, false);
@@ -98,35 +110,42 @@ private function doProcessValue($value, $isRoot = false)
98110
}
99111

100112
if ($constructor) {
101-
array_unshift($methodCalls, array($constructor, $value->getArguments()));
113+
array_unshift($this->methodCalls, array($constructor, $value->getArguments()));
102114
}
103115

104-
$methodCalls = $this->autowireCalls($reflectionClass, $methodCalls);
116+
$this->methodCalls = $this->autowireCalls($reflectionClass, $isRoot);
105117

106118
if ($constructor) {
107-
list(, $arguments) = array_shift($methodCalls);
119+
list(, $arguments) = array_shift($this->methodCalls);
108120

109121
if ($arguments !== $value->getArguments()) {
110122
$value->setArguments($arguments);
111123
}
112124
}
113125

114-
if ($methodCalls !== $value->getMethodCalls()) {
115-
$value->setMethodCalls($methodCalls);
126+
if ($this->methodCalls !== $value->getMethodCalls()) {
127+
$value->setMethodCalls($this->methodCalls);
116128
}
117129

118130
return $value;
119131
}
120132

121133
/**
122134
* @param \ReflectionClass $reflectionClass
123-
* @param array $methodCalls
124135
*
125136
* @return array
126137
*/
127-
private function autowireCalls(\ReflectionClass $reflectionClass, array $methodCalls)
138+
private function autowireCalls(\ReflectionClass $reflectionClass, bool $isRoot): array
128139
{
129-
foreach ($methodCalls as $i => $call) {
140+
if ($isRoot && ($definition = $this->container->getDefinition($this->currentId)) && $this->container->has($this->decoratedId = $definition->innerServiceId)) {
141+
$this->decoratedClass = $this->container->findDefinition($this->decoratedId)->getClass();
142+
} else {
143+
$this->decoratedId = null;
144+
$this->decoratedClass = null;
145+
}
146+
147+
foreach ($this->methodCalls as $i => $call) {
148+
$this->decoratedMethodIndex = $i;
130149
list($method, $arguments) = $call;
131150

132151
if ($method instanceof \ReflectionFunctionAbstract) {
@@ -138,11 +157,11 @@ private function autowireCalls(\ReflectionClass $reflectionClass, array $methodC
138157
$arguments = $this->autowireMethod($reflectionMethod, $arguments);
139158

140159
if ($arguments !== $call[1]) {
141-
$methodCalls[$i][1] = $arguments;
160+
$this->methodCalls[$i][1] = $arguments;
142161
}
143162
}
144163

145-
return $methodCalls;
164+
return $this->methodCalls;
146165
}
147166

148167
/**
@@ -190,18 +209,40 @@ private function autowireMethod(\ReflectionFunctionAbstract $reflectionMethod, a
190209
continue;
191210
}
192211

193-
if (!$value = $this->getAutowiredReference($ref = new TypedReference($type, $type, !$parameter->isOptional() ? $class : ''), 'for '.sprintf('argument "$%s" of method "%s()"', $parameter->name, $class.'::'.$method))) {
194-
$failureMessage = $this->createTypeNotFoundMessage($ref, sprintf('argument "$%s" of method "%s()"', $parameter->name, $class !== $this->currentId ? $class.'::'.$method : $method));
212+
$getValue = function () use ($type, $parameter, $class, $method) {
213+
if (!$value = $this->getAutowiredReference($ref = new TypedReference($type, $type, !$parameter->isOptional() ? $class : ''), 'for '.sprintf('argument "$%s" of method "%s()"', $parameter->name, $class.'::'.$method))) {
214+
$failureMessage = $this->createTypeNotFoundMessage($ref, sprintf('argument "$%s" of method "%s()"', $parameter->name, $class !== $this->currentId ? $class.'::'.$method : $method));
215+
216+
if ($parameter->isDefaultValueAvailable()) {
217+
$value = $parameter->getDefaultValue();
218+
} elseif (!$parameter->allowsNull()) {
219+
throw new AutowiringFailedException($this->currentId, $failureMessage);
220+
}
221+
$this->container->log($this, $failureMessage);
222+
}
223+
224+
return $value;
225+
};
226+
227+
if ($this->decoratedClass && $isDecorated = is_a($this->decoratedClass, $type, true)) {
228+
if ($this->getPreviousValue) {
229+
// The inner service is injected only if there is only 1 argument matching the type of the decorated class
230+
// across all arguments of all autowired methods.
231+
// If a second matching argument is found, the default behavior is restored.
195232

196-
if ($parameter->isDefaultValueAvailable()) {
197-
$value = $parameter->getDefaultValue();
198-
} elseif (!$parameter->allowsNull()) {
199-
throw new AutowiringFailedException($this->currentId, $failureMessage);
233+
$getPreviousValue = $this->getPreviousValue;
234+
$this->methodCalls[$this->decoratedMethodIndex][1][$this->decoratedMethodArgumentIndex] = $getPreviousValue();
235+
$this->decoratedClass = null; // Prevent further checks
236+
} else {
237+
$arguments[$index] = new TypedReference($this->decoratedId, $this->decoratedClass);
238+
$this->getPreviousValue = $getValue;
239+
$this->decoratedMethodArgumentIndex = $index;
240+
241+
continue;
200242
}
201-
$this->container->log($this, $failureMessage);
202243
}
203244

204-
$arguments[$index] = $value;
245+
$arguments[$index] = $getValue();
205246
}
206247

207248
if ($parameters && !isset($arguments[++$index])) {

Compiler/DecoratorServicePass.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ public function process(ContainerBuilder $container)
4343
if (!$renamedId) {
4444
$renamedId = $id.'.inner';
4545
}
46+
$definition->innerServiceId = $renamedId;
4647

4748
// we create a new alias/service for the service we are replacing
4849
// to be able to reference it in the new one

Definition.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,13 @@ class Definition
4949

5050
private static $defaultDeprecationTemplate = 'The "%service_id%" service is deprecated. You should stop using it, as it will soon be removed.';
5151

52+
/**
53+
* @internal
54+
*
55+
* Used to store the name of the inner id when using service decoration together with autowiring
56+
*/
57+
public $innerServiceId;
58+
5259
/**
5360
* @param string|null $class The service class
5461
* @param array $arguments An array of arguments to pass to the service constructor

Tests/Compiler/AutowirePassTest.php

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,12 @@
1212
namespace Symfony\Component\DependencyInjection\Tests\Compiler;
1313

1414
use PHPUnit\Framework\TestCase;
15+
use Psr\Log\LoggerInterface;
16+
use Psr\Log\NullLogger;
1517
use Symfony\Component\Config\FileLocator;
1618
use Symfony\Component\DependencyInjection\Compiler\AutowireRequiredMethodsPass;
1719
use Symfony\Component\DependencyInjection\Compiler\AutowirePass;
20+
use Symfony\Component\DependencyInjection\Compiler\DecoratorServicePass;
1821
use Symfony\Component\DependencyInjection\Compiler\ResolveClassPass;
1922
use Symfony\Component\DependencyInjection\ContainerBuilder;
2023
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
@@ -787,4 +790,59 @@ public function testInlineServicesAreNotCandidates()
787790

788791
$this->assertSame(array(), $container->getDefinition('autowired')->getArguments());
789792
}
793+
794+
public function testAutowireDecorator()
795+
{
796+
$container = new ContainerBuilder();
797+
$container->register(LoggerInterface::class, NullLogger::class);
798+
$container->register(Decorated::class, Decorated::class);
799+
$container
800+
->register(Decorator::class, Decorator::class)
801+
->setDecoratedService(Decorated::class)
802+
->setAutowired(true)
803+
;
804+
805+
(new DecoratorServicePass())->process($container);
806+
(new AutowirePass())->process($container);
807+
808+
$definition = $container->getDefinition(Decorator::class);
809+
$this->assertSame(Decorator::class.'.inner', (string) $definition->getArgument(1));
810+
}
811+
812+
public function testAutowireDecoratorRenamedId()
813+
{
814+
$container = new ContainerBuilder();
815+
$container->register(LoggerInterface::class, NullLogger::class);
816+
$container->register(Decorated::class, Decorated::class);
817+
$container
818+
->register(Decorator::class, Decorator::class)
819+
->setDecoratedService(Decorated::class, 'renamed')
820+
->setAutowired(true)
821+
;
822+
823+
(new DecoratorServicePass())->process($container);
824+
(new AutowirePass())->process($container);
825+
826+
$definition = $container->getDefinition(Decorator::class);
827+
$this->assertSame('renamed', (string) $definition->getArgument(1));
828+
}
829+
830+
/**
831+
* @expectedException \Symfony\Component\DependencyInjection\Exception\AutowiringFailedException
832+
* @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?
833+
*/
834+
public function testDoNotAutowireDecoratorWhenSeveralArgumentOfTheType()
835+
{
836+
$container = new ContainerBuilder();
837+
$container->register(LoggerInterface::class, NullLogger::class);
838+
$container->register(Decorated::class, Decorated::class);
839+
$container
840+
->register(NonAutowirableDecorator::class, NonAutowirableDecorator::class)
841+
->setDecoratedService(Decorated::class)
842+
->setAutowired(true)
843+
;
844+
845+
(new DecoratorServicePass())->process($container);
846+
(new AutowirePass())->process($container);
847+
}
790848
}

Tests/Fixtures/includes/autowiring_classes.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace Symfony\Component\DependencyInjection\Tests\Compiler;
44

5+
use Psr\Log\LoggerInterface;
6+
57
class Foo
68
{
79
}
@@ -352,3 +354,28 @@ public function setDefaultLocale($defaultLocale)
352354
{
353355
}
354356
}
357+
358+
interface DecoratorInterface
359+
{
360+
}
361+
362+
class Decorated implements DecoratorInterface
363+
{
364+
public function __construct($quz = null, \NonExistent $nonExistent = null, DecoratorInterface $decorated = null, array $foo = array())
365+
{
366+
}
367+
}
368+
369+
class Decorator implements DecoratorInterface
370+
{
371+
public function __construct(LoggerInterface $logger, DecoratorInterface $decorated)
372+
{
373+
}
374+
}
375+
376+
class NonAutowirableDecorator implements DecoratorInterface
377+
{
378+
public function __construct(LoggerInterface $logger, DecoratorInterface $decorated1, DecoratorInterface $decorated2)
379+
{
380+
}
381+
}

Tests/Fixtures/php/services_subscriber.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ public function getRemovedIds()
5757
'Psr\\Container\\ContainerInterface' => true,
5858
'Symfony\\Component\\DependencyInjection\\ContainerInterface' => true,
5959
'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CustomDefinition' => true,
60-
'service_locator.MtGsMEd' => true,
61-
'service_locator.MtGsMEd.foo_service' => true,
60+
'service_locator.KT3jhJ7' => true,
61+
'service_locator.KT3jhJ7.foo_service' => true,
6262
);
6363
}
6464

0 commit comments

Comments
 (0)