Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit e394d68

Browse filesBrowse files
[FrameworkBundle] Generate configuration classes and traits
1 parent ed7dba6 commit e394d68
Copy full SHA for e394d68
Expand file treeCollapse file tree

31 files changed

+1375
-17
lines changed

‎src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md
+1Lines changed: 1 addition & 0 deletions
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 class and config traits generation
2829

2930
7.2
3031
---

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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/CacheWarmer/ConfigBuilderCacheWarmer.php
+17-8Lines changed: 17 additions & 8 deletions
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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/ConfigBuilderCacheWarmerTest.php
+21Lines changed: 21 additions & 0 deletions
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

+160Lines changed: 160 additions & 0 deletions
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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Config/Builder/ClassBuilder.php
+9Lines changed: 9 additions & 0 deletions
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)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.