8000 [FrameworkBundle] Generate configuration functions · symfony/symfony@91086d0 · GitHub
[go: up one dir, main page]

Skip to content

Commit 91086d0

Browse files
[FrameworkBundle] Generate configuration functions
1 parent b28e597 commit 91086d0

29 files changed

+1221
-17
lines changed
9E81

src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ CHANGELOG
1313
* Add `framework.validation.disable_translation` option
1414
* Add support for signal plain name in the `messenger.stop_worker_on_signals` configuration
1515
* Deprecate the `framework.validation.cache` option
16+
* Add configuration functions generation
1617

1718
7.2
1819
---

src/Symfony/Bundle/FrameworkBundle/CacheWarmer/ConfigBuilderCacheWarmer.php

+17-8
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
use Psr\Log\LoggerInterface;
1515
use Symfony\Component\Config\Builder\ConfigBuilderGenerator;
16-
use Symfony\Component\Config\Builder\ConfigBuilderGeneratorInterface;
16+
use Symfony\Component\Config\Builder\ConfigFunctionAwareBuilderGeneratorInterface;
1717
use Symfony\Component\Config\Definition\ConfigurationInterface;
1818
use Symfony\Component\DependencyInjection\Container;
1919
use Symfony\Component\DependencyInjection\ContainerBuilder;
@@ -29,6 +29,7 @@
2929
* Generate all config builders.
3030
*
3131
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
32+
* @author Alexandre Daubois <alex.daubois@gmail.com>
3233
*
3334
* @final since Symfony 7.1
3435
*/
@@ -68,19 +69,31 @@ public function warmUp(string $cacheDir, ?string $buildDir = null): array
6869
}
6970
}
7071

72+
$configurations = [];
7173
foreach ($extensions as $extension) {
74+
if (null === $configuration = $this->getConfigurationFromExtension($extension)) {
75+
continue;
76+
}
77+
78+
$alias = lcfirst(str_replace('_', '', ucwords($extension->getAlias(), '_')));
79+
$configurations[$alias] = $configuration;
80+
7281
try {
73-
$this->dumpExtension($extension, $generator);
82+
$generator->build($configurations[$alias]);
7483
} catch (\Exception $e) {
7584
$this->logger?->warning('Failed to generate ConfigBuilder for extension {extensionClass}: '.$e->getMessage(), ['exception' => $e, 'extensionClass' => $extension::class]);
7685
}
7786
}
7887

88+
if ($generator instanceof ConfigFunctionAwareBuilderGeneratorInterface && $configurations) {
89+
$generator->buildConfigFunction($configurations)();
90+
}
91+
7992
// No need to preload anything
8093
return [];
8194
}
8295

83-
private function dumpExtension(ExtensionInterface $extension, ConfigBuilderGeneratorInterface $generator): void
96+
private function getConfigurationFromExtension(ExtensionInterface $extension): ?ConfigurationInterface
8497
{
8598
$configuration = null;
8699
if ($extension instanceof ConfigurationInterface) {
@@ -90,11 +103,7 @@ private function dumpExtension(ExtensionInterface $extension, ConfigBuilderGener
90103
$configuration = $extension->getConfiguration([], new ContainerBuilder($container instanceof Container ? new ContainerBag($container) : new ParameterBag()));
91104
}
92105

93-
if (!$configuration) {
94-
return;
95-
}
96-
97-
$generator->build($configuration);
106+
return $configuration;
98107
}
99108

100109
public function isOptional(): bool

src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/ConfigBuilderCacheWarmerTest.php

+24
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Symfony\Bundle\FrameworkBundle\CacheWarmer\ConfigBuilderCacheWarmer;
1515
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
1616
use Symfony\Bundle\FrameworkBundle\Tests\TestCase;
17+
use Symfony\Component\Config\Builder\ConfigFunctionAwareBuilderGeneratorInterface;
1718
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
1819
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
1920
use Symfony\Component\Config\Definition\ConfigurationInterface;
@@ -31,6 +32,9 @@
3132
use Symfony\Component\HttpKernel\Kernel;
3233
use Symfony\Component\HttpKernel\KernelInterface;
3334

35+
/**
36+
* @runTestsInSeparateProcesses because the loaded-state of the "config()" function is global
37+
*/
3438
class ConfigBuilderCacheWarmerTest extends TestCase
3539
{
3640
private string $varDir;
@@ -182,6 +186,11 @@ public function getCharset(): string
182186
$warmer->warmUp($kernel->getCacheDir(), $kernel->getBuildDir());
183187

184188
self::assertFileExists($kernel->getBuildDir().'/Symfony/Config/FrameworkConfig.php');
189+
190+
if (interface_exists(ConfigFunctionAwareBuilderGeneratorInterface::class)) {
191+
self::assertFileExists($kernel->getBuildDir().'/Symfony/Config/config.php');
192+
self::assertTrue(\function_exists('\Symfony\Config\framework'), 'the framework() function should be generated and loaded');
193+
}
185194
}
186195

187196
public function testExtensionAddedInKernel()
@@ -222,6 +231,11 @@ public function getAlias(): string
222231

223232
self::assertFileExists($kernel->getBuildDir().'/Symfony/Config/FrameworkConfig.php');
224233
self::assertFileExists($kernel->getBuildDir().'/Symfony/Config/AppConfig.php');
234+
235+
if (interface_exists(ConfigFunctionAwareBuilderGeneratorInterface::class)) {
236+
self::assertFileExists($kernel->getBuildDir().'/Symfony/Config/config.php');
237+
self::assertTrue(\function_exists('\Symfony\Config\framework'), 'the framework() function should be generated and loaded');
238+
}
225239
}
226240

