Skip to content

Commit 2a1d924

Browse files
feature #62129 [FrameworkBundle] Auto-generate config/reference.php to assist in writing and discovering app's configuration (nicolas-grekas)
This PR was squashed before being merged into the 7.4 branch. Discussion ---------- [FrameworkBundle] Auto-generate `config/reference.php` to assist in writing and discovering app's configuration | Q | A | ------------- | --- | Branch? | 7.4 | Bug fix? | no | New feature? | yes | Deprecations? | no | Doc PR | symfony/symfony-docs#21511 | License | MIT This PR reverts #61490 and #61885, and builds on #61894. These reverts explain a big chunk of the attached patch. This adds a compiler pass that generates a `config/reference.php` file. This file contains two classes that define array-shapes for app's and routing configuration. Part of these shapes are auto-generated from the list of bundles found in `config/bundles.php`. The `config/reference.php` file should be loaded by a new line in the "autoload" entry of composer.json files: `"classmap": ["config/"]` - recipe update pending. This means that the file should be committed. This is on purpose: as the name suggests, this file is also a config reference for human readers. Having to commit the changes is also a nice way to convey config improvements to the community - at least for ppl that review their commits ;). It also solves a discovery problem that happens with phpstan/etc having a hard time to find the classes currently generated for config builders in the cache directory. With this PR, `config/services.php` could start as such: ```php <?php namespace Symfony\Component\DependencyInjection\Loader\Configurator; return App::config([ 'services' => [ 'App\\' => [ 'resource' => '../src/' ], ], ]); ``` and `config/routes.php` would start as: ```php <?php namespace Symfony\Component\Routing\Loader\Configurator; return Routes::config([ 'controllers' => [ 'resource' => 'attributes', 'type' => 'tagged_services', ] ]); ``` The generated shapes use advanced features that are not fully supported by phpstan / phpstorm. But the gap should be closed soon. PS: https://symfony.com/blog/new-in-symfony-7-4-deprecated-xml-configuration will need an update. Commits ------- 22349f5 [FrameworkBundle] Auto-generate `config/reference.php` to assist in writing and discovering app's configuration
2 parents 742d742 + 22349f5 commit 2a1d924

File tree

78 files changed

+1369
-1109
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

78 files changed

+1369
-1109
lines changed

UPGRADE-7.4.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,15 @@ DependencyInjection
4141
* Deprecate `ExtensionInterface::getXsdValidationBasePath()` and `getNamespace()`;
4242
bundles that need to support older versions of Symfony can keep the methods
4343
but need to add the `@deprecated` annotation on them
44-
* Deprecate the fluent PHP format for semantic configuration, instantiate builders inline with the config array as argument and return them instead:
44+
* Deprecate the fluent PHP format for semantic configuration, use `$container->extension()` or return an array instead
4545
```diff
4646
-return function (AcmeConfig $config) {
4747
- $config->color('red');
4848
-}
49-
+return new AcmeConfig([
50-
+ 'color' => 'red',
49+
+return App::config([
50+
+ 'acme' => [
51+
+ 'color' => 'red',
52+
+ ],
5153
+]);
5254
```
5355

