8000 [DependencyInjection] Add argument type `closure` to help passing clo… · symfony/symfony@0678f00 · GitHub
[go: up one dir, main page]

Skip to content

Commit 0678f00

Browse files
[DependencyInjection] Add argument type closure to help passing closures to services
1 parent 9a14dd0 commit 0678f00

File tree

15 files changed

+165
-4
lines changed

15 files changed

+165
-4
lines changed

src/Symfony/Component/DependencyInjection/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ CHANGELOG
99
* Add an `env` function to the expression language provider
1010
* Add an `Autowire` attribute to tell a parameter how to be autowired
1111
* Allow using expressions as service factories
12+
* Add argument type `closure` to help passing closures to services
1213
* Deprecate `ReferenceSetArgumentTrait`
1314

1415
6.0

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1132,6 +1132,15 @@ private function addNewInstance(Definition $definition, string $return = '', str
11321132
if (null !== $definition->getFactory()) {
11331133
$callable = $definition->getFactory();
11341134

1135+
if (['Closure', 'fromCallable'] === $callable && [0] === array_keys($definition->getArguments())) {
1136+
$callable = $definition->getArgument(0);
1137+
$arguments = ['...'];
1138+
1139+
if ($callable instanceof Reference || $callable instanceof Definition) {
1140+
$callable = [$callable, '__invoke'];
1141+
}
1142+
}
1143+
11351144
if (\is_array($callable)) {
11361145
if (!preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $callable[1])) {
11371146
throw new RuntimeException(sprintf('Cannot dump definition because of invalid factory method (%s).', $callable[1] ?: 'n/a'));
@@ -1159,7 +1168,7 @@ private function addNewInstance(Definition $definition, string $return = '', str
11591168
return $return.sprintf("[%s, '%s'](%s)", $class, $callable[1], $arguments ? implode(', ', $arguments) : '').$tail;
11601169
}
11611170

1162-
if (str_starts_with($callable, '@=')) {
1171+
if (\is_string($callable) && str_starts_with($callable, '@=')) {
11631172
return $return.sprintf('(($args = %s) ? (%s) : null)',
11641173
$this->dumpValue(new ServiceLocatorArgument($definition->getArguments())),
11651174
$this->getExpressionLanguage()->compile(substr($callable, 2), ['this' => 'container', 'args' => 'args'])

src/Symfony/Component/DependencyInjection/Loader/Configurator/ContainerConfigurator.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,3 +183,13 @@ function service_closure(string $serviceId): ClosureReferenceConfigurator
183183
{
184184
return new ClosureReferenceConfigurator($serviceId);
185185
}
186+
187+
/**
188+
* Creates a closure.
189+
*/
190+
function closure(string|array|ReferenceConfigurator|Expression $callable): InlineServiceConfigurator
191+
{
192+
return (new InlineServiceConfigurator(new Definition('Closure')))
193+
->factory(['Closure', 'fromCallable'])
194+
->args([$callable]);
195+
}

src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -495,7 +495,7 @@ private function getAr F438 gumentsAsPhp(\DOMElement $node, string $name, string $file
495495
$invalidBehavior = ContainerInterface::NULL_ON_INVALID_REFERENCE;
496496
}
497497

498-
switch ($arg->getAttribute('type')) {
498+
switch ($type = $arg->getAttribute('type')) {
499499
case 'service':
500500
if ('' === $arg->getAttribute('id')) {
501501
throw new InvalidArgumentException(sprintf('Tag "<%s>" with type="service" has no or empty "id" attribute in "%s".', $name, $file));
@@ -517,13 +517,19 @@ private function getArgumentsAsPhp(\DOMElement $node, string $name, string $file
517517
$arg = $this->getArgumentsAsPhp($arg, $name, $file);
518518
$arguments[$key] = new IteratorArgument($arg);
519519
break;
520+
case 'closure':
520521
case 'service_closure':
521522
if ('' !== $arg->getAttribute('id')) {
522523
$arg = new Reference($arg->getAttribute('id'), $invalidBehavior);
523524
} else {
524525
$arg = $this->getArgumentsAsPhp($arg, $name, $file);
525526
}
526-
$arguments[$key] = new ServiceClosureArgument($arg);
527+
$arguments[$key] = match ($type) {
528+
'service_closure' => new ServiceClosureArgument($arg),
529+
'closure' => (new Definition('Closure'))
530+
->setFactory(['Closure', 'fromCallable'])
531+
->addArgument($arg),
532+
};
527533
break;
528534
case 'service_locator':
529535
$arg = $this->getArgumentsAsPhp($arg, $name, $file);
@@ -532,7 +538,6 @@ private function getArgumentsAsPhp(\DOMElement $node, string $name, string $file
532538
case 'tagged':
533539
case 'tagged_iterator':
534540
case 'tagged_locator':
535-
$type = $arg->getAttribute('type');
536541
$forLocator = 'tagged_locator' === $type;
537542

538543
if (!$arg->getAttribute('tag')) {

src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -804,6 +804,14 @@ private function resolveServices(mixed $value, string $file, bool $isParameter =
804804
{
805805
if ($value instanceof TaggedValue) {
806806
$argument = $value->getValue();
807+
808+
if ('closure' === $value->getTag()) {
809+
$argument = $this->resolveServices($argument, $file, $isParameter);
810+
811+
return (new Definition('Closure'))
812+
->setFactory(['Closure', 'fromCallable'])
813+
->addArgument($argument);
814+
}
807815
if ('iterator' === $value->getTag()) {
808816
if (!\is_array($argument)) {
809817
throw new InvalidArgumentException(sprintf('"!iterator" tag only accepts sequences in "%s".', $file));

src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,7 @@
323323
<xsd:enumeration value="constant" />
324324
<xsd:enumeration value="binary" />
325325
<xsd:enumeration value="iterator" />
326+
<xsd:enumeration value="closure" />
326327
<xsd:enumeration value="service_closure" />
327328
<xsd:enumeration value="service_locator" />
328329
<!-- "tagged" is an alias of "tagged_iterator", using "tagged_iterator" is preferred. -->

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1534,6 +1534,21 @@ public function testExpressionInFactory()
15341534

15351535
$this->assertSame(247, $container->get('foo')->bar);
15361536
}
1537+
1538+
public function testClosure()
1539+
{
1540+
$container = new ContainerBuilder();
1541+
$container->register('closure', 'Closure')
1542+
->setPublic('true')
1543+
->setFactory(['Closure', 'fromCallable'])
1544+
->setArguments([new Reference('bar')]);
1545+
$container->register('bar', 'stdClass');
1546+
$container->compile();
1547+
$dumper = new PhpDumper($container);
1548+
1549+
file_put_contents(self::$fixturesPath.'/php/closure.php', $dumper->dump());
1550+
$this->assertStringEqualsFile(self::$fixturesPath.'/php/closure.php', $dumper->dump());
1551+
}
15371552
}
15381553

15391554
class Rot13EnvVarProcessor implements EnvVarProcessorInterface
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
2+
services:
3+
service_container:
4+
class: Symfony\Component\DependencyInjection\ContainerInterface
5+
public: true
6+
synthetic: true
7+
closure_property:
8+
class: stdClass
9+
public: true
10+
properties: { foo: !service { class: Closure, arguments: [!service { class: stdClass }], factory: [Closure, fromCallable] } }
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
4+
5+
return new class() {
6+
public function __invoke(ContainerConfigurator $c)
7+
{
8+
$c->services()
9+
->set('closure_property', 'stdClass')
10+
->public()
11+
->property('foo', closure(service('bar')))
12+
->set('bar', 'stdClass');
13+
}
14+
};
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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\InvalidArgumentException;
7+
use Symfony\Component\DependencyInjection\Exception\LogicException;
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 ProjectServiceContainer extends Container
16+
{
17+
protected $parameters = [];
18+
19+
public function __construct()
20+
{
21+
$this->services = $this->privates = [];
22+
$this->methodMap = [
23+
'closure' => 'getClosureService',
24+
];
25+
26+
$this->aliases = [];
27+
}
28+
29+
public function compile(): void
30+
{
31+
throw new LogicException('You cannot compile a dumped container that was already compiled.');
32+
}
33+
34+
public function isCompiled(): bool
35+
{
36+
return true;
37+
}
38+
39+
public function getRemovedIds(): array
40+
{
41+
return [
42+
'bar' => true,
43+
];
44+
}
45+
46+
/**
47+
* Gets the public 'closure' shared service.
48+
*
49+
* @return \Closure
50+
*/
51+
protected function getClosureService()
52+
{
53+
return $this->services['closure'] = (new \stdClass())->__invoke(...);
54+
}
55+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd">
3+
<services>
4+
<service id="closure_property" class="stdClass">
5+
<property key="foo" type="closure" id="bar" />
6+
</service>
7+
</services>
8+
</container>
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
services:
2+
closure_property:
3+
class: stdClass
4+
properties: { foo: !closure '@bar' }

src/Symfony/Component/DependencyInjection/Tests/Loader/PhpFileLoaderTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ public function provideConfig()
9898
yield ['remove'];
9999
yield ['config_builder'];
100100
yield ['expression_factory'];
101+
yield ['closure'];
101102
}
102103

103104
public function testAutoConfigureAndChildDefinition()

src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1113,4 +1113,14 @@ public function testWhenEnv()
11131113

11141114
$this->assertSame(['foo' => 234, 'bar' => 345], $container->getParameterBag()->all());
11151115
}
1116+
1117+
public function testClosure()
1118+
{
1119+
$container = new ContainerBuilder();
1120+
$loader = new XmlFileLoader($container, new FileLocator(self::$fixturesPath.'/xml'));
1121+
$loader->load('closure.xml');
1122+
1123+
$definition = $container->getDefinition('closure_property')->getProperties()['foo'];
1124+
$this->assertEquals((new Definition('Closure'))->setFactory(['Closure', 'fromCallable'])->addArgument(new Reference('bar')), $definition);
1125+
}
11161126
}

src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1088,4 +1088,14 @@ public function testWhenEnv()
10881088

10891089
$this->assertSame(['foo' => 234, 'bar' => 345], $container->getParameterBag()->all());
10901090
}
1091+
1092+
public function testClosure()
1093+
{
1094+
$container = new ContainerBuilder();
1095+
$loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml'));
1096+
$loader->load('closure.yml');
1097+
1098+
$definition = $container->getDefinition('closure_property')->getProperties()['foo'];
1099+
$this->assertEquals((new Definition('Closure'))->setFactory(['Closure', 'fromCallable'])->addArgument(new Reference('bar')), $definition);
1100+
}
10911101
}

0 commit comments

Comments
 (0)
0