227241
public function testKernelAsExtension()
@@ -267,6 +281,11 @@ public function getConfigTreeBuilder(): TreeBuilder
267281

268282
self::assertFileExists($kernel->getBuildDir().'/Symfony/Config/FrameworkConfig.php');
269283
self::assertFileExists($kernel->getBuildDir().'/Symfony/Config/KernelConfig.php');
284+
285+
if (interface_exists(ConfigFunctionAwareBuilderGeneratorInterface::class)) {
286+
self::assertFileExists($kernel->getBuildDir().'/Symfony/Config/config.php');
287+
self::assertTrue(\function_exists('\Symfony\Config\framework'), 'the framework() function should be generated and loaded');
288+
}
270289
}
271290

272291
public function testExtensionsExtendedInBuildMethods()
@@ -333,6 +352,11 @@ public function addConfiguration(NodeDefinition $node): void
333352
self::assertFileExists($kernel->getBuildDir().'/Symfony/Config/Security/FirewallConfig.php');
334353
self::assertFileExists($kernel->getBuildDir().'/Symfony/Config/Security/FirewallConfig/FormLoginConfig.php');
335354
self::assertFileExists($kernel->getBuildDir().'/Symfony/Config/Security/FirewallConfig/TokenConfig.php');
355+
356+
if (interface_exists(ConfigFunctionAwareBuilderGeneratorInterface::class)) {
357+
self::assertFileExists($kernel->getBuildDir().'/Symfony/Config/config.php');
358+
self::assertTrue(\function_exists('\Symfony\Config\framework'), 'the framework() function should be generated and loaded');
359+
}
336360
}
337361
}
338362

