8000 feature #21770 [DI] Allow extensions to create ServiceLocator as serv… · symfony/symfony@c6e1a49 · GitHub
[go: up one dir, main page]

Skip to content

Commit c6e1a49

Browse files
committed
feature #21770 [DI] Allow extensions to create ServiceLocator as services (nicolas-grekas)
This PR was merged into the 3.3-dev branch. Discussion ---------- [DI] Allow extensions to create ServiceLocator as services | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | - | License | MIT | Doc PR | - https://github.com/symfony/symfony/pull/21770/files?w=1 With this PR, DI extensions are able to create "service locator" services. They are easily created as such: ```php $container->register('my_service_locator', ServiceLocator::class) ->addArgument(array( 'exposed_id' => new ServiceClosureArgument(new Reference('internal_id')), )) ``` I already need this in two different PRs to come. Commits ------- 1d96633 [DI] Allow creating ServiceLocator-based services in extensions
2 parents ed30348 + 1d96633 commit c6e1a49

19 files changed

+602
-31
lines changed

src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/JsonDescriptor.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Symfony\Component\DependencyInjection\Alias;
1515
use Symfony\Component\DependencyInjection\Argument\ArgumentInterface;
16+
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
1617
use Symfony\Component\DependencyInjection\ContainerBuilder;
1718
use Symfony\Component\DependencyInjection\Definition;
1819
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
@@ -388,6 +389,10 @@ private function describeValue($value, $omitTags, $showArguments)
388389
return $data;
389390
}
390391

