8000 feature #45512 [DependencyInjection] Allow using expressions as servi… · symfony/symfony@c66bb29 · GitHub
[go: up one dir, main page]

Skip to content

Commit c66bb29

Browse files
committed
feature #45512 [DependencyInjection] Allow using expressions as service factories (nicolas-grekas, jvasseur)
This PR was squashed before being merged into the 6.1 branch. Discussion ---------- [DependencyInjection] Allow using expressions as service factories | Q | A | ------------- | --- | Branch? | 6.1 | Bug fix? | no | New feature? | yes | Deprecations? | yes | Tickets | - | License | MIT | Doc PR | - Replaces #45447 This PR allows using expressions as service factories: - in YAML: `factory: '@=service("foo").bar()'` - in PHP: `->factory(expr('service("foo").bar()'))` - in XML: `<factory expression="service('foo').bar()" />` In addition, it allows the corresponding expressions to get access to the arguments of the service definition using the `arg($index)` function and `args` variable inside expressions: ```yaml services: foo: factory: '@=arg(0).baz()' # works also: @=args.get(0).baz() arguments: ['@bar'] ``` Internally, instead of allowing `Expression` objects in `Definition` objects as in #45447, factory expressions are conveyed as a strings that starts with `@=`. This is chosen by taking inspiration from yaml and to not collide with any existing callable. Commits ------- c430989 [DependencyInjection] Allow using expressions as service factories
2 parents c3c752a + c430989 commit c66bb29

30 files changed

+248
-91
lines changed

UPGRADE-6.1.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ Console
1515
* Add argument `$suggestedValues` to `Command::addArgument` and `Command::addOption`
1616
* Add argument `$suggestedValues` to `InputArgument` and `InputOption` constructors
1717

18+
DependencyInjection
19+
-------------------
20+
21+
* Deprecate `ReferenceSetArgumentTrait`
22+
1823
FrameworkBundle
1924
---------------
2025

src/Symfony/Component/DependencyInjection/Argument/IteratorArgument.php

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,20 @@
1818
*/
1919
class IteratorArgument implements ArgumentInterface
2020
{
21-
use ReferenceSetArgumentTrait;
21+
private array $values;
22+
23+
public function __construct(array $values)
24+
{
25+
$this->setValues($values);
26+
}
27+
28+
public function getValues(): array
29+
{
30+
return $this->values;
31+
}
32+
33+
public function setValues(array $values)
34+
{
35+
$this->values = $values;
36+
}
2237
}

src/Symfony/Component/DependencyInjection/Argument/ReferenceSetArgumentTrait.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,16 @@
1111

1212
namespace Symfony\Component\DependencyInjection\Argument;
1313

14+
trigger_deprecation('symfony/dependency-injection', '6.1', '"%s" is deprecated.', ReferenceSetArgumentTrait::class);
15+
1416
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
1517
use Symfony\Component\DependencyInjection\Reference;
1618

