Skip to content

Commit 7c91b1a

Browse files
[EXPERIMENTAL][DI] Add tail injection for methods + autowiring
1 parent b5d9b3b commit 7c91b1a

30 files changed

+736
-30
lines changed

src/Symfony/Component/DependencyInjection/CHANGELOG.md

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

7+
* [EXPERIMENTAL] added support for tail-injection for methods + autowiring
78
* [EXPERIMENTAL] added "service-locator" argument for lazy loading a set of identified values and services
89
* [EXPERIMENTAL] added prototype services for PSR4-based discovery and registration
910
* added `ContainerBuilder::getReflectionClass()` for retrieving and tracking reflection class info

src/Symfony/Component/DependencyInjection/ChildDefinition.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,16 @@ public function setAutowiredCalls(array $autowiredCalls)
169169
return parent::setAutowiredCalls($autowiredCalls);
170170
}
171171

172+
/**
173+
* {@inheritdoc}
174+
*/
175+
public function setAutowiredTails(array $autowiredTails)
176+
{
177+
$this->changes['autowired_tails'] = true;
178+
179+
return parent::setAutowiredTails($autowiredTails);
180+
}
181+
172182
/**
173183
* Gets an argument to pass to the service constructor/factory method.
174184
*

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ protected function processValue($value, $isRoot = false)
6262
$value->setArguments($this->processValue($value->getArguments()));
6363
$value->setProperties($this->processValue($value->getProperties()));
6464
$value->setOverriddenGetters($this->processValue($value->getOverriddenGetters()));
65+
$value->setOverridenTails($this->processValue($value->getOverridenTails()));
6566
$value->setMethodCalls($this->processValue($value->getMethodCalls()));
6667

6768
if ($v = $value->getFactory()) {

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

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -104,11 +104,17 @@ protected function processValue($value, $isRoot = false)
104104
}
105105
$this->lazy = false;
106106

107-
if ($this->onlyConstructorArguments) {
108-
$this->processValue($value->getFactory());
109-
$this->processValue($value->getArguments());
110-
} else {
111-
parent::processValue($value, $isRoot);
107+
$this->processValue($value->getFactory());
108+
$this->processValue($value->getArguments());
109+
110+
if (!$this->onlyConstructorArguments) {
111+
$this->processValue($value->getProperties());
112+
$this->lazy = true;
113+
$this->processValue($value->getOverriddenGetters());
114+
$this->processValue($value->getOverridenTails());
115+
$this->lazy = false;
116+
$this->processValue($value->getMethodCalls());
117+
$this->processValue($value->getConfigurator());
112118
}
113119
$this->lazy = $lazy;
114120

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

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ class AutowirePass extends AbstractRecursivePass
3434
*/
3535
const MODE_OPTIONAL = 2;
3636

37+
/**
38+
* @internal
39+
*/
40+
const MODE_TAIL = 3;
41+
3742
private $definedTypes = array();
3843
private $types;
3944
private $ambiguousServiceTypes = array();
@@ -82,7 +87,7 @@ public static function createResourceForClass(\ReflectionClass $reflectionClass)
8287
*/
8388
protected function processValue($value, $isRoot = false)
8489
{
85-
if (!$value instanceof Definition || !$value->getAutowiredCalls()) {
90+
if (!$value instanceof Definition || !($value->getAutowiredCalls() || $value->getAutowiredTails())) {
8691
return parent::processValue($value, $isRoot);
8792
}
8893

@@ -118,6 +123,13 @@ protected function processValue($value, $isRoot = false)
118123
$value->setOverriddenGetters($overriddenGetters);
119124
}
120125

126+
$autowiredMethods = $this->getMethodsToAutowire($reflectionClass, $value->getAutowiredTails());
127+
$overridenTails = $this->autowireTails($reflectionClass, $value->getOverridenTails(), $autowiredMethods);
128+
129+
if ($overridenTails !== $value->getOverridenTails()) {
130+
$value->setOverridenTails($overridenTails);
131+
}
132+
121133
return parent::processValue($value, $isRoot);
122134
}
123135