composer.json

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -197,10 +197,6 @@
197197
"Symfony\\Bridge\\Twig\\": "src/Symfony/Bridge/Twig/",
198198
"Symfony\\Bundle\\": "src/Symfony/Bundle/",
199199
"Symfony\\Component\\": "src/Symfony/Component/",
200-
"Symfony\\Config\\": [
201-
"src/Symfony/Component/DependencyInjection/Loader/Config/",
202-
"src/Symfony/Component/Routing/Loader/Config/"
203-
],
204200
"Symfony\\Runtime\\Symfony\\Component\\": "src/Symfony/Component/Runtime/Internal/"
205201
},
206202
"files": [

src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ CHANGELOG
44
7.4
55
---
66

7+
* Auto-generate `config/reference.php` to assist in writing and discovering app's configuration
78
* Auto-register routes from attributes found on controller services
89
* Add `ControllerHelper`; the helpers from AbstractController as a standalone service
910
* Allow using their name without added suffix when using `#[Target]` for custom services
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler;
13+
14+
use Symfony\Component\Config\Definition\ArrayShapeGenerator;
15+
use Symfony\Component\Config\Definition\ConfigurationInterface;
16+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
17+
use Symfony\Component\DependencyInjection\ContainerBuilder;
18+
use Symfony\Component\DependencyInjection\Extension\ConfigurationExtensionInterface;
19+
use Symfony\Component\DependencyInjection\Extension\ExtensionInterface;
20+
use Symfony\Component\DependencyInjection\Loader\Configurator\AppReference;
21+
use Symfony\Component\Routing\Loader\Configurator\RoutesReference;
22+
23+
/**
24+
* @internal
25+
*/
26+
class PhpConfigReferenceDumpPass implements CompilerPassInterface
27+
{
28+
private const REFERENCE_TEMPLATE = <<<'PHP'
29+
<?php
30+
31+
// This file is auto-generated and is for apps only. Bundles SHOULD NOT rely on its content.
32+
33+
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
34+
35+
{APP_TYPES}
36+
final class App extends AppReference
37+
{
38+
{APP_PARAM}
39+
public static function config(array $config): array
40+
{
41+
return parent::config($config);
42+
}
43+
}
44+
45+
namespace Symfony\Component\Routing\Loader\Configurator;
46+
47+
{ROUTES_TYPES}
48+
final class Routes extends RoutesReference
49+
{
50+
{ROUTES_PARAM}
51+
public static function config(array $config): array
52+
{
53+
return parent::config($config);
54+
}
55+
}
56+
57+
PHP;
58+
59+
private const WHEN_ENV_APP_TEMPLATE = <<<'PHPDOC'
60+
61+
* "when@{ENV}"?: array{
62+
* imports?: ImportsConfig,
63+
* parameters?: ParametersConfig,
64+
* services?: ServicesConfig,{SHAPE}
65+
* },
66+
PHPDOC;
67+
68+
private const ROUTES_TYPES_TEMPLATE = <<<'PHPDOC'
69+
70+
* @psalm-type RoutesConfig = array{{SHAPE}
71+
* ...<string, RouteConfig|ImportConfig|AliasConfig>
72+
* }
73+
*/
74+
PHPDOC;
75+
76+
private const WHEN_ENV_ROUTES_TEMPLATE = <<<'PHPDOC'
77+
78+
* "when@{ENV}"?: array<string, RouteConfig|ImportConfig|AliasConfig>,
79+
PHPDOC;
80+
81+
public function __construct(
82+
private string $referenceFile,
83+
private array $bundlesDefinition,
84+
) {
85+
}
86+
87+
public function process(ContainerBuilder $container): void
88+
{
89+
$knownEnvs = $container->hasParameter('.container.known_envs') ? $container->getParameter('.container.known_envs') : [$container->getParameter('kernel.environment')];
90+
$knownEnvs = array_unique($knownEnvs);
91+
sort($knownEnvs);
92+
$extensionsPerEnv = [];
93+
$appTypes = '';
94+
95+
$anyEnvExtensions = [];
96+
foreach ($this->bundlesDefinition as $bundle => $envs) {
97+
if (!$extension = (new $bundle())->getContainerExtension()) {
98+
continue;
99+
}
100+
if (!$configuration = $this->getConfiguration($extension, $container)) {
101+
continue;
102+
}
103+
$anyEnvExtensions[$bundle] = $extension;
104+
$type = $this->camelCase($extension->getAlias()).'Config';
105+
$appTypes .= \sprintf("\n * @psalm-type %s = %s", $type, ArrayShapeGenerator::generate($configuration->getConfigTreeBuilder()->buildTree()));
106+
107+
foreach ($knownEnvs as $env) {
108+
if ($envs[$env] ?? $envs['all'] ?? false) {
109+
$extensionsPerEnv[$env][] = $extension;
110+
} else {
111+
unset($anyEnvExtensions[$bundle]);
112+
}
113+
}
114+
}
115+
krsort($extensionsPerEnv);
116+
117+
$r = new \ReflectionClass(AppReference::class);
118+
119+
if (false === $i = strpos($phpdoc = $r->getDocComment(), "\n */")) {
120+
throw new \LogicException(\sprintf('Cannot insert config shape in "%s".', AppReference::class));
121+
}
122+
$appTypes = substr_replace($phpdoc, $appTypes, $i, 0);
123+
124+
if (false === $i = strpos($phpdoc = $r->getMethod('config')->getDocComment(), "\n * ...<string, ExtensionType|array{")) {
125+
throw new \LogicException(\sprintf('Cannot insert config shape in "%s".', AppReference::class));
126+
}
127+
$appParam = substr_replace($phpdoc, $this->getShapeForExtensions($anyEnvExtensions, $container), $i, 0);
128+
$i += \strlen($appParam) - \strlen($phpdoc);
129+
130+
foreach ($extensionsPerEnv as $env => $extensions) {
131+
$appParam = substr_replace($appParam, strtr(self::WHEN_ENV_APP_TEMPLATE, [
132+
'{ENV}' => $env,
133+
'{SHAPE}' => $this->getShapeForExtensions($extensions, $container, ' '),
134+
]), $i, 0);
135+
}
136+
137+
$r = new \ReflectionClass(RoutesReference::class);
138+
139+
if (false === $i = strpos($phpdoc = $r->getDocComment(), "\n * @psalm-type RoutesConfig = ")) {
140+
throw new \LogicException(\sprintf('Cannot insert config shape in "%s".', RoutesReference::class));
141+
}
142+
$routesTypes = '';
143+
foreach ($knownEnvs as $env) {
144+
$routesTypes .= strtr(self::WHEN_ENV_ROUTES_TEMPLATE, ['{ENV}' => $env]);
145+
}
146+
if ('' !== $routesTypes) {
147+
$routesTypes = strtr(self::ROUTES_TYPES_TEMPLATE, ['{SHAPE}' => $routesTypes]);
148+
$routesTypes = substr_replace($phpdoc, $routesTypes, $i);
149+
}
150+
151+
$configReference = strtr(self::REFERENCE_TEMPLATE, [
152+
'{APP_TYPES}' => $appTypes,
153+
'{APP_PARAM}' => $appParam,
154+
'{ROUTES_TYPES}' => $routesTypes,
155+
'{ROUTES_PARAM}' => $r->getMethod('config')->getDocComment(),
156+
]);
157+
158+
$dir = \dirname($this->referenceFile);
159+
if (is_dir($dir) && is_writable($dir) && (!is_file($this->referenceFile) || file_get_contents($this->referenceFile) !== $configReference)) {
160+
file_put_contents($this->referenceFile, $configReference);
161+
}
162+
}
163+
164+
private function camelCase(string $input): string
165+
{
166+
$output = ucfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $input))));
167+
168+
return preg_replace('#\W#', '', $output);
169+
}
170+
171+
private function getConfiguration(ExtensionInterface $extension, ContainerBuilder $container): ?ConfigurationInterface
172+
{
173+
return match (true) {
174+
$extension instanceof ConfigurationInterface => $extension,
175+
$extension instanceof ConfigurationExtensionInterface => $extension->getConfiguration([], $container),
176+
default => null,
177+
};
178+
}
179+
180+
private function getShapeForExtensions(array $extensions, ContainerBuilder $container, string $indent = ''): string
181+
{
182+
$shape = '';
183+
foreach ($extensions as $extension) {
184+
if ($this->getConfiguration($extension, $container)) {
185+
$type = $this->camelCase($extension->getAlias()).'Config';
186+
$shape .= \sprintf("\n * %s%s?: %s,", $indent, $extension->getAlias(), $type);
187+
}
188+
}
189+
190+
return $shape;
191+
}
192+
}