1719
/**
1820
* @author Titouan Galopin <galopintitouan@gmail.com>
1921
* @author Nicolas Grekas <p@tchwork.com>
22+
*
23+
* @deprecated since Symfony 6.1
2024
*/
2125
trait ReferenceSetArgumentTrait
2226
{

src/Symfony/Component/DependencyInjection/Argument/ServiceClosureArgument.php

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

1414
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
15-
use Symfony\Component\DependencyInjection\Reference;
1615

1716
/**
1817
* Represents a service wrapped in a memoizing closure.
@@ -23,9 +22,9 @@ class ServiceClosureArgument implements ArgumentInterface
2322
{
2423
private array $values;
2524

26-
public function __construct(Reference $reference)
25+
public function __construct(mixed $value)
2726
{
28-
$this->values = [$reference];
27+
$this->values = [$value];
2928
}
3029

3130
/**
@@ -41,8 +40,8 @@ public function getValues(): array
4140
*/
4241
public function setValues(array $values)
4342
{
44-
if ([0] !== array_keys($values) || !($values[0] instanceof Reference || null === $values[0])) {
45-
throw new InvalidArgumentException('A ServiceClosureArgument must hold one and only one Reference.');
43+
if ([0] !== array_keys($values)) {
44+
throw new InvalidArgumentException('A ServiceClosureArgument must hold one and only one value.');
4645
}
4746

4847
$this->values = $values;

src/Symfony/Component/DependencyInjection/Argument/ServiceLocator.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,11 @@ public function __construct(\Closure $factory, array $serviceMap, array $service
3737
*/
3838
public function get(string $id): mixed
3939
{
40-
return isset($this->serviceMap[$id]) ? ($this->factory)(...$this->serviceMap[$id]) : parent::get($id);
40+
return match (\count($this->serviceMap[$id] ?? [])) {
41+
0 => parent::get($id),
42+
1 => $this->serviceMap[$id][0],
43+
default => ($this->factory)(...$this->serviceMap[$id]),
44+
};
4145
}
4246

4347
/**

src/Symfony/Component/DependencyInjection/Argument/ServiceLocatorArgument.php

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,34 +11,38 @@
1111

1212
namespace Symfony\Component\DependencyInjection\Argument;
1313

14-
use Symfony\Component\DependencyInjection\Reference;
15-
1614
/**
1715
* Represents a closure acting as a service locator.
1816
*
1917
* @author Nicolas Grekas <p@tchwork.com>
2018
*/
2119
class ServiceLocatorArgument implements ArgumentInterface
2220
{
23-
use ReferenceSetArgumentTrait;
24-
21+
private array $values;
2522
private ?TaggedIteratorArgument $taggedIteratorArgument = null;
2623

27-
/**
28-
* @param Reference[]|TaggedIteratorArgument $values
29-
*/
3024
public function __construct(array|TaggedIteratorArgument $values = [])
3125
{
3226
if ($values instanceof TaggedIteratorArgument) {
3327
$this->taggedIteratorArgument = $values;
34-
$this->values = [];
35-
} else {
36-
$this->setValues($values);
28+
$values = [];
3729
}
30+
31+
$this->setValues($values);
3832
}
3933

4034
public function getTaggedIteratorArgument(): ?TaggedIteratorArgument
4135
{
4236
return $this->taggedIteratorArgument;
4337
}
38+
39+
public function getValues(): array
40+
{
41+
return $this->values;
42+
}
43+
44+
public function setValues(array $values)
45+
{
46+
$this->values = $values;
47+
}
4448
}

src/Symfony/Component/DependencyInjection/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ CHANGELOG
88
* Add `$exclude` to `tagged_iterator` and `tagged_locator` configurator
99
* Add an `env` function to the expression language provider
1010
* Add an `Autowire` attribute to tell a parameter how to be autowired
11+
* Allow using expressions as service factories
12+
* Deprecate `ReferenceSetArgumentTrait`
1113

1214
6.0
1315
---

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

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,15 +84,24 @@ protected function processValue(mixed $value, bool $isRoot = false)
8484
} elseif ($value instanceof ArgumentInterface) {
8585
$value->setValues($this->processValue($value->getValues()));
8686
} elseif ($value instanceof Expression && $this->processExpressions) {
87-
$this->getExpressionLanguage()->compile((string) $value, ['this' => 'container']);
87+
$this->getExpressionLanguage()->compile((string) $value, ['this' => 'container', 'args' => 'args']);
8888
} elseif ($value instanceof Definition) {
8989
$value->setArguments($this->processValue($value->getArguments()));
9090
$value->setProperties($this->processValue($value->getProperties()));
9191
$value->setMethodCalls($this->processValue($value->getMethodCalls()));
9292

9393
$changes = $value->getChanges();
9494
if (isset($changes['factory'])) {
95-
$value->setFactory($this->processValue($value->getFactory()));
95+
if (\is_string($factory = $value->getFactory()) && str_starts_with($factory, '@=')) {
96+
if (!class_exists(Expression::class)) {
97+
throw new LogicException('Expressions cannot be used in service factories without the ExpressionLanguage component. Try running "composer require symfony/expression-language".');
98+
}
99+
$factory = new Expression(substr($factory, 2));
100+
}
101+
if (($factory = $this->processValue($factory)) instanceof Expression) {
102+
$factory = '@='.$factory;
103+
}
104+
$value->setFactory($factory);
96105
}
97106
if (isset($changes['configurator'])) {
98107
$value->setConfigurator($this->processValue($value->getConfigurator()));
@@ -112,6 +121,10 @@ protected function getConstructor(Definition $definition, bool $required): ?\Ref
112121
}
113122

114123
if (\is_string($factory = $definition->getFactory())) {
124+
if (str_starts_with($factory, '@=')) {
125+
return new \ReflectionFunction(static function (...$args) {});
126+
}
127+
115128
if (!\function_exists($factory)) {
116129
throw new RuntimeException(sprintf('Invalid service "%s": function "%s" does not exist.', $this->currentId, $factory));
117130
}

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
use Symfony\Component\DependencyInjection\ContainerBuilder;
1717
use Symfony\Component\DependencyInjection\ContainerInterface;
1818
use Symfony\Component\DependencyInjection\Definition;
19+
use Symfony\Component\DependencyInjection\Exception\LogicException;
1920
use Symfony\Component\DependencyInjection\Reference;
21+
use Symfony\Component\ExpressionLanguage\Expression;
2022

2123
/**
2224
* Run this pass before passes that need to know more about the relation of
@@ -135,8 +137,16 @@ protected function processValue(mixed $value, bool $isRoot = false): mixed
135137

136138
$byFactory = $this->byFactory;
137139
$this->byFactory = true;
138-
$this->processValue($value->getFactory());
140+
if (\is_string($factory = $value->getFactory()) && str_starts_with($factory, '@=')) {
141+
if (!class_exists(Expression::class)) {
142+
throw new LogicException('Expressions cannot be used in service factories without the ExpressionLanguage component. Try running "composer require symfony/expression-language".');
143+
}
144+
145+
$factory = new Expression(substr($factory, 2));
146+
}
147+
$this->processValue($factory);
139148
$this->byFactory = $byFactory;
149+
140150
$this->processValue($value->getArguments());
141151

142152
$properties = $value->getProperties();

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

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -64,18 +64,17 @@ protected function processValue(mixed $value, bool $isRoot = false): mixed
6464
if ($v instanceof ServiceClosureArgument) {
6565
continue;
6666
}
67-
if (!$v instanceof Reference) {
68-
throw new InvalidArgumentException(sprintf('Invalid definition for service "%s": an array of references is expected as first argument when the "container.service_locator" tag is set, "%s" found for key "%s".', $this->currentId, get_debug_type($v), $k));
69-
}
7067

7168
if ($i === $k) {
72-
unset($services[$k]);
73-
74-
$k = (string) $v;
69+
if ($v instanceof Reference) {
70+
unset($services[$k]);
71+
$k = (string) $v;
72+
}
7573
++$i;
7674
} elseif (\is_int($k)) {
7775
$i = null;
7876
}
77+
7978
$services[$k] = new ServiceClosureArgument($v);
8079
}
8180
ksort($services);
@@ -97,20 +96,14 @@ protected function processValue(mixed $value, bool $isRoot = false): mixed
9796
return new Reference($id);
9897
}
9998

100-
/**
101-
* @param Reference[] $refMap
102-
*/
103-
public static function register(ContainerBuilder $container, array $refMap, string $callerId = null): Reference
99+
public static function register(ContainerBuilder $container, array $map, string $callerId = null): Reference
104100
{
105-
foreach ($refMap as $id => $ref) {
106-
if (!$ref instanceof Reference) {
107-
throw new InvalidArgumentException(sprintf('Invalid service locator definition: only services can be referenced, "%s" found for key "%s". Inject parameter values using constructors instead.', get_debug_type($ref), $id));
108-
}
109-
$refMap[$id] = new ServiceClosureArgument($ref);
101+
foreach ($map as $k => $v) {
102+
$map[$k] = new ServiceClosureArgument($v);
110103
}
111104

112105
$locator = (new Definition(ServiceLocator::class))
113-
->addArgument($refMap)
106+
->addArgument($map)
114107
->addTag('container.service_locator');
115108

116109
if (null !== $callerId && $container->hasDefinition($callerId)) {

src/Symfony/Component/DependencyInjection/ContainerBuilder.php

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1008,16 +1008,23 @@ private function createService(Definition $definition, array &$inlineServices, b
10081008
require_once $parameterBag->resolveValue($definition->getFile());
10091009
}
10101010

1011-
$arguments = $this->doResolveServices($parameterBag->unescapeValue($parameterBag->resolveValue($definition->getArguments())), $inlineServices, $isConstructorArgument);
1011+
$arguments = $definition->getArguments();
10121012

10131013
if (null !== $factory = $definition->getFactory()) {
10141014
if (\is_array($factory)) {
10151015
$factory = [$this->doResolveServices($parameterBag->resolveValue($factory[0]), $inlineServices, $isConstructorArgument), $factory[1]];
10161016
} elseif (!\is_string($factory)) {
10171017
throw new RuntimeException(sprintf('Cannot create service "%s" because of invalid factory.', $id));
1018+
} elseif (str_starts_with($factory, '@=')) {
1019+
$factory = function (ServiceLocator $arguments) use ($factory) {
1020+
return $this->getExpressionLanguage()->evaluate(substr($factory, 2), ['container' => $this, 'args' => $arguments]);
1021+
};
1022+
$arguments = [new ServiceLocatorArgument($arguments)];
10181023
}
10191024
}
10201025

1026+
$arguments = $this->doResolveServices($parameterBag->unescapeValue($parameterBag->resolveValue($arguments)), $inlineServices, $isConstructorArgument);
1027+
10211028
if (null !== $id && $definition->isShared() && isset($this->services[$id]) && ($tryProxy || !$definition->isLazy())) {
10221029
return $this->services[$id];
10231030
}
@@ -1149,10 +1156,8 @@ private function doResolveServices(mixed $value, array &$inlineServices = [], bo
11491156
} elseif ($value instanceof ServiceLocatorArgument) {
11501157
$refs = $types = [];
11511158
foreach ($value->getValues() as $k => $v) {
1152-
if ($v) {
1153-
$refs[$k] = [$v];
1154-
$types[$k] = $v instanceof TypedReference ? $v->getType() : '?';
1155-
}
1159+
$refs[$k] = [$v, null];
1160+
$types[$k] = $v instanceof TypedReference ? $v->getType() : '?';
11561161
}
11571162
$value = new ServiceLocator($this->resolveServices(...), $refs, $types);
11581163
} elseif ($value instanceof Reference) {
@@ -1583,8 +1588,8 @@ private function shareService(Definition $definition, mixed $service, ?string $i
15831588
private function getExpressionLanguage(): ExpressionLanguage
15841589
{
15851590
if (!isset($this->expressionLanguage)) {
1586-
if (!class_exists(\Symfony\Component\ExpressionLanguage\ExpressionLanguage::class)) {
1587-
throw new LogicException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed< F41A /span>.');
1591+
if (!class_exists(Expression::class)) {
1592+
throw new LogicException('Expressions cannot be used without the ExpressionLanguage component. Try running "composer require symfony/expression-language".');
15881593
}
15891594
$this->expressionLanguage = new ExpressionLanguage(null, $this->expressionLanguageProviders, null, $this->getEnv(...));
15901595
}

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

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -806,7 +806,7 @@ private function addService(string $id, Definition $definition): array
806806
$return[] = sprintf(str_starts_with($class, '%') ? '@return object A %1$s instance' : '@return \%s', ltrim($class, '\\'));
807807
} elseif ($definition->getFactory()) {
808808
$factory = $definition->getFactory();
809-
if (\is_string($factory)) {
809+
if (\is_string($factory) && !str_starts_with($factory, '@=')) {
810810
$return[] = sprintf('@return object An instance returned by %s()', $factory);
811811
} elseif (\is_array($factory) && (\is_string($factory[0]) || $factory[0] instanceof Definition || $factory[0] instanceof Reference)) {
812812
$class = $factory[0] instanceof Definition ? $factory[0]->getClass() : (string) $factory[0];
@@ -1159,6 +1159,13 @@ private function addNewInstance(Definition $definition, string $return = '', str
11591159
return $return.sprintf("[%s, '%s'](%s)", $class, $callable[1], $arguments ? implode(', ', $arguments) : '').$tail;
11601160
}
11611161

1162+
if (str_starts_with($callable, '@=')) {
1163+
return $return.sprintf('(($args = %s) ? (%s) : null)',
1164+
$this->dumpValue(new ServiceLocatorArgument($definition->getArguments())),
1165+
$this->getExpressionLanguage()->compile(substr($callable, 2), ['this' => 'container', 'args' => 'args'])
1166+
).$tail;
1167+
}
1168+
11621169
return $return.sprintf('%s(%s)', $this->dumpLiteralClass($this->dumpValue($callable)), $arguments ? implode(', ', $arguments) : '').$tail;
11631170
}
11641171

@@ -1740,7 +1747,7 @@ private function dumpValue(mixed $value, bool $interpolate = true): string
17401747
$code = sprintf('return %s;', $code);
17411748

17421749
$attribute = '';
1743-
if ($value) {
1750+
if ($value instanceof Reference) {
17441751
$attribute = 'name: '.$this->dumpValue((string) $value, $interpolate);
17451752

17461753
if ($this->container->hasDefinition($value) && ($class = $this->container->findDefinition($value)->getClass()) && $class !== (string) $value) {
@@ -1787,7 +1794,9 @@ private function dumpValue(mixed $value, bool $interpolate = true): string
17871794
$serviceMap = '';
17881795
$serviceTypes = '';
17891796
foreach ($value->getValues() as $k => $v) {
1790-
if (!$v) {
1797+
if (!$v instanceof Reference) {
1798+
$serviceMap .= sprintf("\n %s => [%s],", $this->export($k), $this->dumpValue($v));
1799+
$serviceTypes .= sprintf("\n %s => '?',", $this->export($k));
17911800
continue;
17921801
}
17931802
$id = (string) $v;

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,9 @@ private function convertParameters(array $parameters, string $type, \DOMElement
293293
} elseif ($value instanceof ServiceLocatorArgument) {
294294
$element->setAttribute('type', 'service_locator');
295295
$this->convertParameters($value->getValues(), $type, $element, 'key');
296+
} elseif ($value instanceof ServiceClosureArgument && !$value->getValues()[0] instanceof Reference) {
297+
$element->setAttribute('type', 'service_closure');
298+
$this->convertParameters($value->getValues(), $type, $element, 'key');
296299
} elseif ($value instanceof Reference || $value instanceof ServiceClosureArgument) {
297300
$element->setAttribute('type', 'service');
298301
if ($value instanceof ServiceClosureArgument) {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ private function dumpValue(mixed $value): mixed
245245
if ($value instanceof ServiceClosureArgument) {
246246
$value = $value->getValues()[0];
247247

248-
return new TaggedValue('service_closure', $this->getServiceCall((string) $value, $value));
248+
return new TaggedValue('service_closure', $this->dumpValue($value));
249249
}
250250
if ($value instanceof ArgumentInterface) {
251251
$tag = $value;

src/Symfony/Component/DependencyInjection/ExpressionLanguageProvider.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@ public function getFunctions(): array
6060

6161
return ($this->getEnv)($value);
6262
}),
63+
64+
new ExpressionFunction('arg', function ($arg) {
65+
return sprintf('$args?->get(%s)', $arg);
66+
}, function (array $variables, $value) {
67+
return $variables['args']?->get($value);
68+
}),
6369
];
6470
}
6571
}

0 commit comments

Comments
 (0)
0