Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 540b0c8

Browse files
[DependencyInjection] Add support for autowiring services as closures when using #[Autowire]
1 parent d6ddbfe commit 540b0c8

File tree

10 files changed

+233
-26
lines changed

10 files changed

+233
-26
lines changed

src/Symfony/Component/DependencyInjection/Attribute/Autowire.php

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Component\DependencyInjection\Attribute;
1313

14+
use Symfony\Component\DependencyInjection\Argument\ArgumentInterface;
1415
use Symfony\Component\DependencyInjection\Exception\LogicException;
1516
use Symfony\Component\DependencyInjection\Reference;
1617
use Symfony\Component\ExpressionLanguage\Expression;
@@ -23,19 +24,19 @@
2324
#[\Attribute(\Attribute::TARGET_PARAMETER)]
2425
class Autowire
2526
{
26-
public readonly string|array|Expression|Reference $value;
27+
public readonly string|array|Expression|Reference|ArgumentInterface $value;
2728

2829
/**
2930
* Use only ONE of the following.
3031
*
31-
* @param string|array|null $value Parameter value (ie "%kernel.project_dir%/some/path")
32-
* @param string|null $service Service ID (ie "some.service")
33-
* @param string|null $expression Expression (ie 'service("some.service").someMethod()')
34-
* @param string|null $env Environment variable name (ie 'SOME_ENV_VARIABLE')
35-
* @param string|null $param Parameter name (ie 'some.parameter.name')
32+
* @param string|array|ArgumentInterface|null $value Value to inject (ie "%kernel.project_dir%/some/path")
33+
* @param string|null $service Service ID (ie "some.service")
34+
* @param string|null $expression Expression (ie 'service("some.service").someMethod()')
35+
* @param string|null $env Environment variable name (ie 'SOME_ENV_VARIABLE')
36+
* @param string|null $param Parameter name (ie 'some.parameter.name')
3637
*/
3738
public function __construct(
38-
string|array $value = null,
39+
string|array|ArgumentInterface $value = null,
3940
string $service = null,
4041
string $expression = null,
4142
string $env = null,
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\Attribute;
13+
14+
use Symfony\Component\DependencyInjection\Exception\LogicException;
15+
use Symfony\Component\DependencyInjection\Reference;
16+
17+
/**
18+
* Attribute to tell which callable to give to an argument of type Closure.
19+
*/
20+
#[\Attribute(\Attribute::TARGET_PARAMETER)]
21+
class AutowireCallable extends Autowire
22+
{
23+
public function __construct(
24+
string|array $callable = null,
25+
string $service = null,
26+
string $method = null,
27+
public bool $lazy = false,
28+
) {
29+
if (!(null !== $callable xor null !== $service)) {
30+
throw new LogicException('#[AutowireCallable] attribute must declare exactly one of $callable or $service.');
31+
}
32+
if (!(null !== $callable xor null !== $method)) {
33+
throw new LogicException('#[AutowireCallable] attribute must declare one of $callable or $method.');
34+
}
35+
36+
parent::__construct($callable ?? [new Reference($service), $method ?? '__invoke']);
37+
}
38+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\Attribute;
13+
14+
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
15+
use Symfony\Component\DependencyInjection\Reference;
16+
17+
/**
18+
* Attribute to wrap a service in a closure returns it.
19+
*/
20+
#[\Attribute(\Attribute::TARGET_PARAMETER)]
21+
class AutowireServiceClosure extends Autowire
22+
{
23+
public function __construct(string $service)
24+
{
25+
parent::__construct(new ServiceClosureArgument(new Reference($service)));
26+
}
27+
}

src/Symfony/Component/DependencyInjection/Attribute/TaggedIterator.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@
1111

1212
namespace Symfony\Component\DependencyInjection\Attribute;
1313

14+
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
15+
1416
#[\Attribute(\Attribute::TARGET_PARAMETER)]
15-
class TaggedIterator
17+
class TaggedIterator extends Autowire
1618
{
1719
public function __construct(
1820
public string $tag,
@@ -22,5 +24,6 @@ public function __construct(
2224
public string|array $exclude = [],
2325
public bool $excludeSelf = true,
2426
) {
27+
parent::__construct(new TaggedIteratorArgument($tag, $indexAttribute, $defaultIndexMethod, false, $defaultPriorityMethod, (array) $exclude, $excludeSelf));
2528
}
2629
}

src/Symfony/Component/DependencyInjection/Attribute/TaggedLocator.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,11 @@
1111

1212
namespace Symfony\Component\DependencyInjection\Attribute;
1313

14+
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
15+
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
16+
1417
#[\Attribute(\Attribute::TARGET_PARAMETER)]
15-
class TaggedLocator
18+
class TaggedLocator extends Autowire
1619
{
1720
public function __construct(
1821
public string $tag,
@@ -22,5 +25,6 @@ public function __construct(
2225
public string|array $exclude = [],
2326
public bool $excludeSelf = true,
2427
) {
28+
parent::__construct(new ServiceLocatorArgument(new TaggedIteratorArgument($tag, $indexAttribute, $defaultIndexMethod, true, $defaultPriorityMethod, (array) $exclude, $excludeSelf)));
2529
}
2630
}

src/Symfony/Component/DependencyInjection/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ CHANGELOG
1515
* Allow to trim XML service parameters value by using `trim="true"` attribute
1616
* Allow extending the `Autowire` attribute
1717
* Add `#[Exclude]` to skip autoregistering a class
18+
* Add support for autowiring services as closures when using `#[Autowire]`
1819

1920
6.2
2021
---

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

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@
1212
namespace Symfony\Component\DependencyInjection\Compiler;
1313

1414
use Symfony\Component\Config\Resource\ClassExistenceResource;
15+
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
1516
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
1617
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
1718
use Symfony\Component\DependencyInjection\Attribute\Autowire;
19+
use Symfony\Component\DependencyInjection\Attribute\AutowireCallable;
1820
use Symfony\Component\DependencyInjection\Attribute\MapDecorated;
1921
use Symfony\Component\DependencyInjection\Attribute\TaggedIterator;
2022
use Symfony\Component\DependencyInjection\Attribute\TaggedLocator;
@@ -86,14 +88,6 @@ protected function processValue(mixed $value, bool $isRoot = false): mixed
8688
return $this->processValue($this->container->getParameterBag()->resolveValue($value->value));
8789
}
8890

89-
if ($value instanceof TaggedIterator) {
90-
return new TaggedIteratorArgument($value->tag, $value->indexAttribute, $value->defaultIndexMethod, false, $value->defaultPriorityMethod, (array) $value->exclude, $value->excludeSelf);
91-
}
92-
93-
if ($value instanceof TaggedLocator) {
94-
return new ServiceLocatorArgument(new TaggedIteratorArgument($value->tag, $value->indexAttribute, $value->defaultIndexMethod, true, $value->defaultPriorityMethod, (array) $value->exclude, $value->excludeSelf));
95-
}
96-
9791
if ($value instanceof MapDecorated) {
9892
$definition = $this->container->getDefinition($this->currentId);
9993

@@ -191,8 +185,6 @@ private function processAttribute(object $attribute, bool $isOptional = false):
191185
return new Reference($attribute->value, ContainerInterface::NULL_ON_INVALID_REFERENCE);
192186
}
193187
// no break
194-
case $attribute instanceof TaggedIterator:
195-
case $attribute instanceof TaggedLocator:
196188
case $attribute instanceof MapDecorated:
197189
return $this->processValue($attribute);
198190
}
@@ -291,17 +283,32 @@ private function autowireMethod(\ReflectionFunctionAbstract $reflectionMethod, a
291283
continue;
292284
}
293285

294-
if ($checkAttributes) {
295-
foreach ([TaggedIterator::class, TaggedLocator::class, Autowire::class, MapDecorated::class] as $attributeClass) {
296-
foreach ($parameter->getAttributes($attributeClass, Autowire::class === $attributeClass ? \ReflectionAttribute::IS_INSTANCEOF : 0) as $attribute) {
297-
$arguments[$index] = $this->processAttribute($attribute->newInstance(), $parameter->allowsNull());
286+
$type = ProxyHelper::exportType($parameter, true);
298287

299-
continue 3;
288+
if ($checkAttributes) {
289+
foreach ($parameter->getAttributes(Autowire::class, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
290+
$attribute = $attribute->newInstance();
291+
$value = $this->processAttribute($attribute, $parameter->allowsNull());
292+
293+
if ($attribute instanceof AutowireCallable || 'Closure' === $type && \is_array($value)) {
294+
$value = (new Definition('Closure'))
295+
->setFactory(['Closure', 'fromCallable'])
296+
->setArguments([$value + [1 => '__invoke']])
297+
->setLazy($attribute instanceof AutowireCallable && $attribute->lazy);
300298
}
299+
$arguments[$index] = $value;
300+
301+
continue 2;
302+
}
303+
304+
foreach ($parameter->getAttributes(MapDecorated::class) as $attribute) {
305+
$arguments[$index] = $this->processAttribute($attribute->newInstance(), $parameter->allowsNull());
306+
307+
continue 2;
301308
}
302309
}
303310

304-
if (!$type = ProxyHelper::exportType($parameter, true)) {
311+
if (!$type) {
305312
if (isset($arguments[$index])) {
306313
continue;
307314
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -462,7 +462,7 @@ public static function getSubscribedServices(): array
462462
'autowired' => new ServiceClosureArgument(new Reference('service.id')),
463463
'autowired.nullable' => new ServiceClosureArgument(new Reference('service.id', ContainerInterface::NULL_ON_INVALID_REFERENCE)),
464464
'autowired.parameter' => new ServiceClosureArgument('foobar'),
465-
'map.decorated' => new ServiceClosureArgument(new Reference('.service_locator.LnJLtj2.inner', ContainerInterface::NULL_ON_INVALID_REFERENCE)),
465+
'map.decorated' => new ServiceClosureArgument(new Reference('.service_locator.EeZIdVM.inner', ContainerInterface::NULL_ON_INVALID_REFERENCE)),
466466
'target' => new ServiceClosureArgument(new TypedReference('stdClass', 'stdClass', ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, 'someTarget', [new Target('someTarget')])),
467467
];
468468
$this->assertEquals($expected, $container->getDefinition((string) $locator->getFactory()[0])->getArgument(0));

src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
2222
use Symfony\Component\DependencyInjection\Argument\ServiceLocator as ArgumentServiceLocator;
2323
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
24+
use Symfony\Component\DependencyInjection\Attribute\Autowire;
25+
use Symfony\Component\DependencyInjection\Attribute\AutowireCallable;
26+
use Symfony\Component\DependencyInjection\Attribute\AutowireServiceClosure;
2427
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
2528
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
2629
use Symfony\Component\DependencyInjection\Container;
@@ -1651,6 +1654,38 @@ public function testClosure()
16511654

16521655
$this->assertStringEqualsFile(self::$fixturesPath.'/php/closure.php', $dumper->dump());
16531656
}
1657+
1658+
public function testAutowireClosure()
1659+
{
1660+
$container = new ContainerBuilder();
1661+
$container->register('foo', Foo::class)
1662+
->setPublic('true');
1663+
$container->register('baz', \Closure::class)
1664+
->setFactory(['Closure', 'fromCallable'])
1665+
->setArguments(['var_dump'])
1666+
->setPublic('true');
1667+
$container->register('bar', LazyConsumer::class)
1668+
->setPublic('true')
1669+
->setAutowired(true);
1670+
$container->compile();
1671+
$dumper = new PhpDumper($container);
1672+
1673+
$this->assertStringEqualsFile(self::$fixturesPath.'/php/autowire_closure.php', $dumper->dump(['class' => 'Symfony_DI_PhpDumper_Test_Autowire_Closure']));
1674+
1675+
require self::$fixturesPath.'/php/autowire_closure.php';
1676+
1677+
$container = new \Symfony_DI_PhpDumper_Test_Autowire_Closure();
1678+
1679+
$this->assertInstanceOf(Foo::class, $container->get('foo'));
1680+
$this->assertInstanceOf(LazyConsumer::class, $bar = $container->get('bar'));
1681+
$this->assertInstanceOf(\Closure::class, $bar->foo);
1682+
$this->assertInstanceOf(\Closure::class, $bar->baz);
1683+
$this->assertInstanceOf(\Closure::class, $bar->buz);
1684+
$this->assertSame($container->get('foo'), ($bar->foo)());
1685+
$this->assertSame($container->get('baz'), $bar->baz);
1686+
$this->assertInstanceOf(Foo::class, $fooClone = ($bar->buz)());
1687+
$this->assertNotSame($container->get('foo'), $fooClone);
1688+
}
16541689
}
16551690

16561691
class Rot13EnvVarProcessor implements EnvVarProcessorInterface
@@ -1676,3 +1711,16 @@ public function __construct(\stdClass $a, \stdClass $b)
16761711
$this->bClone = clone $b;
16771712
}
16781713
}
1714+
1715+
class LazyConsumer
1716+
{
1717+
public function __construct(
1718+
#[AutowireServiceClosure('foo')]
1719+
public \Closure $foo,
1720+
#[Autowire(service: 'baz')]
1721+
public \Closure $baz,
1722+
#[AutowireCallable(service: 'foo', method: 'cloneFoo')]
1723+
public \Closure $buz,
1724+
) {
1725+
}
1726+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php
2+
3+
use Symfony\Component\DependencyInjection\Argument\RewindableGenerator;
4+
use Symfony\Component\DependencyInjection\ContainerInterface;
5+
use Symfony\Component\DependencyInjection\Container;
6+
use Symfony\Component\DependencyInjection\Exception\LogicException;
7+
use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException;
8+
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
9+
use Symfony\Component\DependencyInjection\ParameterBag\FrozenParameterBag;
10+
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
11+
12+
/**
13+
* @internal This class has been auto-generated by the Symfony Dependency Injection Component.
14+
*/
15+
class Symfony_DI_PhpDumper_Test_Autowire_Closure extends Container
16+
{
17+
protected $parameters = [];
18+
protected readonly \WeakReference $ref;
19+
20+
public function __construct()
21+
{
22+
$this->ref = \WeakReference::create($this);
23+
$this->services = $this->privates = [];
24+
$this->methodMap = [
25+
'bar' => 'getBarService',
26+
'baz' => 'getBazService',
27+
'foo' => 'getFooService',
28+
];
29+
30+
$this->aliases = [];
31+
}
32+
33+
public function compile(): void
34+
{
35+
throw new LogicException('You cannot compile a dumped container that was already compiled.');
36+
}
37+
38+
public function isCompiled(): bool
39+
{
40+
return true;
41+
}
42+
43+
/**
44+
* Gets the public 'bar' shared autowired service.
45+
*
46+
* @return \Symfony\Component\DependencyInjection\Tests\Dumper\LazyConsumer
47+
*/
48+
protected static function getBarService($container)
49+
{
50+
$containerRef = $container->ref;
51+
52+
return $container->services['bar'] = new \Symfony\Component\DependencyInjection\Tests\Dumper\LazyConsumer(#[\Closure(name: 'foo', class: 'Symfony\\Component\\DependencyInjection\\Tests\\Compiler\\Foo')] function () use ($containerRef) {
53+
$container = $containerRef->get();
54+
55+
return ($container->services['foo'] ??= new \Symfony\Component\DependencyInjection\Tests\Compiler\Foo());
56+
}, ($container->services['baz'] ?? self::getBazService($container)), ($container->services['foo'] ??= new \Symfony\Component\DependencyInjection\Tests\Compiler\Foo())->cloneFoo(...));
57+
}
58+
59+
/**
60+
* Gets the public 'baz' shared service.
61+
*
62+
* @return \Closure
63+
*/
64+
protected static function getBazService($container)
65+
{
66+
return $container->services['baz'] = \var_dump(...);
67+
}
68+
69+
/**
70+
* Gets the public 'foo' shared service.
71+
*
72+
* @return \Symfony\Component\DependencyInjection\Tests\Compiler\Foo
73+
*/
74+
protected static function getFooService($container)
75+
{
76+
return $container->services['foo'] = new \Symfony\Component\DependencyInjection\Tests\Compiler\Foo();
77+
}
78+
}

0 commit comments

Comments
 (0)