Skip to content

Commit a9ba320

Browse files
Nyholmnicolas-grekas
authored andcommitted
[Config][DependencyInjection] Add configuration builder for writing PHP config
1 parent 741e728 commit a9ba320

File tree

7 files changed

+187
-2
lines changed

7 files changed

+187
-2
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ CHANGELOG
1313
* Add `ContainerBuilder::willBeAvailable()` to help with conditional configuration
1414
* Add support an integer return value for default_index_method
1515
* Add `env()` and `EnvConfigurator` in the PHP-DSL
16+
* Add support for `ConfigBuilder` in the `PhpFileLoader`
1617

1718
5.2.0
1819
-----

Loader/PhpFileLoader.php

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,15 @@
1111

1212
namespace Symfony\Component\DependencyInjection\Loader;
1313

14+
use Symfony\Component\Config\Builder\ConfigBuilderGenerator;
15+
use Symfony\Component\Config\Builder\ConfigBuilderGeneratorInterface;
16+
use Symfony\Component\Config\Builder\ConfigBuilderInterface;
17+
use Symfony\Component\Config\FileLocatorInterface;
18+
use Symfony\Component\DependencyInjection\Container;
19+
use Symfony\Component\DependencyInjection\ContainerBuilder;
20+
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
21+
use Symfony\Component\DependencyInjection\Extension\ConfigurationExtensionInterface;
22+
use Symfony\Component\DependencyInjection\Extension\ExtensionInterface;
1423
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
1524

1625
/**
@@ -24,6 +33,13 @@
2433
class PhpFileLoader extends FileLoader
2534
{
2635
protected $autoRegisterAliasesForSinglyImplementedInterfaces = false;
36+
private $generator;
37+
38+
public function __construct(ContainerBuilder $container, FileLocatorInterface $locator, string $env = null, ?ConfigBuilderGeneratorInterface $generator = null)
39+
{
40+
parent::__construct($container, $locator, $env);
41+
$this->generator = $generator;
42+
}
2743

2844
/**
2945
* {@inheritdoc}
@@ -47,7 +63,7 @@ public function load($resource, string $type = null)
4763
$callback = $load($path);
4864

4965
if (\is_object($callback) && \is_callable($callback)) {
50-
$callback(new ContainerConfigurator($this->container, $this, $this->instanceof, $path, $resource, $this->env), $this->container, $this);
66+
$this->executeCallback($callback, new ContainerConfigurator($this->container, $this, $this->instanceof, $path, $resource, $this->env), $path);
5167
}
5268
} finally {
5369
$this->instanceof = [];
@@ -70,6 +86,97 @@ public function supports($resource, string $type = null)
7086

7187
return 'php' === $type;
7288
}
89+
90+
/**
91+
* Resolve the parameters to the $callback and execute it.
92+
*/
93+
private function executeCallback(callable $callback, ContainerConfigurator $containerConfigurator, string $path)
94+
{
95+
if (!$callback instanceof \Closure) {
96+
$callback = \Closure::fromCallable($callback);
97+
}
98+
99+
$arguments = [];
100+
$configBuilders = [];
101+
$parameters = (new \ReflectionFunction($callback))->getParameters();
102+
foreach ($parameters as $parameter) {
103+
$reflectionType = $parameter->getType();
104+
if (!$reflectionType instanceof \ReflectionNamedType) {
105+
throw new \InvalidArgumentException(sprintf('Could not resolve argument "%s" for "%s".', $parameter->getName(), $path));
106+
}
107+
$type = $reflectionType->getName();
108+
109+
switch ($type) {
110+
case ContainerConfigurator::class:
111+
$arguments[] = $containerConfigurator;
112+
break;
113+
case ContainerBuilder::class:
114+
$arguments[] = $this->container;
115+
break;
116+
case FileLoader::class:
117+
case self::class:
118+
$arguments[] = $this;
119+
break;
120+
default:
121+
try {
122+
$configBuilder = $this->configBuilder($type);
123+
} catch (InvalidArgumentException | \LogicException $e) {
124+
throw new \InvalidArgumentException(sprintf('Could not resolve argument "%s" for "%s".', $type.' '.$parameter->getName(), $path), 0, $e);
125+
}
126+
$configBuilders[] = $configBuilder;
127+
$arguments[] = $configBuilder;
128+
}
129+
}
130+
131+
$callback(...$arguments);
132+
133+
/** @var ConfigBuilderInterface $configBuilder */
134+
foreach ($configBuilders as $configBuilder) {
135+
$containerConfigurator->extension($configBuilder->getExtensionAlias(), $configBuilder->toArray());
136+
}
137+
}
138+
139+
/**
140+
* @param string $namespace FQCN string for a class implementing ConfigBuilderInterface
141+
*/
142+
private function configBuilder(string $namespace): ConfigBuilderInterface
143+
{
144+
if (!class_exists(ConfigBuilderGenerator::class)) {
145+
throw new \LogicException('You cannot use the config builder as the Config component is not installed. Try running "composer require symfony/config".');
146+
}
147+
148+
if (null === $this->generator) {
149+
throw new \LogicException('You cannot use the ConfigBuilders without providing a class implementing ConfigBuilderGeneratorInterface.');
150+
}
151+
152+
// If class exists and implements ConfigBuilderInterface
153+
if (class_exists($namespace) && is_subclass_of($namespace, ConfigBuilderInterface::class)) {
154+
return new $namespace();
155+
}
156+
157+
// If it does not start with Symfony\Config\ we dont know how to handle this
158+
if ('Symfony\\Config\\' !== substr($namespace, 0, 15)) {
159+
throw new InvalidargumentException(sprintf('Could not find or generate class "%s".', $namespace));
160+
}
161+
162+
// Try to get the extension alias
163+
$alias = Container::underscore(substr($namespace, 15, -6));
164+
165+
if (!$this->container->hasExtension($alias)) {
166+
$extensions = array_filter(array_map(function (ExtensionInterface $ext) { return $ext->getAlias(); }, $this->container->getExtensions()));
167+
throw new InvalidArgumentException(sprintf('There is no extension able to load the configuration for "%s". Looked for namespace "%s", found "%s".', $namespace, $namespace, $extensions ? implode('", "', $extensions) : 'none'));
168+
}
169+
170+
$extension = $this->container->getExtension($alias);
171+
if (!$extension instanceof ConfigurationExtensionInterface) {
172+
throw new \LogicException(sprintf('You cannot use the config builder for "%s" because the extension does not implement "%s".', $namespace, ConfigurationExtensionInterface::class));
173+
}
174+
175+
$configuration = $extension->getConfiguration([], $this->container);
176+
$loader = $this->generator->build($configuration);
177+
178+
return $loader();
179+
}
73180
}
74181

