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)) {

0 commit comments

Comments
 (0)