src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AssetsContextPass;
1717
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\ContainerBuilderDebugDumpPass;
1818
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\ErrorLoggerCompilerPass;
19+
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\PhpConfigReferenceDumpPass;
1920
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\ProfilerPass;
2021
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\RemoveUnusedSessionMarshallingHandlerPass;
2122
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TestServiceContainerRealRefPass;
@@ -148,7 +149,10 @@ public function build(ContainerBuilder $container): void
148149
]);
149150
}
150151

151-
$container->addCompilerPass(new AssetsContextPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION);
152+
if ($container->hasParameter('.kernel.config_dir') && $container->hasParameter('.kernel.bundles_definition')) {
153+
$container->addCompilerPass(new PhpConfigReferenceDumpPass($container->getParameter('.kernel.config_dir').'/reference.php', $container->getParameter('.kernel.bundles_definition')));
154+
}
155+
$container->addCompilerPass(new AssetsContextPass());
152156
$container->addCompilerPass(new LoggerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -32);
153157
$container->addCompilerPass(new RegisterControllerArgumentLocatorsPass());
154158
$container->addCompilerPass(new RemoveEmptyControllerArgumentLocatorsPass(), PassConfig::TYPE_BEFORE_REMOVING);

src/Symfony/Bundle/FrameworkBundle/Kernel/MicroKernelTrait.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,4 +230,27 @@ public function loadRoutes(LoaderInterface $loader): RouteCollection
230230

231231
return $collection;
232232
}
233+
234+
/**
235+
* Returns the kernel parameters.
236+
*
237+
* @return array<string, array|bool|string|int|float|\UnitEnum|null>
238+
*/
239+
protected function getKernelParameters(): array
240+
{
241+
$parameters = parent::getKernelParameters();
242+
$bundlesPath = $this->getBundlesPath();
243+
$bundlesDefinition = !is_file($bundlesPath) ? [FrameworkBundle::class => ['all' => true]] : require $bundlesPath;
244+
$knownEnvs = [$this->environment => true];
245+
246+
foreach ($bundlesDefinition as $envs) {
247+
$knownEnvs += $envs;
248+
}
249+
unset($knownEnvs['all']);
250+
$parameters['.container.known_envs'] = array_keys($knownEnvs);
251+
$parameters['.kernel.config_dir'] = $this->getConfigDir();
252+
$parameters['.kernel.bundles_definition'] = $bundlesDefinition;
253+
254+
return $parameters;
255+
}
233256
}

0 commit comments

Comments
 (0)