diff --git a/src/Symfony/Component/DependencyInjection/CHANGELOG.md b/src/Symfony/Component/DependencyInjection/CHANGELOG.md
index f798f11897d5b..6976b55814bd1 100644
--- a/src/Symfony/Component/DependencyInjection/CHANGELOG.md
+++ b/src/Symfony/Component/DependencyInjection/CHANGELOG.md
@@ -9,6 +9,7 @@ CHANGELOG
* Add an `env` function to the expression language provider
* Add an `Autowire` attribute to tell a parameter how to be autowired
* Allow using expressions as service factories
+ * Add argument type `closure` to help passing closures to services
* Deprecate `ReferenceSetArgumentTrait`
6.0
diff --git a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php
index d25f929b32b1a..204c0f3113f07 100644
--- a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php
+++ b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php
@@ -1132,6 +1132,15 @@ private function addNewInstance(Definition $definition, string $return = '', str
if (null !== $definition->getFactory()) {
$callable = $definition->getFactory();
+ if (['Closure', 'fromCallable'] === $callable && [0] === array_keys($definition->getArguments())) {
+ $callable = $definition->getArgument(0);
+ $arguments = ['...'];
+
+ if ($callable instanceof Reference || $callable instanceof Definition) {
+ $callable = [$callable, '__invoke'];
+ }
+ }
+
if (\is_array($callable)) {
if (!preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $callable[1])) {
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
return $return.sprintf("[%s, '%s'](%s)", $class, $callable[1], $arguments ? implode(', ', $arguments) : '').$tail;
}
- if (str_starts_with($callable, '@=')) {
+ if (\is_string($callable) && str_starts_with($callable, '@=')) {
return $return.sprintf('(($args = %s) ? (%s) : null)',
$this->dumpValue(new ServiceLocatorArgument($definition->getArguments())),
$this->getExpressionLanguage()->compile(substr($callable, 2), ['this' => 'container', 'args' => 'args'])
diff --git a/src/Symfony/Component/DependencyInjection/Loader/Configurator/ContainerConfigurator.php b/src/Symfony/Component/DependencyInjection/Loader/Configurator/ContainerConfigurator.php
index 03c09773c27cb..3065a94b3bff0 100644
--- a/src/Symfony/Component/DependencyInjection/Loader/Configurator/ContainerConfigurator.php
+++ b/src/Symfony/Component/DependencyInjection/Loader/Configurator/ContainerConfigurator.php
@@ -183,3 +183,13 @@ function service_closure(string $serviceId): ClosureReferenceConfigurator
{
return new ClosureReferenceConfigurator($serviceId);
}
+
+/**
+ * Creates a closure.
+ */
+function closure(string|array|ReferenceConfigurator|Expression $callable): InlineServiceConfigurator
+{
+ return (new InlineServiceConfigurator(new Definition('Closure')))
+ ->factory(['Closure', 'fromCallable'])
+ ->args([$callable]);
+}
diff --git a/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php
index 7bd397aea11bf..f05c72e0e1504 100644
--- a/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php
+++ b/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php
@@ -495,7 +495,7 @@ private function getArgumentsAsPhp(\DOMElement $node, string $name, string $file
$invalidBehavior = ContainerInterface::NULL_ON_INVALID_REFERENCE;
}
- switch ($arg->getAttribute('type')) {
+ switch ($type = $arg->getAttribute('type')) {
case 'service':
if ('' === $arg->getAttribute('id')) {
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
$arg = $this->getArgumentsAsPhp($arg, $name, $file);
$arguments[$key] = new IteratorArgument($arg);
break;
+ case 'closure':
case 'service_closure':
if ('' !== $arg->getAttribute('id')) {
$arg = new Reference($arg->getAttribute('id'), $invalidBehavior);
} else {
$arg = $this->getArgumentsAsPhp($arg, $name, $file);
}
- $arguments[$key] = new ServiceClosureArgument($arg);
+ $arguments[$key] = match ($type) {
+ 'service_closure' => new ServiceClosureArgument($arg),
+ 'closure' => (new Definition('Closure'))
+ ->setFactory(['Closure', 'fromCallable'])
+ ->addArgument($arg),
+ };
break;
case 'service_locator':
$arg = $this->getArgumentsAsPhp($arg, $name, $file);
@@ -532,7 +538,6 @@ private function getArgumentsAsPhp(\DOMElement $node, string $name, string $file
case 'tagged':
case 'tagged_iterator':
case 'tagged_locator':
- $type = $arg->getAttribute('type');
$forLocator = 'tagged_locator' === $type;
if (!$arg->getAttribute('tag')) {
diff --git a/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php
index 12069c56529b9..9b480a3810580 100644
--- a/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php
+++ b/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php
@@ -804,6 +804,14 @@ private function resolveServices(mixed $value, string $file, bool $isParameter =
{
if ($value instanceof TaggedValue) {
$argument = $value->getValue();
+
+ if ('closure' === $value->getTag()) {
+ $argument = $this->resolveServices($argument, $file, $isParameter);
+
+ return (new Definition('Closure'))
+ ->setFactory(['Closure', 'fromCallable'])
+ ->addArgument($argument);
+ }
if ('iterator' === $value->getTag()) {
if (!\is_array($argument)) {
throw new InvalidArgumentException(sprintf('"!iterator" tag only accepts sequences in "%s".', $file));
diff --git a/src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd b/src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd
index 6deab5202b890..ec642212c2a92 100644
--- a/src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd
+++ b/src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd
@@ -323,6 +323,7 @@
+
diff --git a/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php b/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php
index 58ceeb5f7f8f8..aaef9930692db 100644
--- a/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php
+++ b/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php
@@ -1534,6 +1534,20 @@ public function testExpressionInFactory()
$this->assertSame(247, $container->get('foo')->bar);
}
+
+ public function testClosure()
+ {
+ $container = new ContainerBuilder();
+ $container->register('closure', 'Closure')
+ ->setPublic('true')
+ ->setFactory(['Closure', 'fromCallable'])
+ ->setArguments([new Reference('bar')]);
+ $container->register('bar', 'stdClass');
+ $container->compile();
+ $dumper = new PhpDumper($container);
+
+ $this->assertStringEqualsFile(self::$fixturesPath.'/php/closure.php', $dumper->dump());
+ }
}
class Rot13EnvVarProcessor implements EnvVarProcessorInterface
diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/closure.expected.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/closure.expected.yml
new file mode 100644
index 0000000000000..2fcce6c6d7751
--- /dev/null
+++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/closure.expected.yml
@@ -0,0 +1,10 @@
+
+services:
+ service_container:
+ class: Symfony\Component\DependencyInjection\ContainerInterface
+ public: true
+ synthetic: true
+ closure_property:
+ class: stdClass
+ public: true
+ properties: { foo: !service { class: Closure, arguments: [!service { class: stdClass }], factory: [Closure, fromCallable] } }
diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/closure.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/closure.php
new file mode 100644
index 0000000000000..4f67ba048b028
--- /dev/null
+++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/closure.php
@@ -0,0 +1,14 @@
+services()
+ ->set('closure_property', 'stdClass')
+ ->public()
+ ->property('foo', closure(service('bar')))
+ ->set('bar', 'stdClass');
+ }
+};
diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/closure.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/closure.php
new file mode 100644
index 0000000000000..626f78a8a3530
--- /dev/null
+++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/closure.php
@@ -0,0 +1,55 @@
+services = $this->privates = [];
+ $this->methodMap = [
+ 'closure' => 'getClosureService',
+ ];
+
+ $this->aliases = [];
+ }
+
+ public function compile(): void
+ {
+ throw new LogicException('You cannot compile a dumped container that was already compiled.');
+ }
+
+ public function isCompiled(): bool
+ {
+ return true;
+ }
+
+ public function getRemovedIds(): array
+ {
+ return [
+ 'bar' => true,
+ ];
+ }
+
+ /**
+ * Gets the public 'closure' shared service.
+ *
+ * @return \Closure
+ */
+ protected function getClosureService()
+ {
+ return $this->services['closure'] = (new \stdClass())->__invoke(...);
+ }
+}
diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/closure.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/closure.xml
new file mode 100644
index 0000000000000..4f45cac98de6a
--- /dev/null
+++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/closure.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/closure.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/closure.yml
new file mode 100644
index 0000000000000..c44aee08f2da2
--- /dev/null
+++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/closure.yml
@@ -0,0 +1,4 @@
+services:
+ closure_property:
+ class: stdClass
+ properties: { foo: !closure '@bar' }
diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/PhpFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/PhpFileLoaderTest.php
index 84713b45601d6..a16d44814fb04 100644
--- a/src/Symfony/Component/DependencyInjection/Tests/Loader/PhpFileLoaderTest.php
+++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/PhpFileLoaderTest.php
@@ -98,6 +98,7 @@ public function provideConfig()
yield ['remove'];
yield ['config_builder'];
yield ['expression_factory'];
+ yield ['closure'];
}
public function testAutoConfigureAndChildDefinition()
diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php
index 9f6759d26875a..57f1e7cd6f1cb 100644
--- a/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php
+++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php
@@ -1113,4 +1113,14 @@ public function testWhenEnv()
$this->assertSame(['foo' => 234, 'bar' => 345], $container->getParameterBag()->all());
}
+
+ public function testClosure()
+ {
+ $container = new ContainerBuilder();
+ $loader = new XmlFileLoader($container, new FileLocator(self::$fixturesPath.'/xml'));
+ $loader->load('closure.xml');
+
+ $definition = $container->getDefinition('closure_property')->getProperties()['foo'];
+ $this->assertEquals((new Definition('Closure'))->setFactory(['Closure', 'fromCallable'])->addArgument(new Reference('bar')), $definition);
+ }
}
diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php
index 8f510ae792b6d..4181f4209a523 100644
--- a/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php
+++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php
@@ -1088,4 +1088,14 @@ public function testWhenEnv()
$this->assertSame(['foo' => 234, 'bar' => 345], $container->getParameterBag()->all());
}
+
+ public function testClosure()
+ {
+ $container = new ContainerBuilder();
+ $loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml'));
+ $loader->load('closure.yml');
+
+ $definition = $container->getDefinition('closure_property')->getProperties()['foo'];
+ $this->assertEquals((new Definition('Closure'))->setFactory(['Closure', 'fromCallable'])->addArgument(new Reference('bar')), $definition);
+ }
}