< 325D td data-grid-cell-id="diff-ecd29623aefc0d05358573ce718e8e5d63a80507e4df7991083b6e1c4730e058-empty-81-0" data-selected="false" role="gridcell" style="background-color:var(--diffBlob-additionNum-bgColor, var(--diffBlob-addition-bgColor-num));text-align:center" tabindex="-1" valign="top" class="focusable-grid-cell diff-line-number position-relative left-side">
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
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\Component\Config\Builder;
13+
14+
use Symfony\Component\Config\Definition\ArrayNode;
15+
use Symfony\Component\Config\Definition\BaseNode;
16+
use Symfony\Component\Config\Definition\BooleanNode;
17+
use Symfony\Component\Config\Definition\EnumNode;
18+
use Symfony\Component\Config\Definition\FloatNode;
19+
use Symfony\Component\Config\Definition\IntegerNode;
20+
use Symfony\Component\Config\Definition\NodeInterface;
21+
use Symfony\Component\Config\Definition\NumericNode;
22+
use Symfony\Component\Config\Definition\PrototypedArrayNode;
23+
use Symfony\Component\Config\Definition\ScalarNode;
24+
use Symfony\Component\Config\Definition\StringNode;
25+
use Symfony\Component\Config\Definition\VariableNode;
26+
27+
/**
28+
* @author Alexandre Daubois <alex.daubois@gmail.com>
29+
*
30+
* @internal
31+
*/
32+
final class ArrayShapeGenerator
33+
{
34+
public const FORMAT_PHPDOC = 'phpdoc';
35+
36+
public static function generate(ArrayNode $node): string
37+
{
38+
return self::prependPhpDocWithStar(self::doGeneratePhpDoc($node));
39+
}
40+
41+
private static function doGeneratePhpDoc(NodeInterface $node, int $nestingLevel = 1): string
42+
{
43+
if (!$node instanceof ArrayNode) {
44+
return $node->getName();
45+
}
46+
47+
if ($node instanceof PrototypedArrayNode) {
48+
$isHashmap = (bool) $node->getKeyAttribute();
49+
50+
$prototype = $node->getPrototype();
51+
if ($prototype instanceof ArrayNode) {
52+
return 'array<'.($isHashmap ? 'string, ' : '').self::doGeneratePhpDoc($prototype, $nestingLevel).'>';
53+
}
54+
55+
return 'array<'.($isHashmap ? 'string, ' : '').self::handleScalarNode($prototype).'>';
56+
}
57+
58+
if (!($children = $node->getChildren()) && !$node->getParent() instanceof PrototypedArrayNode) {
59+
return 'array<array-key, mixed>';
60+
}
61+
62+
$arrayShape = \sprintf("array{%s\n", self::generateInlinePhpDocForNode($node));
63+
64+
/** @var NodeInterface $child */
65+
foreach ($children as $child) {
66+
$arrayShape .= str_repeat(' ', $nestingLevel * 4).self::dumpNodeKey($child).': ';
67+
68+
if ($child instanceof PrototypedArrayNode) {
69+
$isHashmap = (bool) $child->getKeyAttribute();
70+
71+
$arrayShape .= 'array<'.($isHashmap ? 'string, ' : '').self::handleNode($child->getPrototype(), $nestingLevel, self::FORMAT_PHPDOC).'>';
72+
} else {
73+
$arrayShape .= self::handleNode($child, $nestingLevel, self::FORMAT_PHPDOC);
74+
}
75+
76+
$arrayShape .= \sprintf(",%s\n", !$child instanceof ArrayNode ? self::generateInlinePhpDocForNode($child) : '');
77+
}
78+
79+
return $arrayShape.str_repeat(' ', 4 * ($nestingLevel - 1)).'}';
80+
}
81+
82+
private static function dumpNodeKey(NodeInterface $node): string
83+
{
84+
$name = $node->getName();
85+
$quoted = str_starts_with($name, '@')
86+
|| \in_array(strtolower($name), ['int', 'float', 'bool', 'null', 'scalar'], true)
87+
|| strpbrk($name, '\'"');
88+
89+
if ($quoted) {
90+
$name = "'".addslashes($name)."'";
91+
}
92+
93+
return $name.($node->isRequired() ? '' : '?');
94+
}
95+
96+
private static function handleNumericNode(NumericNode $node): string
97+
{
98+
$min = $node->getMin() ?? 'min';
99+
$max = $node->getMax() ?? 'max';
100+
101+
if ($node instanceof IntegerNode) {
102+
return \sprintf('int<%s, %s>', $min, $max);
103+
} elseif ($node instanceof FloatNode) {
104+
return 'float';
105+
}
106+
107+
return \sprintf('int<%s, %s>|float', $min, $max);
108+
}
109+
110+
private static function prependPhpDocWithStar(string $shape): string
111+
{
112+
return str_replace("\n", "\n * ", $shape);
113+
}
114+
115+
private static function generateInlinePhpDocForNode(BaseNode $node): string
116+
{
117+
$hasContent = false;
118+
$comment = ' // ';
119+
120+
if ($node->hasDefaultValue() || $node->getInfo() || $node->isDeprecated()) {
121+
if ($node->isDeprecated()) {
122+
$hasContent = true;
123+
$comment .= 'Deprecated: '.$node->getDeprecation($node->getName(), $node->getPath())['message'].' ';
124+
}
125+
126+
if ($info = $node->getInfo()) {
127+
$hasContent = true;
128+
$comment .= $info.' ';
129+
}
130+
131+
if ($node->hasDefaultValue() && !\is_array($defaultValue = $node->getDefaultValue())) {
132+
$hasContent = true;
133+
$comment .= 'Default: '.json_encode($defaultValue, \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE | \JSON_PRESERVE_ZERO_FRACTION);
134+
}
135+
}
136+
137+
return $hasContent ? rtrim($comment) : '';
138+
}
139+
140+
private static function handleNode(NodeInterface $node, int $nestingLevel, string $format): string
141+
{
142+
if ($node instanceof ArrayNode) {
143+
return self::doGeneratePhpDoc($node, 1 + $nestingLevel);
144+
}
145+
146+
return self::handleScalarNode($node);
147+
}
148+
149+
private static function handleScalarNode(NodeInterface $node): string
150+
{
151+
return match (true) {
152+
$node instanceof BooleanNode => 'bool',
153+
$node instanceof StringNode => 'string',
154+
$node instanceof NumericNode => self::handleNumericNode($node),
155+
$node instanceof EnumNode => $node->getPermissibleValues('|'),
156+
$node instanceof ScalarNode => 'string|int|float|bool',
157+
$node instanceof VariableNode => 'mixed',
158+
};
159+
}
160+
}

0 commit comments

Comments
 (0)
0