8000 [FrameworkBundle] Add the `config()` function · symfony/symfony@1f9c0b0 · GitHub
[go: up one dir, main page]

Skip to content

Commit 1f9c0b0

Browse files
[FrameworkBundle] Add the config() function
1 parent 3b5f623 commit 1f9c0b0

25 files changed

+1247
-14
lines changed

src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md

< F438 button data-component="IconButton" type="button" class="prc-Button-ButtonBase-c50BI ml-1 flex-shrink-0 prc-Button-IconButton-szpyj" data-loading="false" data-no-visuals="true" data-size="medium" data-variant="invisible" aria-describedby=":R1qdjmlab:-loading-announcement" aria-labelledby=":R2djmlab:">
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
7.3
5+
---
6+
7+
* Add the `Symfony\Config\config()` function
8+
49
7.2
510
---
611

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

Lines changed: 16 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\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,30 @@ 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+
$configurations[$extension->getAlias()] = $configuration;
79+
7280
try {
73-
$this->dumpExtension($extension, $generator);
81+
$generator->build($configurations[$extension->getAlias()]);
7482
} catch (\Exception $e) {
7583
$this->logger?->warning('Failed to generate ConfigBuilder for extension {extensionClass}: '.$e->getMessage(), ['exception' => $e, 'extensionClass' => $extension::class]);
7684
}
7785
}
7886

87+
if (class_exists(ConfigFunctionAwareBuilderGeneratorInterface::class) && $configurations) {
88+
$generator->buildConfigFunction($configurations);
89+
}
90+
7991
// No need to preload anything
8092
return [];
8193
}
8294