392+
if ($value instanceof ServiceClosureArgument) {
393+
$value = $value->getValues()[0];
394+
}
395+
391396
if ($value instanceof Reference) {
392397
return array(
393398
'type' => 'service',

src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/TextDescriptor.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use Symfony\Component\DependencyInjection\Alias;
1717
use Symfony\Component\DependencyInjection\Argument\ClosureProxyArgument;
1818
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
19+
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
1920
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
2021
use Symfony\Component\DependencyInjection\ContainerBuilder;
2122
use Symfony\Component\DependencyInjection\Definition;
@@ -324,6 +325,9 @@ protected function describeContainerDefinition(Definition $definition, array $op
324325
$argumentsInformation = array();
325326
if ($showArguments && ($arguments = $definition->getArguments())) {
326327
foreach ($arguments as $argument) {
328+
if ($argument instanceof ServiceClosureArgument) {
329+
$argument = $argument->getValues()[0];
330+
}
327331
if ($argument instanceof Reference) {
328332
$argumentsInformation[] = sprintf('Service(%s)', (string) $argument);
329333
} elseif ($argument instanceof IteratorArgument) {

src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/XmlDescriptor.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Symfony\Component\DependencyInjection\Alias;
1515
use Symfony\Component\DependencyInjection\Argument\ClosureProxyArgument;
1616
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
17+
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
1718
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
1819
use Symfony\Component\DependencyInjection\ContainerBuilder;
1920
use Symfony\Component\DependencyInjection\Definition;
@@ -425,6 +426,10 @@ private function getArgumentNodes(array $arguments, \DOMDocument $dom)
425426
$argumentXML->setAttribute('key', $argumentKey);
426427
}
427428

429+
if ($argument instanceof ServiceClosureArgument) {
430+
$argument = $argument->getValues()[0];
431+
}
432+
428433
if ($argument instanceof Reference) {
429434
$argumentXML->setAttribute('type', 'service');
430435
$argumentXML->setAttribute('id', (string) $argument);
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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\Argument;
13+
14+
use Symfony\Component\DependencyInjection\Reference;
15+
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
16+
17+
/**
18+
* Represents a service wrapped in a memoizing closure.
19+
*
20+
* @author Nicolas Grekas <p@tchwork.com>
21+
*
22+
* @experimental in version 3.3
23+
*/
24+
class ServiceClosureArgument implements ArgumentInterface
25+
{
26+
private $values;
27+
28+
public function __construct(Reference $reference)
29+
{
30+
$this->values = array($reference);
31+
}
32+
33+
public function getValues()
34+
{
35+
return $this->values;
36+
}
37+
38+
public function setValues(array $values)
39+
{
40+
if (array(0) !== array_keys($values) || !($values[0] instanceof Reference || null === $values[0])) {
41+
throw new InvalidArgumentException('A ServiceClosureArgument must hold one and only one Reference.');
42+
}
43+
44+
$this->values = $values;
45+
}
46+
}

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 "TypedReference" and "ServiceClosureArgument" for creating service-locator services
78
* [EXPERIMENTAL] added "instanceof" section for local interface-defined confi 1CF5 gs
89
* added "service-locator" argument for lazy loading a set of identified values and services
910
* [EXPERIMENTAL] added prototype services for PSR4-based discovery and registration

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

Lines changed: 40 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
1818
use Symfony\Component\DependencyInjection\LazyProxy\InheritanceProxyHelper;
1919
use Symfony\Component\DependencyInjection\Reference;
20+
use Symfony\Component\DependencyInjection\TypedReference;
2021

2122
/**
2223
* Guesses constructor arguments of services definitions and try to instantiate services if necessary.
@@ -39,6 +40,7 @@ class AutowirePass extends AbstractRecursivePass
3940
private $types;
4041
private $ambiguousServiceTypes = array();
4142
private $usedTypes = array();
43+
private $currentDefinition;
4244

4345
/**
4446
* {@inheritdoc}
@@ -100,43 +102,55 @@ public static function createResourceForClass(\ReflectionClass $reflectionClass)
100102
*/
101103
protected function processValue($value, $isRoot = false)
102104
{
103-
if (!$value instanceof Definition || !$value->isAutowired()) {
104-
return parent::processValue($value, $isRoot);
105+
if ($value instanceof TypedReference && $this->currentDefinition->isAutowired() && !$this->container->has((string) $value)) {
106+
if ($ref = $this->getAutowiredReference($value->getType(), $value->canBeAutoregistered())) {
107+
$value = new TypedReference((string) $ref, $value->getType(), $value->getInvalidBehavior(), $value->canBeAutoregistered());
108+
}
105109
}
106-
107-
if (!$reflectionClass = $this->container->getReflectionClass($value->getClass())) {
110+
if (!$value instanceof Definition) {
108111
return parent::processValue($value, $isRoot);
109112
}
110113

111-
$autowiredMethods = $this->getMethodsToAutowire($reflectionClass);
112-
$methodCalls = $value->getMethodCalls();
114+
$parentDefinition = $this->currentDefinition;
115+
$this->currentDefinition = $value;
113116

114-
if ($constructor = $reflectionClass->getConstructor()) {
115-
array_unshift($methodCalls, array($constructor->name, $value->getArguments()));
116-
} elseif ($value->getArguments()) {
117-
throw new RuntimeException(sprintf('Cannot autowire service "%s": class %s has no constructor but arguments are defined.', $this->currentId, $reflectionClass->name));
118-
}
117+
try {
118+
if (!$value->isAutowired() || !$reflectionClass = $this->container->getReflectionClass($value->getClass())) {
119+
return parent::processValue($value, $isRoot);
120+
}
121+
122+
$autowiredMethods = $this->getMethodsToAutowire($reflectionClass);
123+
$methodCalls = $value->getMethodCalls();
124+
125+
if ($constructor = $reflectionClass->getConstructor()) {
126+
array_unshift($methodCalls, array($constructor->name, $value->getArguments()));
127+
} elseif ($value->getArguments()) {
128+
throw new RuntimeException(sprintf('Cannot autowire service "%s": class %s has no constructor but arguments are defined.', $this->currentId, $reflectionClass->name));
129+
}
119130

120-
$methodCalls = $this->autowireCalls($reflectionClass, $methodCalls, $autowiredMethods);
121-
$overriddenGetters = $this->autowireOverridenGetters($value->getOverriddenGetters(), $autowiredMethods);
131+
$methodCalls = $this->autowireCalls($reflectionClass, $methodCalls, $autowiredMethods);
132+
$overriddenGetters = $this->autowireOverridenGetters($value->getOverriddenGetters(), $autowiredMethods);
122133

123-
if ($constructor) {
124-
list(, $arguments) = array_shift($methodCalls);
134+
if ($constructor) {
135+
list(, $arguments) = array_shift($methodCalls);
125136

126-
if ($arguments !== $value->getArguments()) {
127-
$value->setArguments($arguments);
137+
if ($arguments !== $value->getArguments()) {
138+
$value->setArguments($arguments);
139+
}
128140
}
129-
}
130141

131-
if ($methodCalls !== $value->getMethodCalls()) {
132-
$value->setMethodCalls($methodCalls);
133-
}
142+
if ($methodCalls !== $value->getMethodCalls()) {
143+
$value->setMethodCalls($methodCalls);
144+
}
134145

135-
if ($overriddenGetters !== $value->getOverriddenGetters()) {
136-
$value->setOverriddenGetters($overriddenGetters);
137-
}
146+
if ($overriddenGetters !== $value->getOverriddenGetters()) {
147+
$value->setOverriddenGetters($overriddenGetters);
148+
}
138149

139-
return parent::processValue($value, $isRoot);
150+
return parent::processValue($value, $isRoot);
151+
} finally {
152+
$this->currentDefinition = $parentDefinition;
153+
}
140154
}
141155

142156
/**
@@ -465,7 +479,7 @@ private function createAutowiredDefinition(\ReflectionClass $typeHint)
465479

466480
$this->populateAvailableType($argumentId, $argumentDefinition);
467481

468-
$this->processValue($argumentDefinition);
482+
$this->processValue($argumentDefinition, true);
469483
$this->currentId = $currentId;
470484

471485
return new Reference($argumentId);

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Component\DependencyInjection\Compiler;
1313

1414
use Symfony\Component\DependencyInjection\Argument\ArgumentInterface;
15+
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
1516
use Symfony\Component\DependencyInjection\ContainerInterface;
1617
use Symfony\Component\DependencyInjection\Definition;
1718
use Symfony\Component\DependencyInjection\Reference;
@@ -53,7 +54,9 @@ public function process(ContainerBuilder $container)
5354
*/
5455
private function processValue($value, $rootLevel = 0, $level = 0)
5556
{
56-
if ($value instanceof ArgumentInterface) {
57+
if ($value instanceof ServiceClosureArgument) {
58+
$value->setValues($this->processValue($value->getValues(), 1, 1));
59+
} elseif ($value instanceof ArgumentInterface) {
5760
$value->setValues($this->processValue($value->getValues(), $rootLevel, 1 + $level));
5861
} elseif ($value instanceof Definition) {
5962
if ($value->isSynthetic() || $value->isAbstract()) {

src/Symfony/Component/DependencyInjection/ContainerBuilder.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Symfony\Component\DependencyInjection\Argument\ClosureProxyArgument;
1616
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
1717
use Symfony\Component\DependencyInjection\Argument\RewindableGenerator;
18+
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
1819
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
1920
use Symfony\Component\DependencyInjection\Compiler\Compiler;
2021
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
@@ -1140,11 +1141,16 @@ public function resolveServices($value)
11401141
foreach ($value as $k => $v) {
11411142
$value[$k] = $this->resolveServices($v);
11421143
}
1144+
} elseif ($value instanceof ServiceClosureArgument) {
1145+
$reference = $value->getValues()[0];
1146+
$value = function () use ($reference) {
1147+
return $this->resolveServices($reference);
1148+
};
11431149
} elseif ($value instanceof ServiceLocatorArgument) {
11441150
$parameterBag = $this->getParameterBag();
11451151
$services = array();
11461152
foreach ($value->getValues() as $k => $v) {
1147-
if ($v->getInvalidBehavior() === ContainerInterface::IGNORE_ON_INVALID_REFERENCE && !$this->has((string) $v)) {
1153+
if ($v && $v->getInvalidBehavior() === ContainerInterface::IGNORE_ON_INVALID_REFERENCE && !$this->has((string) $v)) {
11481154
continue;
11491155
}
11501156
$services[$k] = function () use ($v, $parameterBag) {

src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@
1414
use Symfony\Component\DependencyInjection\Argument\ArgumentInterface;
1515
use Symfony\Component\DependencyInjection\Argument\ClosureProxyArgument;
1616
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
17+
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
1718
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
1819
use Symfony\Component\DependencyInjection\Variable;
1920
use Symfony\Component\DependencyInjection\Definition;
2021
use Symfony\Component\DependencyInjection\ContainerBuilder;
2122
use Symfony\Component\DependencyInjection\Container;
2223
use Symfony\Component\DependencyInjection\ContainerInterface;
2324
use Symfony\Component\DependencyInjection\Reference;
25+
use Symfony\Component\DependencyInjection\TypedReference;
2426
use Symfony\Component\DependencyInjection\Parameter;
2527
use Symfony\Component\DependencyInjection\Exception\EnvParameterException;
2628
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
@@ -1540,10 +1542,12 @@ private function dumpValue($value, $interpolate = true)
15401542
}
15411543

15421544
return sprintf('array(%s)', implode(', ', $code));
1545+
} elseif ($value instanceof ServiceClosureArgument) {
1546+
return $this->dumpServiceClosure($value->getValues()[0], $interpolate, false);
15431547
} elseif ($value instanceof ServiceLocatorArgument) {
15441548
$code = "\n";
15451549
foreach ($value->getValues() as $k => $v) {
1546-
$code .= sprintf(" %s => function () { return %s; },\n", $this->dumpValue($k, $interpolate), $this->dumpValue($v, $interpolate));
1550+
$code .= sprintf(" %s => %s,\n", $this->dumpValue($k, $interpolate), $this->dumpServiceClosure($v, $interpolate, true));
15471551
}
15481552
$code .= ' ';
15491553

@@ -1681,6 +1685,27 @@ private function dumpValue($value, $interpolate = true)
16811685
return $this->export($value);
16821686
}
16831687

1688+
private function dumpServiceClosure(Reference $reference, $interpolate, $oneLine)
1689+
{
1690+
$type = '';
1691+
if (PHP_VERSION_ID >= 70000 && $reference instanceof TypedReference) {
1692+
$type = $reference->getType();
1693+
if (ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE === $reference->getInvalidBehavior()) {
1694+
$type = ': \\'.$type;
1695+
} elseif (PHP_VERSION_ID >= 70100) {
1696+
$type = ': ?\\'.$type;
1697+
} else {
1698+
$type = '';
1699+
}
1700+
}
1701+
1702+
if ($oneLine) {
1703+
return sprintf('function ()%s { return %s; }', $type, $this->dumpValue($reference, $interpolate));
1704+
}
1705+
1706+
return sprintf("function ()%s {\n return %s;\n }", $type, $this->dumpValue($reference, $interpolate));
1707+
}
1708+
16841709
/**
16851710
* Dumps a string to a literal (aka PHP Code) class value.
16861711
*

src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Symfony\Component\DependencyInjection\Argument\ClosureProxyArgument;
1515
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
16+
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
1617
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
1718
use Symfony\Component\DependencyInjection\ContainerInterface;
1819
use Symfony\Component\DependencyInjection\Parameter;
@@ -289,6 +290,9 @@ private function convertParameters(array $parameters, $type, \DOMElement $parent
289290
$element->setAttribute($keyAttribute, $key);
290291
}
291292

293+
if ($value instanceof ServiceClosureArgument) {
294+
$value = $value->getValues()[0];
295+
}
292296
if (is_array($value)) {
293297
$element->setAttribute('type', 'collection');
294298
$this->convertParameters($value, $type, $element, 'key');

src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use Symfony\Component\DependencyInjection\Argument\ArgumentInterface;
1818
use Symfony\Component\DependencyInjection\Argument\ClosureProxyArgument;
1919
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
20+
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
2021
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
2122
use Symfony\Component\DependencyInjection\ContainerInterface;
2223
use Symfony\Component\DependencyInjection\Definition;
@@ -254,6 +255,9 @@ private function dumpCallable($callable)
254255
*/
255256
private function dumpValue($value)
256257
{
258+
if ($value instanceof ServiceClosureArgument) {
259+
$value = $value->getValues()[0];
260+
}
257261
if ($value instanceof ArgumentInterface) {
258262
if ($value instanceof IteratorArgument) {
259263
$tag = 'iterator';

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use Symfony\Component\DependencyInjection\Tests\Fixtures\AbstractGetterOverriding;
1919
use Symfony\Component\DependencyInjection\Tests\Fixtures\includes\FooVariadic;
2020
use Symfony\Component\DependencyInjection\Tests\Fixtures\GetterOverriding;
21+
use Symfony\Component\DependencyInjection\TypedReference;
2122

2223
/**
2324
* @author Kévin Dunglas <dunglas@gmail.com>
@@ -518,6 +519,22 @@ public function testExplicitMethodInjection()
518519
);
519520
}
520521

522+
public function testTtypedReference()
523+
{
524+
$container = new ContainerBuilder();
525+
526+
$container
527+
->register('bar', Bar::class)
528+
->setAutowired(true)
529+
->setProperty('a', array(new TypedReference(A::class, A::class)))
530+
;
531+
532+
$pass = new AutowirePass();
533+
$pass->process($container);
534+
535+
$this->assertSame(A::class, $container->getDefinition('autowired.'.A::class)->getClass());
536+
}
537+
521538
/**
522539
* @requires PHP 7.0
523540
*/

0 commit comments

Comments
 (0)
0