diff --git a/src/Symfony/Component/DependencyInjection/CHANGELOG.md b/src/Symfony/Component/DependencyInjection/CHANGELOG.md index 9413290cf615..afc87d925f30 100644 --- a/src/Symfony/Component/DependencyInjection/CHANGELOG.md +++ b/src/Symfony/Component/DependencyInjection/CHANGELOG.md @@ -10,6 +10,8 @@ CHANGELOG * Add `#[Lazy]` attribute as shortcut for `#[Autowire(lazy: [bool|string])]` and `#[Autoconfigure(lazy: [bool|string])]` * Add `#[AutowireMethodOf]` attribute to autowire a method of a service as a callable * Make `ContainerBuilder::registerAttributeForAutoconfiguration()` propagate to attribute classes that extend the registered class + * Add argument `$prepend` to `FileLoader::construct()` to prepend loaded configuration instead of appending it + * [BC BREAK] When used in the `prependExtension()` methods, the `ContainerConfigurator::import()` method now prepends the configuration instead of appending it 7.0 --- diff --git a/src/Symfony/Component/DependencyInjection/Extension/AbstractExtension.php b/src/Symfony/Component/DependencyInjection/Extension/AbstractExtension.php index c5c2f17adf97..795ed810dfc9 100644 --- a/src/Symfony/Component/DependencyInjection/Extension/AbstractExtension.php +++ b/src/Symfony/Component/DependencyInjection/Extension/AbstractExtension.php @@ -49,7 +49,7 @@ final public function prepend(ContainerBuilder $container): void $this->prependExtension($configurator, $container); }; - $this->executeConfiguratorCallback($container, $callback, $this); + $this->executeConfiguratorCallback($container, $callback, $this, true); } final public function load(array $configs, ContainerBuilder $container): void diff --git a/src/Symfony/Component/DependencyInjection/Extension/ExtensionTrait.php b/src/Symfony/Component/DependencyInjection/Extension/ExtensionTrait.php index 5bd88892fb9b..0bb008860fe0 100644 --- a/src/Symfony/Component/DependencyInjection/Extension/ExtensionTrait.php +++ b/src/Symfony/Component/DependencyInjection/Extension/ExtensionTrait.php @@ -30,10 +30,10 @@ */ trait ExtensionTrait { - private function executeConfiguratorCallback(ContainerBuilder $container, \Closure $callback, ConfigurableExtensionInterface $subject): void + private function executeConfiguratorCallback(ContainerBuilder $container, \Closure $callback, ConfigurableExtensionInterface $subject, bool $prepend = false): void { $env = $container->getParameter('kernel.environment'); - $loader = $this->createContainerLoader($container, $env); + $loader = $this->createContainerLoader($container, $env, $prepend); $file = (new \ReflectionObject($subject))->getFileName(); $bundleLoader = $loader->getResolver()->resolve($file); if (!$bundleLoader instanceof PhpFileLoader) { @@ -50,15 +50,15 @@ private function executeConfiguratorCallback(ContainerBuilder $container, \Closu } } - private function createContainerLoader(ContainerBuilder $container, string $env): DelegatingLoader + private function createContainerLoader(ContainerBuilder $container, string $env, bool $prepend): DelegatingLoader { $buildDir = $container->getParameter('kernel.build_dir'); $locator = new FileLocator(); $resolver = new LoaderResolver([ - new XmlFileLoader($container, $locator, $env), - new YamlFileLoader($container, $locator, $env), + new XmlFileLoader($container, $locator, $env, $prepend), + new YamlFileLoader($container, $locator, $env, $prepend), new IniFileLoader($container, $locator, $env), - new PhpFileLoader($container, $locator, $env, new ConfigBuilderGenerator($buildDir)), + new PhpFileLoader($container, $locator, $env, new ConfigBuilderGenerator($buildDir), $prepend), new GlobFileLoader($container, $locator, $env), new DirectoryLoader($container, $locator, $env), new ClosureLoader($container, $env), diff --git a/src/Symfony/Component/DependencyInjection/Loader/FileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/FileLoader.php index 76432ad36086..669082179675 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/FileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/FileLoader.php @@ -45,10 +45,17 @@ abstract class FileLoader extends BaseFileLoader /** @var array */ protected array $aliases = []; protected bool $autoRegisterAliasesForSinglyImplementedInterfaces = true; + protected bool $prepend = false; + protected array $extensionConfigs = []; + protected int $importing = 0; - public function __construct(ContainerBuilder $container, FileLocatorInterface $locator, ?string $env = null) + /** + * @param bool $prepend Whether to prepend extension config instead of appending them + */ + public function __construct(ContainerBuilder $container, FileLocatorInterface $locator, ?string $env = null, bool $prepend = false) { $this->container = $container; + $this->prepend = $prepend; parent::__construct($locator, $env); } @@ -66,6 +73,7 @@ public function import(mixed $resource, ?string $type = null, bool|string $ignor throw new \TypeError(sprintf('Invalid argument $ignoreErrors provided to "%s::import()": boolean or "not_found" expected, "%s" given.', static::class, get_debug_type($ignoreErrors))); } + ++$this->importing; try { return parent::import(...$args); } catch (LoaderLoadException $e) { @@ -82,6 +90,8 @@ public function import(mixed $resource, ?string $type = null, bool|string $ignor if (__FILE__ !== $frame['file']) { throw $e; } + } finally { + --$this->importing; } return null; @@ -217,6 +227,41 @@ public function registerAliasesForSinglyImplementedInterfaces(): void $this->interfaces = $this->singlyImplemented = $this->aliases = []; } + final protected function loadExtensionConfig(string $namespace, array $config): void + { + if (!$this->prepend) { + $this->container->loadFromExtension($namespace, $config); + + return; + } + + if ($this->importing) { + if (!isset($this->extensionConfigs[$namespace])) { + $this->extensionConfigs[$namespace] = []; + } + array_unshift($this->extensionConfigs[$namespace], $config); + + return; + } + + $this->container->prependExtensionConfig($namespace, $config); + } + + final protected function loadExtensionConfigs(): void + { + if ($this->importing || !$this->extensionConfigs) { + return; + } + + foreach ($this->extensionConfigs as $namespace => $configs) { + foreach ($configs as $config) { + $this->container->prependExtensionConfig($namespace, $config); + } + } + + $this->extensionConfigs = []; + } + /** * Registers a definition in the container with its instanceof-conditionals. */ diff --git a/src/Symfony/Component/DependencyInjection/Loader/PhpFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/PhpFileLoader.php index 9acaa8cffbfb..a1314948f82a 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/PhpFileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/PhpFileLoader.php @@ -36,9 +36,9 @@ class PhpFileLoader extends FileLoader protected bool $autoRegisterAliasesForSinglyImplementedInterfaces = false; private ?ConfigBuilderGeneratorInterface $generator; - public function __construct(ContainerBuilder $container, FileLocatorInterface $locator, ?string $env = null, ?ConfigBuilderGeneratorInterface $generator = null) + public function __construct(ContainerBuilder $container, FileLocatorInterface $locator, ?string $env = null, ?ConfigBuilderGeneratorInterface $generator = null, bool $prepend = false) { - parent::__construct($container, $locator, $env); + parent::__construct($container, $locator, $env, $prepend); $this->generator = $generator; } @@ -145,10 +145,19 @@ class_exists(ContainerConfigurator::class); $callback(...$arguments); - /** @var ConfigBuilderInterface $configBuilder */ + $this->loadFromExtensions($configBuilders); + } + + /** + * @param iterable $configBuilders + */ + private function loadFromExtensions(iterable $configBuilders): void + { foreach ($configBuilders as $configBuilder) { - $containerConfigurator->extension($configBuilder->getExtensionAlias(), $configBuilder->toArray()); + $this->loadExtensionConfig($configBuilder->getExtensionAlias(), $configBuilder->toArray()); } + + $this->loadExtensionConfigs(); } /** diff --git a/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php index 6fedd9821ace..48122e9f36cc 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php @@ -808,7 +808,7 @@ private function validateExtensions(\DOMDocument $dom, string $file): void } // can it be handled by an extension? - if (!$this->container->hasExtension($node->namespaceURI)) { + if (!$this->prepend && !$this->container->hasExtension($node->namespaceURI)) { $extensionNamespaces = array_filter(array_map(fn (ExtensionInterface $ext) => $ext->getNamespace(), $this->container->getExtensions())); throw new InvalidArgumentException(sprintf('There is no extension able to load the configuration for "%s" (in "%s"). Looked for namespace "%s", found "%s".', $node->tagName, $file, $node->namespaceURI, $extensionNamespaces ? implode('", "', $extensionNamespaces) : 'none')); } @@ -830,8 +830,10 @@ private function loadFromExtensions(\DOMDocument $xml): void $values = []; } - $this->container->loadFromExtension($node->namespaceURI, $values); + $this->loadExtensionConfig($node->namespaceURI, $values); } + + $this->loadExtensionConfigs(); } /** diff --git a/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php index c74e606be383..6215426542dc 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php @@ -129,22 +129,28 @@ public function load(mixed $resource, ?string $type = null): mixed return null; } - $this->loadContent($content, $path); + ++$this->importing; + try { + $this->loadContent($content, $path); - // per-env configuration - if ($this->env && isset($content['when@'.$this->env])) { - if (!\is_array($content['when@'.$this->env])) { - throw new InvalidArgumentException(sprintf('The "when@%s" key should contain an array in "%s". Check your YAML syntax.', $this->env, $path)); - } + // per-env configuration + if ($this->env && isset($content['when@'.$this->env])) { + if (!\is_array($content['when@'.$this->env])) { + throw new InvalidArgumentException(sprintf('The "when@%s" key should contain an array in "%s". Check your YAML syntax.', $this->env, $path)); + } - $env = $this->env; - $this->env = null; - try { - $this->loadContent($content['when@'.$env], $path); - } finally { - $this->env = $env; + $env = $this->env; + $this->env = null; + try { + $this->loadContent($content['when@'.$env], $path); + } finally { + $this->env = $env; + } } + } finally { + --$this->importing; } + $this->loadExtensionConfigs(); return null; } @@ -802,7 +808,7 @@ private function validate(mixed $content, string $file): ?array continue; } - if (!$this->container->hasExtension($namespace)) { + if (!$this->prepend && !$this->container->hasExtension($namespace)) { $extensionNamespaces = array_filter(array_map(fn (ExtensionInterface $ext) => $ext->getAlias(), $this->container->getExtensions())); throw new InvalidArgumentException(sprintf('There is no extension able to load the configuration for "%s" (in "%s"). Looked for namespace "%s", found "%s".', $namespace, $file, $namespace, $extensionNamespaces ? sprintf('"%s"', implode('", "', $extensionNamespaces)) : 'none')); } @@ -941,12 +947,14 @@ private function loadFromExtensions(array $content): void continue; } - if (!\is_array($values) && null !== $values) { + if (!\is_array($values)) { $values = []; } - $this->container->loadFromExtension($namespace, $values); + $this->loadExtensionConfig($namespace, $values); } + + $this->loadExtensionConfigs(); } private function checkDefinition(string $id, array $definition, string $file): void diff --git a/src/Symfony/Component/DependencyInjection/Tests/Extension/AbstractExtensionTest.php b/src/Symfony/Component/DependencyInjection/Tests/Extension/AbstractExtensionTest.php index 9180ab8342da..e98521b552ac 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Extension/AbstractExtensionTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Extension/AbstractExtensionTest.php @@ -54,28 +54,55 @@ public function configure(DefinitionConfigurator $definition): void self::assertSame($expected, $this->processConfiguration($extension)); } - public function testPrependAppendExtensionConfig() + public function testPrependExtensionConfig() { $extension = new class() extends AbstractExtension { + public function configure(DefinitionConfigurator $definition): void + { + $definition->rootNode() + ->children() + ->scalarNode('foo')->end() + ->end(); + } + public function prependExtension(ContainerConfigurator $container, ContainerBuilder $builder): void { - // append config - $container->extension('third', ['foo' => 'append']); + // prepend config from plain array + $container->extension('third', ['foo' => 'pong'], true); + + // prepend config from external file + $container->import('../Fixtures/config/packages/ping.yaml'); + } + + public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void + { + $container->parameters()->set('foo_param', $config['foo']); + } - // prepend config - $container->extension('third', ['foo' => 'prepend'], true); + public function getAlias(): string + { + return 'third'; } }; $container = $this->processPrependExtension($extension); $expected = [ - ['foo' => 'prepend'], + ['foo' => 'a'], + ['foo' => 'c1'], + ['foo' => 'c2'], + ['foo' => 'b'], + ['foo' => 'ping'], + ['foo' => 'zaa'], + ['foo' => 'pong'], ['foo' => 'bar'], - ['foo' => 'append'], ]; self::assertSame($expected, $container->getExtensionConfig('third')); + + $container = $this->processLoadExtension($extension, $expected); + + self::assertSame('bar', $container->getParameter('foo_param')); } public function testLoadExtension() diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/packages/ping.yaml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/packages/ping.yaml new file mode 100644 index 000000000000..e83b57070469 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/packages/ping.yaml @@ -0,0 +1,10 @@ +imports: + - { resource: './third_a.yaml' } + - { resource: './third_b.yaml' } + +third: + foo: ping + +when@test: + third: + foo: zaa diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/packages/third_a.yaml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/packages/third_a.yaml new file mode 100644 index 000000000000..d15b3f589c68 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/packages/third_a.yaml @@ -0,0 +1,2 @@ +third: + foo: a diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/packages/third_b.yaml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/packages/third_b.yaml new file mode 100644 index 000000000000..40b02dcbff8b --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/packages/third_b.yaml @@ -0,0 +1,5 @@ +imports: + - { resource: './third_c.yaml' } + +third: + foo: b diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/packages/third_c.yaml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/packages/third_c.yaml new file mode 100644 index 000000000000..8ea12d674b40 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/config/packages/third_c.yaml @@ -0,0 +1,6 @@ +third: + foo: c1 + +when@test: + third: + foo: c2 diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/extensions/services1.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/extensions/services1.xml index b1cc3904ab0d..0e77fc6e6bbc 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/extensions/services1.xml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/extensions/services1.xml @@ -11,9 +11,13 @@ - + %project.parameter.foo% + + + + diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/PhpFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/PhpFileLoaderTest.php index a2b440b54d0e..39426d6b691d 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/PhpFileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/PhpFileLoaderTest.php @@ -47,6 +47,21 @@ public function testLoad() $this->assertEquals('foo', $container->getParameter('foo'), '->load() loads a PHP file resource'); } + public function testPrependExtensionConfig() + { + $container = new ContainerBuilder(); + $container->registerExtension(new \AcmeExtension()); + $container->prependExtensionConfig('acme', ['foo' => 'bar']); + $loader = new PhpFileLoader($container, new FileLocator(\dirname(__DIR__).'/Fixtures'), 'prod', new ConfigBuilderGenerator(sys_get_temp_dir()), true); + $loader->load('config/config_builder.php'); + + $expected = [ + ['color' => 'blue'], + ['foo' => 'bar'], + ]; + $this->assertSame($expected, $container->getExtensionConfig('acme')); + } + public function testConfigServices() { $fixtures = realpath(__DIR__.'/../Fixtures'); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php index 45c4d65b769c..ffe602704615 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php @@ -604,6 +604,20 @@ public function testExtensions() } } + public function testPrependExtensionConfig() + { + $container = new ContainerBuilder(); + $container->prependExtensionConfig('http://www.example.com/schema/project', ['foo' => 'bar']); + $loader = new XmlFileLoader($container, new FileLocator(self::$fixturesPath.'/xml'), prepend: true); + $loader->load('extensions/services1.xml'); + + $expected = [ + ['foo' => 'ping'], + ['foo' => 'bar'], + ]; + $this->assertSame($expected, $container->getExtensionConfig('http://www.example.com/schema/project')); + } + public function testExtensionInPhar() { if (\extension_loaded('suhosin') && !str_contains(\ini_get('suhosin.executor.include.whitelist'), 'phar')) { diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php index 5be7ed74ad7f..e9a148b974d9 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php @@ -346,6 +346,20 @@ public function testExtensions() } } + public function testPrependExtensionConfig() + { + $container = new ContainerBuilder(); + $container->prependExtensionConfig('project', ['foo' => 'bar']); + $loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml'), prepend: true); + $loader->load('services10.yml'); + + $expected = [ + ['test' => '%project.parameter.foo%'], + ['foo' => 'bar'], + ]; + $this->assertSame($expected, $container->getExtensionConfig('project')); + } + public function testExtensionWithNullConfig() { $container = new ContainerBuilder(); diff --git a/src/Symfony/Component/HttpKernel/Bundle/BundleExtension.php b/src/Symfony/Component/HttpKernel/Bundle/BundleExtension.php index b80bc21f25cf..8392218a29d9 100644 --- a/src/Symfony/Component/HttpKernel/Bundle/BundleExtension.php +++ b/src/Symfony/Component/HttpKernel/Bundle/BundleExtension.php @@ -51,7 +51,7 @@ public function prepend(ContainerBuilder $container): void $this->subject->prependExtension($configurator, $container); }; - $this->executeConfiguratorCallback($container, $callback, $this->subject); + $this->executeConfiguratorCallback($container, $callback, $this->subject, true); } public function load(array $configs, ContainerBuilder $container): void