Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit d4bfbb8

Browse filesBrowse files
committed
feature #25631 [DI] Service decoration: autowire the inner service (dunglas)
This PR was squashed before being merged into the 4.1-dev branch (closes #25631). Discussion ---------- [DI] Service decoration: autowire the inner service | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes <!-- don't forget to update src/**/CHANGELOG.md files --> | BC breaks? | no | Deprecations? | no <!-- don't forget to update UPGRADE-*.md files --> | Tests pass? | yes | Fixed tickets | n/a | License | MIT | Doc PR | n/a Try to automatically inject the decorated service. Before: ```yaml services: _defaults: autowire: true App\Foo: ~ App\FooDecorator: decorates: App\Foo arguments: {$decorated: @app\FooDecorator.inner} ``` After: ```yaml services: _defaults: autowire: true App\Foo: ~ App\FooDecorator: decorates: App\Foo ``` To trigger the autowiring, the following conditions must be met: * the decorator is autowired * there is only one argument in the constructor of the type of the decorated service Commits ------- 24876f2 [DI] Service decoration: autowire the inner service
2 parents 5605d2f + 24876f2 commit d4bfbb8
Copy full SHA for d4bfbb8

File tree

8 files changed

+158
-21
lines changed
Filter options

8 files changed

+158
-21
lines changed

‎.php_cs.dist

Copy file name to clipboardExpand all lines: .php_cs.dist
+2Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,7 @@ return PhpCsFixer\Config::create()
4040
->notPath('Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/default.phpt')
4141
->notPath('Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/weak.phpt')
4242
->notPath('Symfony/Component/Debug/Tests/DebugClassLoaderTest.php')
43+
// invalid annotations on purpose
44+
->notPath('Symfony/Component/DependencyInjection/Tests/Fixtures/includes/autowiring_classes.php')
4345
)
4446
;

‎src/Symfony/Component/DependencyInjection/CHANGELOG.md

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/CHANGELOG.md
+1Lines 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
-----

‎src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php
+60-19Lines 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])) {

‎src/Symfony/Component/DependencyInjection/Compiler/DecoratorServicePass.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/Compiler/DecoratorServicePass.php
+1Lines 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

‎src/Symfony/Component/DependencyInjection/Definition.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/Definition.php
+7Lines 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

‎src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php
+58Lines 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
}

‎src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/autowiring_classes.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/autowiring_classes.php
+27Lines 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+
}

‎src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_subscriber.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_subscriber.php
+2-2Lines 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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.