75182
/**

Tests/Fixtures/AcmeConfigBuilder.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
namespace Symfony\Component\DependencyInjection\Tests\Fixtures;
4+
5+
use Symfony\Component\Config\Builder\ConfigBuilderInterface;
6+
7+
class AcmeConfigBuilder implements ConfigBuilderInterface
8+
{
9+
private $color;
10+
11+
public function color($value)
12+
{
13+
$this->color = $value;
14+
}
15+
16+
public function toArray(): array
17+
{
18+
return [
19+
'color' => $this->color
20+
];
21+
}
22+
23+
public function getExtensionAlias(): string
24+
{
25+
return 'acme';
26+
}
27+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
parameters:
2+
acme.configs: [{ color: blue }]
3+
4+
services:
5+
service_container:
6+
class: Symfony\Component\DependencyInjection\ContainerInterface
7+
public: true
8+
synthetic: true
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
use Symfony\Component\DependencyInjection\Tests\Fixtures\AcmeConfigBuilder;
4+
5+
return static function (AcmeConfigBuilder $config) {
6+
$config->color('blue');
7+
};
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
use Symfony\Component\DependencyInjection\ContainerBuilder;
4+
use Symfony\Component\DependencyInjection\Extension\ExtensionInterface;
5+
6+
class AcmeExtension implements ExtensionInterface
7+
{
8+
public function load(array $configs, ContainerBuilder $configuration)
9+
{
10+
$configuration->setParameter('acme.configs', $configs);
11+
12+
return $configuration;
13+
}
14+
15+
public function getXsdValidationBasePath()
16+
{
17+
return false;
18+
}
19+
20+
public function getNamespace(): string
21+
{
22+
return 'http://www.example.com/schema/acme';
23+
}
24+
25+
public function getAlias(): string
26+
{
27+
return 'acme';
28+
}
29+
}

Tests/Loader/PhpFileLoaderTest.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,11 @@
1111

1212
namespace Symfony\Component\DependencyInjection\Tests\Loader;
1313

14+
require_once __DIR__.'/../Fixtures/includes/AcmeExtension.php';
15+
1416
use PHPUnit\Framework\TestCase;
1517
use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
18+
use Symfony\Component\Config\Builder\ConfigBuilderGenerator;
1619
use Symfony\Component\Config\FileLocator;
1720
use Symfony\Component\DependencyInjection\ContainerBuilder;
1821
use Symfony\Component\DependencyInjection\Dumper\PhpDumper;
@@ -60,7 +63,9 @@ public function testConfigServices()
6063
public function testConfig($file)
6164
{
6265
$fixtures = realpath(__DIR__.'/../Fixtures');
63-
$loader = new PhpFileLoader($container = new ContainerBuilder(), new FileLocator());
66+
$container = new ContainerBuilder();
67+
$container->registerExtension(new \AcmeExtension());
68+
$loader = new PhpFileLoader($container, new FileLocator(), 'prod', new ConfigBuilderGenerator(sys_get_temp_dir()));
6469
$loader->load($fixtures.'/config/'.$file.'.php');
6570

6671
$container->compile();
@@ -82,6 +87,7 @@ public function provideConfig()
8287
yield ['anonymous'];
8388
yield ['lazy_fqcn'];
8489
yield ['remove'];
90+
yield ['config_builder'];
8591
}
8692

8793
public function testAutoConfigureAndChildDefinition()

0 commit comments

Comments
 (0)