8000 [FrameworkBundle] Generate configuration classes and traits · symfony/symfony@43b5e13 · GitHub
[go: up one dir, main page]

Skip to content

Commit 43b5e13

Browse files
[FrameworkBundle] Generate configuration classes and traits
1 parent ed7dba6 commit 43b5e13

31 files changed

+1375
-17
lines changed

src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ CHANGELOG
2525
* Set `framework.rate_limiter.limiters.*.lock_factory` to `auto` by default
2626
* Deprecate `RateLimiterFactory` autowiring aliases, use `RateLimiterFactoryInterface` instead
2727
* Allow configuring compound rate limiters
28+
* Add configuration functions generation
2829

2930
7.2
3031
---

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\ConfigClassAwareBuilderGeneratorInterface;
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 ConfigClassAwareBuilderGeneratorInterface && $configurations) {
89+
$generator->buildConfigClassAndTraits($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

+21
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\ConfigClassAwareBuilderGeneratorInterface;
1718
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
1819
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
1920
use Symfony\Component\Config\Definition\ConfigurationInterface;
@@ -182,6 +183,11 @@ public function getCharset(): string
182183
$warmer->warmUp($kernel->getCacheDir(), $kernel->getBuildDir());
183184

184185
self::assertFileExists($kernel->getBuildDir().'/Symfony/Config/FrameworkConfig.php');
186+
187+
if (interface_exists(ConfigClassAwareBuilderGeneratorInterface::class)) {
188+
self::assertFileExists($kernel->getBuildDir().'/Symfony/Config/FrameworkTrait.php');
189+
self::assertFileExists($kernel->getBuildDir().'/Symfony/Config/Config.php');
190+
}
185191
}
186192

187193
public function testExtensionAddedInKernel()
@@ -222,6 +228,11 @@ public function getAlias(): string
222228

223229
self::assertFileExists($kernel->getBuildDir().'/Symfony/Config/FrameworkConfig.php');
224230
self::assertFileExists($kernel->getBuildDir().'/Symfony/Config/AppConfig.php');
231+
232+
if (interface_exists(ConfigClassAwareBuilderGeneratorInterface::class)) {
233+
self::assertFileExists($kernel->getBuildDir().'/Symfony/Config/FrameworkTrait.php');
234+
self::assertFileExists($kernel->getBuildDir().'/Symfony/Config/Config.php');
235+
}
225236
}
226237

227238
public function testKernelAsExtension()
@@ -267,6 +278,11 @@ public function getConfigTreeBuilder(): TreeBuilder
267278

268279
self::assertFileExists($kernel->getBuildDir().'/Symfony/Config/FrameworkConfig.php');
269280
self::assertFileExists($kernel->getBuildDir().'/Symfony/Config/KernelConfig.php');
281+
282+
if (interface_exists(ConfigClassAwareBuilderGeneratorInterface::class)) {
283+
self::assertFileExists($kernel->getBuildDir().'/Symfony/Config/FrameworkTrait.php');
284+
self::assertFileExists($kernel->getBuildDir().'/Symfony/Config/Config.php');
285+
}
270286
}
271287

272288
public function testExtensionsExtendedInBuildMethods()
@@ -333,6 +349,11 @@ public function addConfiguration(NodeDefinition $node): void
333349
self::assertFileExists($kernel->getBuildDir().'/Symfony/Config/Security/FirewallConfig.php');
334350
self::assertFileExists($kernel->getBuildDir().'/Symfony/Config/Security/FirewallConfig/FormLoginConfig.php');
335351
self::assertFileExists($kernel->getBuildDir().'/Symfony/Config/Security/FirewallConfig/TokenConfig.php');
352+
353+
if (interface_exists(ConfigClassAwareBuilderGeneratorInterface::class)) {
354+
self::assertFileExists($kernel->getBuildDir().'/Symfony/Config/FrameworkTrait.php');
355+
self::assertFileExists($kernel->getBuildDir().'/Symfony/Config/Config.php');
356+
}
336357
}
337358
}
338359

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+
}

src/Symfony/Component/Config/Builder/ClassBuilder.php

+9
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ class ClassBuilder
3131
private array $use = [];
3232
private array $implements = [];
3333
private bool $allowExtraKeys = false;
34+
private array $traits = [];
3435

3536
public function __construct(
3637
private string $namespace,
@@ -72,6 +73,9 @@ public function build(): string
7273

7374
$implements = [] === $this->implements ? '' : 'implements '.implode(', ', $this->implements);
7475
$body = '';
76+
foreach ($this->traits as $trait) {
77+
$body .= ' use '.$trait.";\n";
78+
}
7579
foreach ($this->properties as $property) {
7680
$body .= ' '.$property->getContent()."\n";
7781
}
@@ -107,6 +111,11 @@ public function addUse(string $class): void
107111
$this->use[$class] = true;
108112
}
109113

114+
public function addTrait(string $trait): void
115+
{
116+
$this->traits[] = '\\'.ltrim($trait, '\\');
117+
}
118+
110119
public function addImplements(string $interface): void
111120
{
112121
$this->implements[] = '\\'.ltrim($interface, '\\');

0 commit comments

Comments
 (0)
0