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); + } }