Skip to content

Commit 4ce4e5e

Browse files
feature #52843 [DependencyInjection] Prepending extension configs with file loaders (yceruto)
This PR was merged into the 7.1 branch. Discussion ---------- [DependencyInjection] Prepending extension configs with file loaders | Q | A | ------------- | --- | Branch? | 7.1 | Bug fix? | no | New feature? | yes | Deprecations? | no | Issues | Fix #52789 | License | MIT #52636 continuation. This is another try to simplify even more the prepending extension config strategy for Bundles and Extensions. Feature: "Import and prepend an extension configuration from an external YAML, XML or PHP file" > Useful when you need to pre-configure some functionalities in your app, such as mapping some DBAL types provided by your bundle, or simply pre-configuring some default values automatically when your bundle is installed, without losing the ability to override them if desired in userland. ```php class AcmeFooBundle extends AbstractBundle { public function prependExtension(ContainerConfigurator $container, ContainerBuilder $builder): void { // Before $config = Yaml::parse(file_get_contents(__DIR__.'/../config/doctrine.yaml')); $builder->prependExtensionConfig('doctrine', $config['doctrine']); // After $container->import('../config/doctrine.yaml'); } } ``` The "Before" section is limited to importing/parsing this kind of configuration by hand; it doesn't support features like `when@env`, recursive importing, or defining parameters/services in the same file. Further, you can't simply use `ContainerConfigurator::import()` or any `*FileLoader::load()` here, as they will append the extension config instead of prepending it, defeating the prepend method purpose and breaking your app's config strategy. Thus, you are forced to use `$builder->prependExtensionConfig()` as the only way. > This is because if you append any extension config using `$container->import()`, then you won't be able to change that config in your app as it's merged with priority. If anyone is doing that currently, it shouldn't be intentional. Therefore, the "After" proposal changes that behavior for `$container->import()`. *Now, all extension configurations encountered in your external file will be prepended instead. BUT, this will only happen here, at this moment, during the `prependExtension()` method.* Of course, this little behavior change is a "BC break". However, it is a behavior that makes more sense than the previous one, enabling the usage of `ContainerConfigurator::import()` here, which was previously ineffective. This capability is also available for `YamlFileLoader`, `XmlFileLoader` and `PhpFileLoader` through a new boolean constructor argument: ```php class AcmeFooBundle extends AbstractBundle { public function prependExtension(ContainerConfigurator $container, ContainerBuilder $builder): void { // Before // - It can't be used for prepending config purposes // After $loader = new YamlFileLoader($builder, new FileLocator(__DIR__.'/../config/'), prepend: true); $loader->load('doctrine.yaml'); // now it prepends extension configs as default behavior // or $loader->import('prepend/*.yaml'); } } ``` These changes only affect the loading strategy for extension configs; everything else keeps working as before. Cheers! Commits ------- 6ffd706 prepend extension configs with file loaders
2 parents 25c93ba + 6ffd706 commit 4ce4e5e

File tree

17 files changed

+201
-38
lines changed

17 files changed

+201
-38
lines changed

src/Symfony/Component/DependencyInjection/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ CHANGELOG
1010
* Add `#[Lazy]` attribute as shortcut for `#[Autowire(lazy: [bool|string])]` and `#[Autoconfigure(lazy: [bool|string])]`
1111
* Add `#[AutowireMethodOf]` attribute to autowire a method of a service as a callable
1212
* Make `ContainerBuilder::registerAttributeForAutoconfiguration()` propagate to attribute classes that extend the registered class
13+
* Add argument `$prepend` to `FileLoader::construct()` to prepend loaded configuration instead of appending it
14+
* [BC BREAK] When used in the `prependExtension()` methods, the `ContainerConfigurator::import()` method now prepends the configuration instead of appending it
1315

1416
7.0
1517
---

src/Symfony/Component/DependencyInjection/Extension/AbstractExtension.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ final public function prepend(ContainerBuilder $container): void
4949
$this->prependExtension($configurator, $container);
5050
};
5151

52-
$this->executeConfiguratorCallback($container, $callback, $this);
52+
$this->executeConfiguratorCallback($container, $callback, $this, true);
5353
}
5454

5555
final public function load(array $configs, ContainerBuilder $container): void

