Skip to content

Commit 018ad42

Browse files
[DI] Extend semantic of @param to describe intent and use it when autowiring services and parameters
1 parent bad5680 commit 018ad42

File tree

4 files changed

+203
-0
lines changed

4 files changed

+203
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\DependencyInjection\Compiler;
13+
14+
use Symfony\Component\DependencyInjection\Definition;
15+
use Symfony\Component\DependencyInjection\Reference;
16+
17+
/**
18+
* Looks for definitions with autowiring enabled and parses their "@param" annotations for service ids and parameters.
19+
*
20+
* @author Nicolas Grekas <p@tchwork.com>
21+
*/
22+
class AutowireAnnotatedArgumentsPass extends AbstractRecursivePass
23+
{
24+
/**
25+
* {@inheritdoc}
26+
*/
27+
protected function processValue($value, $isRoot = false)
28+
{
29+
$value = parent::processValue($value, $isRoot);
30+
31+
if (!$value instanceof Definition || !$value->isAutowired() || $value->isAbstract() || !$value->getClass()) {
32+
return $value;
33+
}
34+
if (!$reflectionClass = $this->container->getReflectionClass($value->getClass(), false)) {
35+
return $value;
36+
}
37+
38+
try {
39+
if ($constructor = $this->getConstructor($value, false)) {
40+
$constructor = strtolower($constructor->name);
41+
}
42+
} catch (RuntimeException $e) {
43+
return $value;
44+
}
45+
if (!$annotatedParams = $this->getAnnotatedParams($reflectionClass, $constructor, $value)) {
46+
return $value;
47+
}
48+
$methodCalls = $value->getMethodCalls();
49+
50+
if ($constructor) {
51+
array_unshift($methodCalls, array($constructor, $value->getArguments()));
52+
}
53+
54+
foreach ($methodCalls as $i => $call) {
55+
list($method, $arguments) = $call;
56+
if (!isset($annotatedParams[$m = strtolower($method)])) {
57+
continue;
58+
}
59+
60+
foreach ($annotatedParams[$m] as $index => $param) {
61+
if (!array_key_exists($index, $arguments) || ('' === $arguments[$index] && $param instanceof Reference)) {
62+
$arguments[$index] = $param;
63+
}
64+
}
65+
ksort($arguments);
66+
67+
if ($arguments !== $call[1]) {
68+
$methodCalls[$i][1] = $arguments;
69+
}
70+
}
71+
72+
if ($constructor) {
73+
list(, $arguments) = array_shift($methodCalls);
74+
75+
if ($arguments !== $value->getArguments()) {
76+
$value->setArguments($arguments);
77+
}
78+
}
79+
80+
if ($methodCalls !== $value->getMethodCalls()) {
81+
$value->setMethodCalls($methodCalls);
82+
}
83+
84+
return $value;
85+
}
86+
87+
private function getAnnotatedParams(\ReflectionClass $reflectionClass, ?string $constructor, Definition $definition): array
88+
{
89+
$annotatedParams = array();
90+
91+
if (null !== $constructor) {
92+
$annotatedParams[$constructor] = array();
93+
}
94+
foreach ($definition->getMethodCalls() as list($method)) {
95+
$annotatedParams[strtolower($method)] = array();
96+
}
97+
98+
foreach ($reflectionClass->getMethods() as $reflectionMethod) {
99+
$r = $reflectionMethod;
100+
if (!isset($annotatedParams[$m = strtolower($r->name)])) {
101+
continue;
102+
}
103+
104+
while (true) {
105+
if (false !== $doc = $r->getDocComment()) {
106+
if (false !== stripos($doc, '@param') && preg_match_all('#(?:^/\*\*|\n\s*+\*)\s*+@param\s[^\$\*]*+\$([^\s\*]++)\s++(@[^\s\*]++|%[^%\s\*]++%)(?=[\s\*])#i', $doc, $params, PREG_SET_ORDER)) {
107+
$paramIndex = array();
108+
foreach ($r->getParameters() as $i => $p) {
109+
$paramIndex[$p->name] = $i;
110+
}
111+
foreach ($params as $p) {
112+
if (!isset($paramIndex[$p[1]])) {
113+
$this->container->log($this, sprintf('Skipping @param "$%s": no such argument on "%s::%s()".', $p[1], $r->class, $r->name));
114+
continue;
115+
}
116+
if ('@' === $p[2][0] && $this->container->has($id = substr($p[2], 1))) {
117+
$p[2] = new Reference($id);
118+
} elseif ('%' === $p[2][0] && $this->container->hasParameter($id = substr($p[2], 1, -1))) {
119+
$p[2] = $this->container->getParameter($id);
120+
} else {
121+
$this->container->log($this, sprintf('Skipping @param "$%s" on "%s::%s(): %s "%s" not found.', $p[1], $r->class, $r->name, '@' === $p[2][0] ? 'service' : 'parameter', $id));
122+
continue;
123+
}
124+
$annotatedParams[$m] += array($paramIndex[$p[1]] => $p[2]);
125+
}
126+
}
127+
if (false === stripos($doc, '@inheritdoc') || !preg_match('#(?:^/\*\*|\n\s*+\*)\s*+(?:\{@inheritdoc\}|@inheritdoc)(?:\s|\*/$)#i', $doc)) {
128+
break;
129+
}
130+
}
131+
try {
132+
$r = $r->getPrototype();
133+
} catch (\ReflectionException $e) {
134+
break; // method has no prototype
135+
}
136+
}
137+
}
138+
139+
return array_filter($annotatedParams);
140+
}
141+
}