83-
private function dumpExtension(ExtensionInterface $extension, ConfigBuilderGeneratorInterface $generator): void
95+
private function getConfigurationFromExtension(ExtensionInterface $extension): ?ConfigurationInterface
8496
{
8597
$configuration = null;
8698
if ($extension instanceof ConfigurationInterface) {
@@ -90,11 +102,7 @@ private function dumpExtension(ExtensionInterface $extension, ConfigBuilderGener
90102
$configuration = $extension->getConfiguration([], new ContainerBuilder($container instanceof Container ? new ContainerBag($container) : new ParameterBag()));
91103
}
92104

93-
if (!$configuration) {
94-
return;
95-
}
96-
97-
$generator->build($configuration);
105+
return $configuration;
98106
}
99107

100108
public function isOptional(): bool
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
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\ScalarNode;
23+
use Symfony\Component\Config\Definition\StringNode;
24+
use Symfony\Component\Config\Definition\VariableNode;
25+
26+
/**
27+
* @author Alexandre Daubois <alex.daubois@gmail.com>
28+
*
29+
* @internal
30+
*/
31+
final class ArrayShapeGenerator
32+
{
33+
public const FORMAT_PHPDOC = 'phpdoc';
34+
public const FORMAT_JETBRAINS_ATTRIBUTE = 'jetbrains_attribute';
35+
36+
/**
37+
* @param self::FORMAT_* $format
38+
*/
39+
public static function generate(ArrayNode $node, string $format): string
40+
{
41+
if (self::FORMAT_PHPDOC === $format) {
42+
return static::prependPhpDocWithStar(static::doGeneratePhpDoc($node));
43+
}
44+
45+
if (self::FORMAT_JETBRAINS_ATTRIBUTE === $format) {
46+
return static::doGenerateJetBrainsArrayShape($node);
47+
}
48+
49+
throw new \LogicException(\sprintf('Unsupported format to generate array shape. Expected one of "%s", got "%s".', implode('", "', [self::FORMAT_PHPDOC, self::FORMAT_JETBRAINS_ATTRIBUTE]), $format));
50+
}
51+
52+
private static function doGeneratePhpDoc(NodeInterface $node, int $nestingLevel = 1): string
53+
{
54+
if (!$node instanceof ArrayNode) {
55+
return $node->getName();
56+
}
57+
58+
$arrayShape = 'array';
59+
60+
if (!$children = $node->getChildren()) {
61+
return $arrayShape.'<array-key, mixed>';
62+
}
63+
64+
$arrayShape .= '{'.\PHP_EOL;
65+
66+
/** @var NodeInterface $child */
67+
foreach ($children as $child) {
68+
$arrayShape .= str_repeat(' ', $nestingLevel * 4).static::dumpNodeKey($child).': ';
69+
70+
if ($child instanceof ArrayNode) {
71+
$arrayShape .= static::doGeneratePhpDoc($child, 1 + $nestingLevel);
72+
} else {
73+
$arrayShape .= static::handleNodeType($child);
74+
}
75+
76+
$arrayShape .= ','.\PHP_EOL;
77+
}
78+
79+
return $arrayShape.str_repeat(' ', 4 * ($nestingLevel - 1)).'}';
80+
}
81+
82+
private static function doGenerateJetBrainsArrayShape(NodeInterface $node, int $nestingLevel = 1): string
83+
{
84+
if (!$node instanceof ArrayNode) {
85+
return $node->getName();
86+
}
87+
88+
$children = $node->getChildren();
89+
90+
$shape = '';
91+
if (1 === $nestingLevel) {
92+
$shape = '#[ArrayShape(';
93+
} elseif (!$children) {
94+
return "'array<array-key, mixed>'";
95+
}
96+
97+
$shape .= '['.\PHP_EOL;
98+
99+
/** @var BaseNode $child */
100+
foreach ($children as $child) {
101+
$shape .= \sprintf("%s'%s' => ", str_repeat(' ', 4 * $nestingLevel), $child->getName());
102+
if ($child instanceof ArrayNode) {
103+
$shape .= static::doGenerateJetBrainsArrayShape($child, 1 + $nestingLevel);
104+
} else {
105+
$shape .= "'".static::handleNodeType($child)."'";
106+
}
107+
108+
$shape .= ','.static::generateInlinePhpDocForNode($child).\PHP_EOL;
109+
}
110+
111+
$shape .= str_repeat(' ', 4 * ($nestingLevel - 1)).']';
112+
113+
return $shape.(1 === $nestingLevel ? ')]' : '');
114+
}
115+
116+
private static function dumpNodeKey(NodeInterface $node): string
117+
{
118+
return $node->getName().($node->isRequired() ? '' : '?');
119+
}
120+
121+
private static function handleNumericNode(NumericNode $node): string
122+
{
123+
if ($node instanceof IntegerNode) {
124+
$type = 'int<%s, %s>';
125+
} elseif ($node instanceof FloatNode) {
126+
$type = 'float<%s, %s>';
127+
} else {
128+
$type = 'int<%s, %s>|float<%1$s, %2$s>';
129+
}
130+
131+
$min = $node->getMin() ?? 'min';
132+
$max = $node->getMax() ?? 'max';
133+
134+
return \sprintf($type, $min, $max);
135+
}
136+
137+
private static function prependPhpDocWithStar(string $shape): string
138+
{
139+
return str_replace("\n", "\n * ", $shape);
140+
}
141+
142+
private static function generateInlinePhpDocForNode(BaseNode $node): string
143+
{
144+
$hasContent = false;
145+
$comment = ' /* ';
146+
147+
if ($node->hasDefaultValue() || $node->getInfo() || $node->isDeprecated()) {
148+
if ($node->isDeprecated()) {
149+
$hasContent = true;
150+
$comment .= 'Deprecated: '.$node->getDeprecation($node->getName(), $node->getPath())['message'].' ';
151+
}
152+
153+
if ($info = $node->getInfo()) {
154+
$hasContent = true;
155+
$comment .= $info.' ';
156+
}
157+
158+
if ($node->hasDefaultValue() && !\is_array($defaultValue = $node->getDefaultValue())) {
159+
$hasContent = true;
160+
$comment .= 'Default: '.json_encode($defaultValue).'. ';
161+
}
162+
163+
$comment .= '*/';
164+
}
165+
166+
return $hasContent ? $comment : '';
167+
}
168+
169+
private static function handleNodeType(NodeInterface $node): string
170+
{
171+
return match (true) {
172+
$node instanceof BooleanNode => 'bool',
173+
$node instanceof StringNode => 'string',
174+
$node instanceof NumericNode => static::handleNumericNode($node),
175+
$node instanceof EnumNode => $node->getPermissibleValues('|'),
176+
$node instanceof ScalarNode => 'string|int|float|bool',
177+
$node instanceof VariableNode => 'mixed',
178+
};
179+
}
180+
}

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

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
*
3232
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
3333
*/
34-
class ConfigBuilderGenerator implements ConfigBuilderGeneratorInterface
34+
class ConfigBuilderGenerator implements ConfigBuilderGeneratorInterface, ConfigFunctionAwareBuilderGeneratorInterface
3535
{
3636
/**
3737
* @var ClassBuilder[]
@@ -55,16 +55,17 @@ public function build(ConfigurationInterface $configuration): \Closure
5555

5656
$path = $this->getFullPath($rootClass);
5757
if (!is_file($path)) {
58-
// Generate the class if the file not exists
59-
$this->classes[] = $rootClass;
58+
// Generate the class if the file doesn't exist
6059
$this->buildNode($rootNode, $rootClass, $this->getSubNamespace($rootClass));
60+
$this->buildConfigureOption($rootNode, $rootClass);
6161
$rootClass->addImplements(ConfigBuilderInterface::class);
6262
$rootClass->addMethod('getExtensionAlias', '
6363
public function NAME(): string
6464
{
6565
return \'ALIAS\';
6666
}', ['ALIAS' => $rootNode->getPath()]);
6767

68+
$this->writeRootClass($rootClass);
6869
$this->writeClasses();
6970
}
7071

@@ -76,6 +77,11 @@ public function NAME(): string
7677
};
7778
}
7879

80+
public function buildConfigFunction(array $configurations): \Closure
81+
{
82+
return (new ConfigFunctionGenerator($this->outputDir))->build($configurations);
83+
}
84+
7985
private function getFullPath(ClassBuilder $class): string
8086
{
8187
$directory = $this->outputDir.\DIRECTORY_SEPARATOR.$class->getDirectory();
@@ -86,6 +92,18 @@ private function getFullPath(ClassBuilder $class): string
8692
return $directory.\DIRECTORY_SEPARATOR.$class->getFilename();
8793
}
8894

95+
private function writeRootClass(ClassBuilder $rootClass): void
96+
{
97+
$this->buildConstructor($rootClass);
98+
$this->buildToArray($rootClass, true);
99+
if ($rootClass->getProperties()) {
100+
$rootClass->addProperty('_usedProperties', null, '[]');
101+
}
102+
$this->buildSetExtraKey($rootClass);
103+
104+
file_put_contents($this->getFullPath($rootClass), $rootClass->build());
105+
}
106+
89107
private function writeClasses(): void
90108
{
91109
foreach ($this->classes as $class) {
@@ -102,6 +120,29 @@ private function writeClasses(): void
102120
$this->classes = [];
103121
}
104122

123+
private function buildConfigureOption(ArrayNode $node, ClassBuilder $class): void
124+
{
125+
$class->addUse('JetBrains\PhpStorm\ArrayShape');
126+
$class->addProperty('configOutput', defaultValue: '[]');
127+
128+
$body = '
129+
/*
130+
* @param PHPDOC_ARRAY_SHAPE $config
131+
*/
132+
public function NAME(
133+
// config:
134+
JETBRAINS_ATTRIBUTE PARAM_TYPE $config = []): void
135+
{
136+
$this->configOutput = $config;
137+
}';
138+
139+
$class->addMethod('configure', $body, [
140+
'PARAM_TYPE' => 'array',
141+
'PHPDOC_ARRAY_SHAPE' => ArrayShapeGenerator::generate($node, ArrayShapeGenerator::FORMAT_PHPDOC),
142+
'JETBRAINS_ATTRIBUTE' => str_replace("\n", "\n ", ArrayShapeGenerator::generate($node, ArrayShapeGenerator::FORMAT_JETBRAINS_ATTRIBUTE)),
143+
]);
144+
}
145+
105146
private function buildNode(NodeInterface $node, ClassBuilder $class, string $namespace): void
106147
{
107148
if (!$node instanceof ArrayNode) {
@@ -469,10 +510,24 @@ private function getSingularName(PrototypedArrayNode $node): string
469510
return $name;
470511
}
471512

472-
private function buildToArray(ClassBuilder $class): void
513+
private function buildToArray(ClassBuilder $class, bool $rootClass = false): void
473514
{
474-
$body = '$output = [];';
515+
$body = '';
516+
if ($rootClass) {
517+
$body = 'if ($this->configOutput) {
518+
return $this->configOutput;
519+
}
520+
521+
';
522+
}
523+
524+
$body .= '$output = [];';
525+
475526
foreach ($class->getProperties() as $p) {
527+
if ('configOutput' === $p->getName()) {
528+
continue;
529+
}
530+
476531
$code = '$this->PROPERTY';
477532
if (null !== $p->getType()) {
478533
if ($p->isArray()) {
@@ -509,6 +564,10 @@ private function buildConstructor(ClassBuilder $class): void
509564
{
510565
$body = '';
511566
foreach ($class->getProperties() as $p) {
567+
if ('configOutput' === $p->getName()) {
568+
continue;
569+
}
570+
512571
$code = '$value[\'ORG_NAME\']';
513572
if (null !== $p->getType()) {
514573
if ($p->isArray()) {

0 commit comments

Comments
 (0)
0