src/Symfony/Component/DependencyInjection/Extension/ExtensionTrait.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,10 @@
3030
*/
3131
trait ExtensionTrait
3232
{
33-
private function executeConfiguratorCallback(ContainerBuilder $container, \Closure $callback, ConfigurableExtensionInterface $subject): void
33+
private function executeConfiguratorCallback(ContainerBuilder $container, \Closure $callback, ConfigurableExtensionInterface $subject, bool $prepend = false): void
3434
{
3535
$env = $container->getParameter('kernel.environment');
36-
$loader = $this->createContainerLoader($container, $env);
36+
$loader = $this->createContainerLoader($container, $env, $prepend);
3737
$file = (new \ReflectionObject($subject))->getFileName();
3838
$bundleLoader = $loader->getResolver()->resolve($file);
3939
if (!$bundleLoader instanceof PhpFileLoader) {
@@ -50,15 +50,15 @@ private function executeConfiguratorCallback(ContainerBuilder $container, \Closu
5050
}
5151
}
5252

53-
private function createContainerLoader(ContainerBuilder $container, string $env): DelegatingLoader
53+
private function createContainerLoader(ContainerBuilder $container, string $env, bool $prepend): DelegatingLoader
5454
{
5555
$buildDir = $container->getParameter('kernel.build_dir');
5656
$locator = new FileLocator();
5757
$resolver = new LoaderResolver([
58-
new XmlFileLoader($container, $locator, $env),
59-
new YamlFileLoader($container, $locator, $env),
58+
new XmlFileLoader($container, $locator, $env, $prepend),
59+
new YamlFileLoader($container, $locator, $env, $prepend),
6060
new IniFileLoader($container, $locator, $env),
61-
new PhpFileLoader($container, $locator, $env, new ConfigBuilderGenerator($buildDir)),
61+
new PhpFileLoader($container, $locator, $env, new ConfigBuilderGenerator($buildDir), $prepend),
6262
new GlobFileLoader($container, $locator, $env),
6363
new DirectoryLoader($container, $locator, $env),
6464
new ClosureLoader($container, $env),

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

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,17 @@ abstract class FileLoader extends BaseFileLoader
4545
/** @var array<string, Alias> */
4646
protected array $aliases = [];
4747
protected bool $autoRegisterAliasesForSinglyImplementedInterfaces = true;
48+
protected bool $prepend = false;
49+
protected array $extensionConfigs = [];
50+
protected int $importing = 0;
4851

49-
public function __construct(ContainerBuilder $container, FileLocatorInterface $locator, ?string $env = null)
52+
/**
53+
* @param bool $prepend Whether to prepend extension config instead of appending them
54+
*/
55+
public function __construct(ContainerBuilder $container, FileLocatorInterface $locator, ?string $env = null, bool $prepend = false)
5056
{
5157
$this->container = $container;
58+
$this->prepend = $prepend;
5259

5360
parent::__construct($locator, $env);
5461
}
@@ -66,6 +73,7 @@ public function import(mixed $resource, ?string $type = null, bool|string $ignor
6673
throw new \TypeError(sprintf('Invalid argument $ignoreErrors provided to "%s::import()": boolean or "not_found" expected, "%s" given.', static::class, get_debug_type($ignoreErrors)));
6774
}
6875

76+
++$this->importing;
6977
try {
7078
return parent::import(...$args);
7179
} catch (LoaderLoadException $e) {
@@ -82,6 +90,8 @@ public function import(mixed $resource, ?string $type = null, bool|string $ignor
8290
if (__FILE__ !== $frame['file']) {
8391
throw $e;
8492
}
93+
} finally {
94+
--$this->importing;
8595
}
8696

8797
return null;
@@ -217,6 +227,41 @@ public function registerAliasesForSinglyImplementedInterfaces(): void
217227
$this->interfaces = $this->singlyImplemented = $this->aliases = [];
218228
}
219229

230+
final protected function loadExtensionConfig(string $namespace, array $config): void
231+
{
232+
if (!$this->prepend) {
233+
$this->container->loadFromExtension($namespace, $config);
234+
235+
return;
236+
}
237+
238+
if ($this->importing) {
239+
if (!isset($this->extensionConfigs[$namespace])) {
240+
$this->extensionConfigs[$namespace] = [];
241+
}
242+
array_unshift($this->extensionConfigs[$namespace], $config);
243+
244+
return;
245+
}
246+
247+
$this->container->prependExtensionConfig($namespace, $config);
248+
}
249+
250+
final protected function loadExtensionConfigs(): void
251+
{
252+
if ($this->importing || !$this->extensionConfigs) {
253+
return;
254+
}
255+
256+
foreach ($this->extensionConfigs as $namespace => $configs) {
257+
foreach ($configs as $config) {
258+
$this->container->prependExtensionConfig($namespace, $config);
259+
}
260+
}
261+
262+
$this->extensionConfigs = [];
263+
}
264+
220265
/**
221266
* Registers a definition in the container with its instanceof-conditionals.
222267
*/

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

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,9 @@ class PhpFileLoader extends FileLoader
3636
protected bool $autoRegisterAliasesForSinglyImplementedInterfaces = false;
3737
private ?ConfigBuilderGeneratorInterface $generator;
3838

39-
public function __construct(ContainerBuilder $container, FileLocatorInterface $locator, ?string $env = null, ?ConfigBuilderGeneratorInterface $generator = null)
39+
public function __construct(ContainerBuilder $container, FileLocatorInterface $locator, ?string $env = null, ?ConfigBuilderGeneratorInterface $generator = null, bool $prepend = false)
4040
{
41-
parent::__construct($container, $locator, $env);
41+
parent::__construct($container, $locator, $env, $prepend);
4242
$this->generator = $generator;
4343
}
4444

@@ -145,10 +145,19 @@ class_exists(ContainerConfigurator::class);
145145

146146
$callback(...$arguments);
147147

148-
/** @var ConfigBuilderInterface $configBuilder */
148+
$this->loadFromExtensions($configBuilders);
149+
}
150+
151+
/**
152+
* @param iterable<ConfigBuilderInterface> $configBuilders
153+
*/
154+
private function loadFromExtensions(iterable $configBuilders): void
155+
{
149156
foreach ($configBuilders as $configBuilder) {
150-
$containerConfigurator->extension($configBuilder->getExtensionAlias(), $configBuilder->toArray());
157+
$this->loadExtensionConfig($configBuilder->getExtensionAlias(), $configBuilder->toArray());
151158
}
159+
160+
$this->loadExtensionConfigs();
152161
}
153162

154163
/**

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -808,7 +808,7 @@ private function validateExtensions(\DOMDocument $dom, string $file): void
808808
}
809809

810810
// can it be handled by an extension?
811-
if (!$this->container->hasExtension($node->namespaceURI)) {
811+
if (!$this->prepend && !$this->container->hasExtension($node->namespaceURI)) {
812812
$extensionNamespaces = array_filter(array_map(fn (ExtensionInterface $ext) => $ext->getNamespace(), $this->container->getExtensions()));
813813
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'));
814814
}
@@ -830,8 +830,10 @@ private function loadFromExtensions(\DOMDocument $xml): void
830830
$values = [];
831831
}
832832

833-
$this->container->loadFromExtension($node->namespaceURI, $values);
833+
$this->loadExtensionConfig($node->namespaceURI, $values);
834834
}
835+
836+
$this->loadExtensionConfigs();
835837
}
836838

837839
/**

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

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -129,22 +129,28 @@ public function load(mixed $resource, ?string $type = null): mixed
129129
return null;
130130
}
131131

132-
$this->loadContent($content, $path);
132+
++$this->importing;
133+
try {
134+
$this->loadContent($content, $path);
133135

134-
// per-env configuration
135-
if ($this->env && isset($content['when@'.$this->env])) {
136-
if (!\is_array($content['when@'.$this->env])) {
137-
throw new InvalidArgumentException(sprintf('The "when@%s" key should contain an array in "%s". Check your YAML syntax.', $this->env, $path));
138-
}
136+
// per-env configuration
137+
if ($this->env && isset($content['when@'.$this->env])) {
138+
if (!\is_array($content['when@'.$this->env])) {
139+
throw new InvalidArgumentException(sprintf('The "when@%s" key should contain an array in "%s". Check your YAML syntax.', $this->env, $path));
140+
}
139141

140-
$env = $this->env;
141-
$this->env = null;
142-
try {
143-
$this->loadContent($content['when@'.$env], $path);
144-
} finally {
145-
$this->env = $env;
142+
$env = $this->env;
143+
$this->env = null;
144+
try {
145+
$this->loadContent($content['when@'.$env], $path);
146+
} finally {
147+
$this->env = $env;
148+
}
146149
}
150+
} finally {
151+
--$this->importing;
147152
}
153+
$this->loadExtensionConfigs();
148154

149155
return null;
150156
}
@@ -802,7 +808,7 @@ private function validate(mixed $content, string $file): ?array
802808
continue;
803809
}
804810

805-
if (!$this->container->hasExtension($namespace)) {
811+
if (!$this->prepend && !$this->container->hasExtension($namespace)) {
806812
$extensionNamespaces = array_filter(array_map(fn (ExtensionInterface $ext) => $ext->getAlias(), $this->container->getExtensions()));
807813
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'));
808814
}
@@ -941,12 +947,14 @@ private function loadFromExtensions(array $content): void
941947
continue;
942948
}
943949

944-
if (!\is_array($values) && null !== $values) {
950+
if (!\is_array($values)) {
945951
$values = [];
946952
}
947953

948-
$this->container->loadFromExtension($namespace, $values);
954+
$this->loadExtensionConfig($namespace, $values);
949955
}
956+
957+
$this->loadExtensionConfigs();
950958
}
951959

952960
private function checkDefinition(string $id, array $definition, string $file): void

src/Symfony/Component/DependencyInjection/Tests/Extension/AbstractExtensionTest.php

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,28 +54,55 @@ public function configure(DefinitionConfigurator $definition): void
5454
self::assertSame($expected, $this->processConfiguration($extension));
5555
}
5656

57-
public function testPrependAppendExtensionConfig()
57+
public function testPrependExtensionConfig()
5858
{
5959
$extension = new class() extends AbstractExtension {
60+
public function configure(DefinitionConfigurator $definition): void
61+
{
62+
$definition->rootNode()
63+
->children()
64+
->scalarNode('foo')->end()
65+
->end();
66+
}
67+
6068
public function prependExtension(ContainerConfigurator $container, ContainerBuilder $builder): void
6169
{
62-
// append config
63-
$container->extension('third', ['foo' => 'append']);
70+
// prepend config from plain array
71+
$container->extension('third', ['foo' => 'pong'], true);
72+
73+
// prepend config from external file
74+
$container->import('../Fixtures/config/packages/ping.yaml');
75+
}
76+
77+
public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void
78+
{
79+
$container->parameters()->set('foo_param', $config['foo']);
80+
}
6481

65-
// prepend config
66-
$container->extension('third', ['foo' => 'prepend'], true);
82+
public function getAlias(): string
83+
{
84+
return 'third';
6785
}
6886
};
6987

7088
$container = $this->processPrependExtension($extension);
7189

7290
$expected = [
73-
['foo' => 'prepend'],
91+
['foo' => 'a'],
92+
['foo' => 'c1'],
93+
['foo' => 'c2'],
94+
['foo' => 'b'],
95+
['foo' => 'ping'],
96+
['foo' => 'zaa'],
97+
['foo' => 'pong'],
7498
['foo' => 'bar'],
75-
['foo' => 'append'],
7699
];
77100

78101
self::assertSame($expected, $container->getExtensionConfig('third'));
102+
103+
$container = $this->processLoadExtension($extension, $expected);
104+
105+
self::assertSame('bar', $container->getParameter('foo_param'));
79106
}
80107

81108
public function testLoadExtension()
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
imports:
2+
- { resource: './third_a.yaml' }
3+
- { resource: './third_b.yaml' }
4+
5+
third:
6+
foo: ping
7+
8+
when@test:
9+
third:
10+
foo: zaa
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
third:
2+
foo: a
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
imports:
2+
- { resource: './third_c.yaml' }
3+
4+
third:
5+
foo: b
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
third:
2+
foo: c1
3+
4+
when@test:
5+
third:
6+
foo: c2

src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/extensions/services1.xml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,13 @@
1111
<service id="project.service.foo" class="BAR" public="true"/>
1212
</services>
1313

14-
<project:bar babar="babar">
14+
<project:bar foo="ping">
1515
<another />
1616
<another2>%project.parameter.foo%</another2>
1717
</project:bar>
1818

19+
<when env="test">
20+
<project:bar foo="zaa">
21+
</project:bar>
22+
</when>
1923
</container>

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,21 @@ public function testLoad()
4747
$this->assertEquals('foo', $container->getParameter('foo'), '->load() loads a PHP file resource');
4848
}
4949

50+
public function testPrependExtensionConfig()
51+
{
52+
$container = new ContainerBuilder();
53+
$container->registerExtension(new \AcmeExtension());
54+
$container->prependExtensionConfig('acme', ['foo' => 'bar']);
55+
$loader = new PhpFileLoader($container, new FileLocator(\dirname(__DIR__).'/Fixtures'), 'prod', new ConfigBuilderGenerator(sys_get_temp_dir()), true);
56+
$loader->load('config/config_builder.php');
57+
58+
$expected = [
59+
['color' => 'blue'],
60+
['foo' => 'bar'],
61+
];
62+
$this->assertSame($expected, $container->getExtensionConfig('acme'));
63+
}
64+
5065
public function testConfigServices()
5166
{
5267
$fixtures = realpath(__DIR__.'/../Fixtures');

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -604,6 +604,20 @@ public function testExtensions()
604604
}
605605
}
606606

607+
public function testPrependExtensionConfig()
608+
{
609+
$container = new ContainerBuilder();
610+
$container->prependExtensionConfig('http://www.example.com/schema/project', ['foo' => 'bar']);
611+
$loader = new XmlFileLoader($container, new FileLocator(self::$fixturesPath.'/xml'), prepend: true);
612+
$loader->load('extensions/services1.xml');
613+
614+
$expected = [
615+
['foo' => 'ping'],
616+
['foo' => 'bar'],
617+
];
618+
$this->assertSame($expected, $container->getExtensionConfig('http://www.example.com/schema/project'));
619+
}
620+
607621
public function testExtensionInPhar()
608622
{
609623
if (\extension_loaded('suhosin') && !str_contains(\ini_get('suhosin.executor.include.whitelist'), 'phar')) {

0 commit comments

Comments
 (0)