From ff3fbd75cf8a60b24cb5b0f74538dceae2bc07f7 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 29 Sep 2025 14:46:59 +0200 Subject: [PATCH] [DependencyInjection] Handle returning arrays and config-builders from config files --- .../DependencyInjection/CHANGELOG.md | 1 + .../Loader/PhpFileLoader.php | 51 ++++++++++++++----- .../Tests/Fixtures/AcmeConfig.php | 6 +++ .../Fixtures/config/return_config_builder.php | 8 +++ .../Fixtures/config/return_generator.php | 7 +++ .../Fixtures/config/return_invalid_types.php | 3 ++ .../config/return_iterable_configs.php | 8 +++ .../Tests/Loader/PhpFileLoaderTest.php | 46 +++++++++++++++++ 8 files changed, 116 insertions(+), 14 deletions(-) create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/return_config_builder.php create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/return_generator.php create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/return_invalid_types.php create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/return_iterable_configs.php diff --git a/src/Symfony/Component/DependencyInjection/CHANGELOG.md b/src/Symfony/Component/DependencyInjection/CHANGELOG.md index aa4463463940f..bc5eb41ae7c21 100644 --- a/src/Symfony/Component/DependencyInjection/CHANGELOG.md +++ b/src/Symfony/Component/DependencyInjection/CHANGELOG.md @@ -10,6 +10,7 @@ CHANGELOG * Add argument `$target` to `ContainerBuilder::registerAliasForArgument()` * Deprecate registering a service without a class when its id is a non-existing FQCN * Allow multiple `#[AsDecorator]` attributes + * Handle returning arrays and config-builders from config files 7.3 --- diff --git a/src/Symfony/Component/DependencyInjection/Loader/PhpFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/PhpFileLoader.php index 217eb7cc56faa..46a389694030d 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/PhpFileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/PhpFileLoader.php @@ -57,17 +57,42 @@ public function load(mixed $resource, ?string $type = null): mixed $this->setCurrentDir(\dirname($path)); $this->container->fileExists($path); + // Force load ContainerConfigurator to make env(), param() etc available. + class_exists(ContainerConfigurator::class); + // the closure forbids access to the private scope in the included file $load = \Closure::bind(function ($path, $env) use ($container, $loader, $resource, $type) { return include $path; }, $this, ProtectedPhpFileLoader::class); try { - $callback = $load($path, $this->env); + if (1 === $result = $load($path, $this->env)) { + $result = null; + } - if (\is_object($callback) && \is_callable($callback)) { - $this->executeCallback($callback, new ContainerConfigurator($this->container, $this, $this->instanceof, $path, $resource, $this->env), $path); + if (\is_object($result) && \is_callable($result)) { + $result = $this->executeCallback($result, new ContainerConfigurator($this->container, $this, $this->instanceof, $path, $resource, $this->env), $path); } + if ($result instanceof ConfigBuilderInterface) { + $this->loadExtensionConfig($result->getExtensionAlias(), ContainerConfigurator::processValue($result->toArray())); + } elseif (is_iterable($result)) { + foreach ($result as $key => $config) { + if ($config instanceof ConfigBuilderInterface) { + if (\is_string($key) && $config->getExtensionAlias() !== $key) { + throw new InvalidArgumentException(\sprintf('The extension alias "%s" of the "%s" config builder does not match the key "%s" in file "%s".', $config->getExtensionAlias(), get_debug_type($config), $key, $path)); + } + $this->loadExtensionConfig($config->getExtensionAlias(), ContainerConfigurator::processValue($config->toArray())); + } elseif (!\is_string($key) || !\is_array($config)) { + throw new InvalidArgumentException(\sprintf('The configuration returned in file "%s" must yield only string-keyed arrays or ConfigBuilderInterface values.', $path)); + } else { + $this->loadExtensionConfig($key, ContainerConfigurator::processValue($config)); + } + } + } elseif (null !== $result) { + throw new InvalidArgumentException(\sprintf('The return value in config file "%s" is invalid.', $path)); + } + + $this->loadExtensionConfigs(); } finally { $this->instanceof = []; $this->registerAliasesForSinglyImplementedInterfaces(); @@ -92,7 +117,7 @@ public function supports(mixed $resource, ?string $type = null): bool /** * Resolve the parameters to the $callback and execute it. */ - private function executeCallback(callable $callback, ContainerConfigurator $containerConfigurator, string $path): void + private function executeCallback(callable $callback, ContainerConfigurator $containerConfigurator, string $path): mixed { $callback = $callback(...); $arguments = []; @@ -125,7 +150,7 @@ private function executeCallback(callable $callback, ContainerConfigurator $cont } if ($excluded) { - return; + return null; } foreach ($r->getParameters() as $parameter) { @@ -163,21 +188,19 @@ private function executeCallback(callable $callback, ContainerConfigurator $cont } } - // Force load ContainerConfigurator to make env(), param() etc available. - class_exists(ContainerConfigurator::class); - ++$this->importing; try { - $callback(...$arguments); + return $callback(...$arguments); + } catch (\Throwable $e) { + $configBuilders = []; + throw $e; } finally { --$this->importing; - } - foreach ($configBuilders as $configBuilder) { - $this->loadExtensionConfig($configBuilder->getExtensionAlias(), ContainerConfigurator::processValue($configBuilder->toArray())); + foreach ($configBuilders as $configBuilder) { + $this->loadExtensionConfig($configBuilder->getExtensionAlias(), ContainerConfigurator::processValue($configBuilder->toArray())); + } } - - $this->loadExtensionConfigs(); } /** diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/AcmeConfig.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/AcmeConfig.php index 5f315de4260b8..afbeedae941ac 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/AcmeConfig.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/AcmeConfig.php @@ -10,6 +10,12 @@ class AcmeConfig implements ConfigBuilderInterface private $nested; + public function __construct(array $config = []) + { + $this->color = $config['color'] ?? null; + $this->nested = $config['nested'] ?? null; + } + public function color($value) { $this->color = $value; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/return_config_builder.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/return_config_builder.php new file mode 100644 index 0000000000000..db7aac8cfea58 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/return_config_builder.php @@ -0,0 +1,8 @@ +color('red'); + +return $config; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/return_generator.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/return_generator.php new file mode 100644 index 0000000000000..83a9dc45d4c0d --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/return_generator.php @@ -0,0 +1,7 @@ + 'red']); +}; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/return_invalid_types.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/return_invalid_types.php new file mode 100644 index 0000000000000..488bf694bb1bc --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/return_invalid_types.php @@ -0,0 +1,3 @@ + ['color' => 'red'], + new AcmeConfig(), +]; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/PhpFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/PhpFileLoaderTest.php index 18fe7e8c2a48c..b241fc5785da7 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/PhpFileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/PhpFileLoaderTest.php @@ -319,4 +319,50 @@ public function testNamedClosure() $dumper = new PhpDumper($container); $this->assertStringEqualsFile(\dirname(__DIR__).'/Fixtures/php/named_closure_compiled.php', $dumper->dump()); } + + public function testReturnsConfigBuilderObject() + { + $container = new ContainerBuilder(); + $container->registerExtension(new \AcmeExtension()); + $loader = new PhpFileLoader($container, new FileLocator(\dirname(__DIR__).'/Fixtures/config'), 'prod', new ConfigBuilderGenerator(sys_get_temp_dir())); + + $loader->load('return_config_builder.php'); + + $this->assertSame([['color' => 'red']], $container->getExtensionConfig('acme')); + } + + public function testReturnsIterableOfArraysAndBuilders() + { + $container = new ContainerBuilder(); + $container->registerExtension(new \AcmeExtension()); + $loader = new PhpFileLoader($container, new FileLocator(\dirname(__DIR__).'/Fixtures/config'), 'prod', new ConfigBuilderGenerator(sys_get_temp_dir())); + + $loader->load('return_iterable_configs.php'); + + $configs = $container->getExtensionConfig('acme'); + $this->assertCount(2, $configs); + $this->assertSame('red', $configs[0]['color']); + $this->assertArrayHasKey('color', $configs[1]); + } + + public function testThrowsOnInvalidReturnType() + { + $container = new ContainerBuilder(); + $loader = new PhpFileLoader($container, new FileLocator(\dirname(__DIR__).'/Fixtures/config'), 'prod', new ConfigBuilderGenerator(sys_get_temp_dir())); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessageMatches('/The return value in config file/'); + + $loader->load('return_invalid_types.php'); + } + + public function testReturnsGenerator() + { + $container = new ContainerBuilder(); + $container->registerExtension(new \AcmeExtension()); + $loader = new PhpFileLoader($container, new FileLocator(\dirname(__DIR__).'/Fixtures/config'), 'prod', new ConfigBuilderGenerator(sys_get_temp_dir())); + + $loader->load('return_generator.php'); + $this->assertSame([['color' => 'red']], $container->getExtensionConfig('acme')); + } }