Skip to content

Commit 358a21a

Browse files
[DependencyInjection] Allow using expressions as service factories
1 parent 387dca3 commit 358a21a

19 files changed

+211
-26
lines changed

UPGRADE-6.1.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ Console
1212

1313
* Deprecate `Command::$defaultName` and `Command::$defaultDescription`, use the `AsCommand` attribute instead
1414

15+
DependencyInjection
16+
-------------------
17+
18+
* Deprecate `ReferenceSetArgumentTrait`
19+
1520
HttpKernel
1621
----------
1722

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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\Definition;
15+
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
16+
17+
/**
18+
* @author Titouan Galopin <galopintitouan@gmail.com>
19+
* @author Nicolas Grekas <p@tchwork.com>
20+
*
21+
* @internal
22+
*/
23+
trait InlineableSetArgumentTrait
24+
{
25+
private array $values;
26+
27+
public function __construct(array $values)
28+
{
29+
$this->setValues($values);
30+
}
31+
32+
public function getValues(): array
33+
{
34+
return $this->values;
35+
}
36+
37+
public function setValues(array $values)
38+
{
39+
foreach ($values as $k => $v) {
40+
if ($v instanceof Definition) {
41+
throw new InvalidArgumentException(sprintf('A "%s" must not hold inline service definitions, "%s" given.', __CLASS__));
42+
}
43+
}
44+
45+
$this->values = $values;
46+
}
47+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,5 @@
1818
*/
1919
class IteratorArgument implements ArgumentInterface
2020
{
21-
use ReferenceSetArgumentTrait;
21+
use InlineableSetArgumentTrait;
2222
}

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/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: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,17 @@
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;
21+
use InlineableSetArgumentTrait;
2422

2523
private ?TaggedIteratorArgument $taggedIteratorArgument = null;
2624

27-
/**
28-
* @param Reference[]|TaggedIteratorArgument $values
29-
*/
3025
public function __construct(array|TaggedIteratorArgument $values = [])
3126
{
3227
if ($values instanceof TaggedIteratorArgument) {

src/Symfony/Component/DependencyInjection/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ CHANGELOG
77
* Add `$exclude` to `TaggedIterator` and `TaggedLocator` attributes
88
* Add `$exclude` to `tagged_iterator` and `tagged_locator` configurator
99
* Add an `env` function to the expression language provider
10+
* Allow using expressions as service factories
11+
* Deprecate `ReferenceSetArgumentTrait`
1012

1113
6.0
1214
---

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 () {});
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/ContainerBuilder.php

Lines changed: 12 additions & 5 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,8 +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];
1159+
if (null !== $v) {
1160+
$refs[$k] = [$v, null];
11541161
$types[$k] = $v instanceof TypedReference ? $v->getType() : '?';
11551162
}
11561163
}
@@ -1583,8 +1590,8 @@ private function shareService(Definition $definition, mixed $service, ?string $i
15831590
private function getExpressionLanguage(): ExpressionLanguage
15841591
{
15851592
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.');
1593+
if (!class_exists(Expression::class)) {
1594+
throw new LogicException('Expressions cannot be used without the ExpressionLanguage component. Try running "composer require symfony/expression-language".');
15881595
}
15891596
$this->expressionLanguage = new ExpressionLanguage(null, $this->expressionLanguageProviders, null, \Closure::fromCallable([$this, 'getEnv']));
15901597
}

0 commit comments

Comments
 (0)