Skip to content

Commit 493b781

Browse files
[FrameworkBundle] Auto-generate config/reference.php to assist in writing and discovering app's configuration
1 parent 742d742 commit 493b781

File tree

77 files changed

+1245
-1097
lines changed

Some content is hidden

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

77 files changed

+1245
-1097
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: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\Loader\Configurator\AppReference;
20+
21+
/**
22+
* @internal
23+
*/
24+
class PhpConfigReferenceDumpPass implements CompilerPassInterface
25+
{
26+
private const REFERENCE_TEMPLATE = <<<'EOPHP'
27+
<?php
28+
29+
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
30+
31+
/**
32+
* This class provides array-shapes for configuring the services and bundles of an application.
33+
*
34+
* Services declared with the config() method are autowired and autoconfigured by default.
35+
*
36+
* All this is auto-generated and is for apps only. Bundles SHOULD NOT rely on it.
37+
*
38+
* Example:
39+
*
40+
* ```php
41+
* // config/services.php
42+
* namespace Symfony\Component\DependencyInjection\Loader\Configurator;
43+
*
44+
* return App::config([
45+
* 'services' => [
46+
* 'App\\' => [
47+
* 'resource' => '../src/',
48+
* ],
49+
* ],
50+
* ]);
51+
* ```
52+
*
53+
* @psalm-import-type ImportsConfig from AppReference
54+
* @psalm-import-type ParametersConfig from AppReference
55+
* @psalm-import-type ServicesConfig from AppReference
56+
*{APP_TYPES}
57+
*/
58+
final class App extends AppReference
59+
{
60+
{APP_SHAPE}
61+
public static function config(array $config): array
62+
{
63+
return parent::config($config);
64+
}
65+
}
66+
67+
namespace Symfony\Component\Routing\Loader\Configurator;
68+
69+
/**
70+
* This class provides array-shapes for configuring the routes of an application.
71+
*
72+
* All this is auto-generated and is for apps only. Bundles SHOULD NOT rely on it.
73+
*
74+
* Example:
75+
*
76+
* ```php
77+
* // config/routes.php
78+
* namespace Symfony\Component\Routing\Loader\Configurator;
79+
*
80+
* return Routes::config([
81+
* 'controllers' => [
82+
* 'resource' => 'attributes',
83+
* 'type' => 'tagged_services',
84+
* ],
85+
* ]);
86+
* ```
87+
*
88+
* @psalm-import-type RouteConfig from RoutesReference
89+
* @psalm-import-type ImportConfig from RoutesReference
90+
* @psalm-import-type AliasConfig from RoutesReference
91+
*
92+
* @psalm-type RoutesConfig = array{{ROUTES_SHAPE}
93+
* ...<string, ImportConfig|RouteConfig|AliasConfig>
94+
* }
95+
*/
96+
final class Routes extends RoutesReference
97+
{
98+
/**
99+
* @param RoutesConfig $config
100+
*
101+
* @return RoutesConfig
102+
*/
103+
public static function config(array $config): array
104+
{
105+
return parent::config($config);
106+
}
107+
}
108+
109+
EOPHP;
110+
111+
public function process(ContainerBuilder $container): void
112+
{
113+
if (!$container->hasParameter('.kernel.config_dir')) {
114+
return;
115+
}
116+
117+
$knownEnvs = [];
118+
if ($container->hasParameter('.container.known_envs')) {
119+
$knownEnvs = array_flip($container->getParameter('.container.known_envs'));
120+
}
121+
if ($container->hasParameter('.kernel.known_envs')) {
122+
$knownEnvs += array_flip($container->getParameter('.kernel.known_envs'));
123+
}
124+
$knownEnvs = array_keys($knownEnvs);
125+
sort($knownEnvs);
126+
127+
if ($container->hasParameter('.kernel.all_bundles')) {
128+
$allBundles = $container->getParameter('.kernel.all_bundles');
129+
foreach ($allBundles as $bundle => $envs) {
130+
if (!$extension = (new $bundle())->getContainerExtension()) {
131+
continue;
132+
}
133+
$extensions[$bundle] = $extension;
134+
}
135+
} else {
136+
$extensions = $container->getExtensions();
137+
}
138+
139+
$types = '';
140+
$genericShape = '';
141+
foreach ($this->getConfigurations($extensions, $container) as $name => $configuration) {
142+
$type = $this->camelCase($name).'Config';
143+
$types .= \sprintf("\n * @psalm-type %s = %s", $type, ArrayShapeGenerator::generate($configuration->getConfigTreeBuilder()->buildTree()));
144+
$genericShape .= \sprintf("\n %s?: %s,", $name, $type);
145+
}
146+
147+
$shape = str_replace("\n", "\n * ", $genericShape);
148+
$phpdoc = (new \ReflectionClass(AppReference::class))->getMethod('config')->getDocComment();
149+
150+
if ($phpdoc === $shape = str_replace("\n * ...<string, ExtensionConfig>,", $shape, $phpdoc)) {
151+
throw new \LogicException(\sprintf('Cannot find insertion point for config shape in "%s".', AppReference::class));
152+
}
153+
$phpdoc = $shape;
154+
155+
$shape = str_replace("\n", "\n * ", $genericShape);
156+
157+
if ($phpdoc === $shape = str_replace("\n * ...<string, ExtensionConfig>,", $shape, $phpdoc)) {
158+
throw new \LogicException(\sprintf('Cannot find insertion point for config shape in "%s".', AppReference::class));
159+
}
160+
$phpdoc = $shape;
161+
$routesShape = '';
162+
163+
if ($knownEnvs) {
164+
if ($phpdoc === $shape = preg_replace('{\.\.\.<string, array\{ .* when@%env% .*}', \sprintf("...<'when@%s', array{", implode("'|'when@", $knownEnvs)), $phpdoc)) {
165+
throw new \LogicException(\sprintf('Cannot find insertion point for config shape in "%s".', AppReference::class));
166+
}
167+
$phpdoc = $shape;
168+
169+
foreach ($knownEnvs as $env) {
170+
$routesShape .= "\n * 'when@{$env}'?: array<string, RouteConfig|ImportConfig|AliasConfig>,";
171+
}
172+
}
173+
174+
$configReference = strtr(self::REFERENCE_TEMPLATE, [
175+
'{APP_TYPES}' => $types,
176+
'{APP_SHAPE}' => $shape,
177+
'{ROUTES_SHAPE}' => $routesShape,
178+
]);
179+
180+
// Ignore errors when writing to the file
181+
@file_put_contents($container->getParameter('.kernel.config_dir').'/reference.php', $configReference);
182+
}
183+
184+
private function camelCase(string $input): string
185+
{
186+
$output = ucfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $input))));
187+
188+
return preg_replace('#\W#', '', $output);
189+
}
190+
191+
private function getConfigurations(array $extensions, ContainerBuilder $container): array
192+
{
193+
$configurations = [];
194+
foreach ($extensions as $extension) {
195+
if (null !== $configuration = match (true) {
196+
$extension instanceof ConfigurationInterface => $extension,
197+
$extension instanceof ConfigurationExtensionInterface => $extension->getConfiguration([], $container),
198+
default => null,
199+
}) {
200+
$configurations[$extension->getAlias()] = $configuration;
201+
}
202+
}
203+
204+
return $configurations;
205+
}
206+
}

src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php

Lines changed: 3 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,8 @@ public function build(ContainerBuilder $container): void
148149
]);
149150
}
150151

151-
$container->addCompilerPass(new AssetsContextPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION);
152+
$container->addCompilerPass(new PhpConfigReferenceDumpPass());
153+
$container->addCompilerPass(new AssetsContextPass());
152154
$container->addCompilerPass(new LoggerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -32);
153155
$container->addCompilerPass(new RegisterControllerArgumentLocatorsPass());
154156
$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+
$allBundles = !is_file($bundlesPath) ? [FrameworkBundle::class => ['all' => true]] : require $bundlesPath;
244+
$knownEnvs = [$this->environment => true];
245+
246+
foreach ($allBundles as $envs) {
247+
$knownEnvs += $envs;
248+
}
249+
unset($knownEnvs['all']);
250+
$parameters['.kernel.config_dir'] = $this->getConfigDir();
251+
$parameters['.kernel.known_envs'] = array_keys($knownEnvs);
252+
$parameters['.kernel.all_bundles'] = $allBundles;
253+
254+
return $parameters;
255+
}
233256
}

0 commit comments

Comments
 (0)