@@ -223,7 +235,12 @@ private function autowireCalls(\ReflectionClass $reflectionClass, array $methodC
223235
private function autowireMethod(\ReflectionMethod $reflectionMethod, array $arguments, $mode)
224236
{
225237
$didAutowire = false; // Whether any arguments have been autowired or not
226-
foreach ($reflectionMethod->getParameters() as $index => $parameter) {
238+
239+
$parameters = $reflectionMethod->getParameters();
240+
if (self::MODE_TAIL === $mode) {
241+
$parameters = array_reverse($parameters, true);
242+
}
243+
foreach ($parameters as $index => $parameter) {
227244
if (array_key_exists($index, $arguments) && '' !== $arguments[$index]) {
228245
continue;
229246
}
@@ -245,6 +262,9 @@ private function autowireMethod(\ReflectionMethod $reflectionMethod, array $argu
245262
if (self::MODE_REQUIRED === $mode) {
246263
throw new RuntimeException(sprintf('Cannot autowire service "%s": argument $%s of method %s::%s() must have a type-hint or be given a value explicitly.', $this->currentId, $parameter->name, $reflectionMethod->class, $reflectionMethod->name));
247264
}
265+
if (self::MODE_TAIL === $mode) {
266+
break;
267+
}
248268

249269
return array();
250270
}
@@ -285,6 +305,9 @@ private function autowireMethod(\ReflectionMethod $reflectionMethod, array $argu
285305
if (1 === $e->getCode() || self::MODE_REQUIRED === $mode) {
286306
throw $e;
287307
}
308+
if (self::MODE_TAIL === $mode) {
309+
break;
310+
}
288311

289312
return array();
290313
}
@@ -296,6 +319,9 @@ private function autowireMethod(\ReflectionMethod $reflectionMethod, array $argu
296319
if (self::MODE_REQUIRED === $mode) {
297320
throw new RuntimeException(sprintf('Cannot autowire argument $%s of method %s::%s() for service "%s": Class %s does not exist.', $parameter->name, $reflectionMethod->class, $reflectionMethod->name, $this->currentId, $typeName));
298321
}
322+
if (self::MODE_TAIL === $mode) {
323+
break;
324+
}
299325

300326
return array();
301327
}
@@ -366,6 +392,38 @@ private function autowireOverridenGetters(array $overridenGetters, array $autowi
366392
return $overridenGetters;
367393
}
368394

395+
/**
396+
* Autowires method tails.
397+
*
398+
* @return array
399+
*/
400+
private function autowireTails(\ReflectionClass $reflectionClass, array $overridenTails, array $autowiredMethods)
401+
{
402+
foreach ($overridenTails as $lcMethod => $defaultValues) {
403+
if (isset($autowiredMethods[$lcMethod]) || !$reflectionClass->hasMethod($lcMethod)) {
404+
continue;
405+
}
406+
$autowiredMethods[$lcMethod] = $reflectionClass->getMethod($lcMethod);
407+
}
408+
409+
foreach ($autowiredMethods as $lcMethod => $reflectionMethod) {
410+
if ($reflectionMethod->isConstructor()
411+
|| !$reflectionMethod->getNumberOfParameters()
412+
|| $reflectionMethod->isFinal()
413+
) {
414+
continue;
415+
}
416+
417+
$defaultValues = isset($overridenTails[$lcMethod]) ? $overridenTails[$lcMethod] : array();
418+
419+
if ($defaultValues = $this->autowireMethod($reflectionMethod, $defaultValues, self::MODE_TAIL)) {
420+
$overridenTails[$lcMethod] = $defaultValues;
421+
}
422+
}
423+
424+
return $overridenTails;
425+
}
426+
369427
/**
370428
* Populates the list of available types.
371429
*/

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Symfony\Component\DependencyInjection\ChildDefinition;
1515
use Symfony\Component\DependencyInjection\Definition;
1616
use Symfony\Component\DependencyInjection\Exception\ExceptionInterface;
17+
use Symfony\Component\DependencyInjection\Exception\OutOfBoundsException;
1718
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
1819

1920
/**
@@ -89,6 +90,7 @@ private function doResolveDefinition(ChildDefinition $definition)
8990
$def->setArguments($parentDef->getArguments());
9091
$def->setMethodCalls($parentDef->getMethodCalls());
9192
$def->setOverriddenGetters($parentDef->getOverriddenGetters());
93+
$def->setOverridenTails($parentDef->getOverridenTails());
9294
$def->setProperties($parentDef->getProperties());
9395
if ($parentDef->getAutowiringTypes(false)) {
9496
$def->setAutowiringTypes($parentDef->getAutowiringTypes(false));
@@ -102,6 +104,7 @@ private function doResolveDefinition(ChildDefinition $definition)
102104
$def->setPublic($parentDef->isPublic());
103105
$def->setLazy($parentDef->isLazy());
104106
$def->setAutowiredCalls($parentDef->getAutowiredCalls());
107+
$def->setAutowiredTails($parentDef->getAutowiredTails());
105108

106109
// overwrite with values specified in the decorator
107110
$changes = $definition->getChanges();
@@ -129,6 +132,9 @@ private function doResolveDefinition(ChildDefinition $definition)
129132
if (isset($changes['autowired_calls'])) {
130133
$def->setAutowiredCalls($definition->getAutowiredCalls());
131134
}
135+
if (isset($changes['autowired_tails'])) {
136+
$def->setAutowiredTails($definition->getAutowiredTails());
137+
}
132138
if (isset($changes['decorated_service'])) {
133139
$decoratedService = $definition->getDecoratedService();
134140
if (null === $decoratedService) {
@@ -169,6 +175,29 @@ private function doResolveDefinition(ChildDefinition $definition)
169175
$def->setOverriddenGetter($k, $v);
170176
}
171177

178+
// merge overridden tails
179+
$tails = $def->getOverridenTails();
180+
foreach ($definition->getOverridenTails() as $method => $defaultValues) {
181+
foreach ($defaultValues as $k => $v) {
182+
if (is_numeric($k)) {
183+
$tails[$method][] = $v;
184+
continue;
185+
}
186+
187+
if (0 !== strpos($k, 'index_')) {
188+
throw new RuntimeException(sprintf('Invalid argument key "%s" found.', $k));
189+
}
190+
191+
$index = (int) substr($k, strlen('index_'));
192+
if (!isset($tails[$method][$index])) {
193+
$range = array_keys($tails[$method]);
194+
throw new OutOfBoundsException(sprintf('The index "%d" is not in the range [%s, %s] defined by the parent.', $index, min($range), max($range)));
195+
}
196+
$tails[$method][$index] = $v;
197+
}
198+
}
199+
$def->setOverridenTails($tails);
200+
172201
// merge autowiring types
173202
foreach ($definition->getAutowiringTypes(false) as $autowiringType) {
174203
$def->addAutowiringType($autowiringType);

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ private function processValue($value, $rootLevel = 0, $level = 0)
6060
$value->setArguments($this->processValue($value->getArguments(), 0));
6161
$value->setProperties($this->processValue($value->getProperties(), 1));
6262
$value->setOverriddenGetters($this->processValue($value->getOverriddenGetters(), 1));
63+
$value->setOverridenTails($this->processValue($value->getOverridenTails(), 1));
6364
$value->setMethodCalls($this->processValue($value->getMethodCalls(), 2));
6465
} elseif (is_array($value)) {
6566
$i = 0;

src/Symfony/Component/DependencyInjection/Compiler/ResolveReferencesToAliasesPass.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
$definition->setArguments($this->processArguments($definition->getArguments()));
4444
$definition->setMethodCalls($this->processArguments($definition->getMethodCalls()));
4545
$definition->setOverriddenGetters($this->processArguments($definition->getOverriddenGetters()));
46+
$definition->setOverridenTails($this->processArguments($definition->getOverridenTails()));
4647
$definition->setProperties($this->processArguments($definition->getProperties()));
4748
$definition->setFactory($this->processFactory($definition->getFactory()));
4849
}

src/Symfony/Component/DependencyInjection/ContainerBuilder.php

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1029,6 +1029,9 @@ private function createService(Definition $definition, $id, $tryProxy = true)
10291029
if ($definition->getOverriddenGetters()) {
10301030
throw new RuntimeException(sprintf('Cannot create service "%s": factories and overridden getters are incompatible with each other.', $id));
10311031
}
1032+
if ($definition->getOverridenTails()) {
1033+
throw new RuntimeException(sprintf('Cannot create service "%s": factories and overridden tails are incompatible with each other.', $id));
1034+
}
10321035
if (is_array($factory)) {
10331036
$factory = array($this->resolveServices($parameterBag->resolveValue($factory[0])), $factory[1]);
10341037
} elseif (!is_string($factory)) {
@@ -1050,12 +1053,12 @@ private function createService(Definition $definition, $id, $tryProxy = true)
10501053
if (!$definition->isDeprecated() && 0 < strpos($r->getDocComment(), "\n * @deprecated ")) {
10511054
@trigger_error(sprintf('The "%s" service relies on the deprecated "%s" class. It should either be deprecated or its implementation upgraded.', $id, $r->name), E_USER_DEPRECATED);
10521055
}
1053-
if ($definition->getOverriddenGetters()) {
1056+
if ($definition->getOverriddenGetters() || $definition->getOverridenTails()) {
10541057
static $salt;
10551058
if (null === $salt) {
10561059
$salt = str_replace('.', '', uniqid('', true));
10571060
}
1058-
$service = sprintf('%s implements \\%s { private $container%4$s; private $getters%4$s; %s }', $r->name, InheritanceProxyInterface::class, $this->generateOverriddenMethods($id, $definition, $r, $salt), $salt);
1061+
$service = sprintf('%s implements \\%s { private $container%4$s; private $getters%4$s; private $tails%4$s; %s }', $r->name, InheritanceProxyInterface::class, $this->generateOverriddenMethods($id, $definition, $r, $salt), $salt);
10591062
if (!class_exists($proxyClass = 'SymfonyProxy_'.md5($service), false)) {
10601063
eval(sprintf('class %s extends %s', $proxyClass, $service));
10611064
}
@@ -1065,7 +1068,7 @@ private function createService(Definition $definition, $id, $tryProxy = true)
10651068
$constructor = null;
10661069
}
10671070
$service = $constructor ? $r->newInstanceWithoutConstructor() : $r->newInstanceArgs($arguments);
1068-
call_user_func(\Closure::bind(function ($c, $g, $s) { $this->{'container'.$s} = $c; $this->{'getters'.$s} = $g; }, $service, $service), $this, $definition->getOverriddenGetters(), $salt);
1071+
call_user_func(\Closure::bind(function ($c, $g, $p, $s) { $this->{'container'.$s} = $c; $this->{'getters'.$s} = $g; $this->{'tails'.$s} = $p; }, $service, $service), $this, $definition->getOverriddenGetters(), $definition->getOverridenTails(), $salt);
10691072
if ($constructor) {
10701073
$constructor->invokeArgs($service, $arguments);
10711074
}
@@ -1342,7 +1345,7 @@ private function generateOverriddenMethods($id, Definition $definition, \Reflect
13421345
throw new RuntimeException(sprintf('Unable to configure service "%s": class "%s" cannot be marked as final.', $id, $class->name));
13431346
}
13441347

1345-
return $this->generateOverriddenGetters($id, $definition, $class, $salt);
1348+
return $this->generateOverriddenGetters($id, $definition, $class, $salt).$this->generateOverridenTails($id, $definition, $class, $salt);
13461349
}
13471350

13481351
private function generateOverriddenGetters($id, Definition $definition, \ReflectionClass $class, $salt)
@@ -1374,6 +1377,43 @@ private function generateOverriddenGetters($id, Definition $definition, \Reflect
13741377
return $getters;
13751378
}
13761379

1380+
private function generateOverridenTails($id, Definition $definition, \ReflectionClass $class, $salt)
1381+
{
1382+
$tails = '';
1383+
1384+
foreach ($definition->getOverridenTails() as $name => $defaultArgs) {
1385+
$r = InheritanceProxyHelper::getReflector($class, $name, $id);
1386+
if (!$defaultArgs = InheritanceProxyHelper::getTailArgs($r, $defaultArgs, $id)) {
1387+
continue;
1388+
}
1389+
1390+
$tail = array();
1391+
$tail[] = sprintf("\n%s function %s\n{", $r->isProtected() ? 'protected' : 'public', InheritanceProxyHelper::getSignature($r, $call, $defaultArgs));
1392+
$tail[] = "\$c{$salt} = \$this->container{$salt};";
1393+
$tail[] = "\$b{$salt} = \$c{$salt}->getParameterBag();";
1394+
$tail[] = "\$v{$salt} = \$this->tails{$salt}['{$name}'];";
1395+
$tail[] = 'switch (func_num_args()) {';
1396+
1397+
if ($numReqArgs = key($defaultArgs)) {
1398+
for ($i = 0; $i < $numReqArgs; ++$i) {
1399+
$tail[] = "case $i:";
1400+
}
1401+
}
1402+
1403+
foreach ($defaultArgs as $i => list($param, $value)) {
1404+
if (null === $value && !$param->allowsNull()) {
1405+
throw new RuntimeException(sprintf('Unable to configure service "%s": argument %d ($%s) of method "%s::%s()" must be non-null.', $id, $i, $param->name, $class->name, $r->name));
1406+
}
1407+
$tail[] = "case $i: \${$param->name} = \$c{$salt}->resolveServices(\$b{$salt}->unescapeValue(\$b{$salt}->resolveValue(\$v{$salt}[{$i}])));";
1408+
}
1409+
1410+
$tail[] = sprintf("}\nreturn parent::%s;\n}", $call);
1411+
$tails .= implode("\n", $tail);
1412+
}
1413+
1414+
return $tails;
1415+
}
1416+
13771417
/**
13781418
* @internal
13791419
*/

0 commit comments

Comments
 (0)