src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php

+1
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ public function __construct()
6060
new ResolveNamedArgumentsPass(),
6161
new AutowireRequiredMethodsPass(),
6262
new ResolveBindingsPass(),
63+
new AutowireAnnotatedArgumentsPass(),
6364
new AutowirePass(false),
6465
new ResolveTaggedIteratorArgumentPass(),
6566
new ResolveServiceSubscribersPass(),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\DependencyInjection\Tests\Compiler;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\DependencyInjection\Compiler\ResolveClassPass;
16+
use Symfony\Component\DependencyInjection\Compiler\AutowireAnnotatedArgumentsPass;
17+
use Symfony\Component\DependencyInjection\ContainerBuilder;
18+
use Symfony\Component\DependencyInjection\Reference;
19+
20+
require_once __DIR__.'/../Fixtures/includes/autowiring_classes.php';
21+
22+
class AutowireAnnotatedArgumentsPassTest extends TestCase
23+
{
24+
public function testSetterInjection()
25+
{
26+
$container = new ContainerBuilder();
27+
$container->setParameter('a', 'a');
28+
$container->setParameter('b', 'b');
29+
$container->register('c');
30+
$container->register('d');
31+
$foo = $container->register(AnnotatedParamsFoo::class)
32+
->setArguments(array(0 => 'A', 2 => new Reference('C')))
33+
->setAutowired(true)
34+
;
35+
36+
(new ResolveClassPass())->process($container);
37+
(new AutowireAnnotatedArgumentsPass())->process($container);
38+
39+
$expected = array(
40+
'A',
41+
'b',
42+
new Reference('C'),
43+
new Reference('d'),
44+
);
45+
$this->assertEquals($expected, $foo->getArguments());
46+
47+
}
48+
}

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

+13
Original file line numberDiff line numberDiff line change
@@ -379,3 +379,16 @@ public function __construct(LoggerInterface $logger, DecoratorInterface $decorat
379379
{
380380
}
381381
}
382+
383+
class AnnotatedParamsFoo
384+
{
385+
/**
386+
* @param $a %a%
387+
* @param $b %b%
388+
* @param $c @c
389+
* @param $d @d
390+
*/
391+
public function __construct($a, $b, $c, $d)
392+
{
393+
}
394+
}

0 commit comments

Comments
 (0)