Skip to content

Commit 545e364

Browse files
alexandre-dauboisnicolas-grekas
authored andcommitted
[Config] Add array-shapes to generated config builders
1 parent 9c413a3 commit 545e364

File tree

33 files changed

+1133
-290
lines changed

33 files changed

+1133
-290
lines changed
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
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 static function generate(NodeInterface $node): string
35+
{
36+
return str_replace("\n", "\n * ", self::doGeneratePhpDoc($node));
37+
}
38+
39+
private static function doGeneratePhpDoc(NodeInterface $node, int $nestingLevel = 1): string
40+
{
41+
if (!$node instanceof ArrayNode) {
42+
return match (true) {
43+
$node instanceof BooleanNode => 'bool',
44+
$node instanceof StringNode => 'string',
45+
$node instanceof NumericNode => self::handleNumericNode($node),
46+
$node instanceof EnumNode => $node->getPermissibleValues('|'),
47+
$node instanceof ScalarNode => 'string|int|float|bool|null',
48+
$node instanceof VariableNode => 'mixed',
49+
};
50+
}
51+
52+
if ($node instanceof PrototypedArrayNode) {
53+
$isHashmap = (bool) $node->getKeyAttribute();
54+
55+
return 'array<'.($isHashmap ? 'string, ' : '').self::doGeneratePhpDoc($node->getPrototype(), 1 + $nestingLevel).'>';
56+
}
57+
58+
if (!($children = $node->getChildren()) && !$node->getParent() instanceof PrototypedArrayNode) {
59+
return 'array<mixed>';
60+
}
61+
62+
$arrayShape = \sprintf("array{%s\n", self::generateInlinePhpDocForNode($node));
63+
64+
foreach ($children as $child) {
65+
$arrayShape .= str_repeat(' ', $nestingLevel).self::dumpNodeKey($child).': ';
66+
67+
if ($child instanceof PrototypedArrayNode) {
68+
$isHashmap = (bool) $child->getKeyAttribute();
69+
70+
$arrayShape .= 'array<'.($isHashmap ? 'string, ' : '').self::doGeneratePhpDoc($child->getPrototype(), 1 + $nestingLevel).'>';
71+
} else {
72+
$arrayShape .= self::doGeneratePhpDoc($child, 1 + $nestingLevel);
73+
}
74+
75+
$arrayShape .= \sprintf(",%s\n", !$child instanceof ArrayNode ? self::generateInlinePhpDocForNode($child) : '');
76+
}
77+
78+
if ($node->shouldIgnoreExtraKeys()) {
79+
$arrayShape .= str_repeat(' ', $nestingLevel)."...<mixed>\n";
80+
}
81+
82+
return $arrayShape.str_repeat(' ', $nestingLevel - 1).'}';
83+
}
84+
85+
private static function dumpNodeKey(NodeInterface $node): string
86+
{
87+
$name = $node->getName();
88+
$quoted = str_starts_with($name, '@')
89+
|| \in_array(strtolower($name), ['int', 'float', 'bool', 'null', 'scalar'], true)
90+
|| strpbrk($name, '\'"');
91+
92+
if ($quoted) {
93+
$name = "'".addslashes($name)."'";
94+
}
95+
96+
return $name.($node->isRequired() ? '' : '?');
97+
}
98+
99+
private static function handleNumericNode(NumericNode $node): string
100+
{
101+
$min = $node->getMin() ?? 'min';
102+
$max = $node->getMax() ?? 'max';
103+
104+
if ($node instanceof IntegerNode) {
105+
return \sprintf('int<%s, %s>', $min, $max);
106+
}
107+
if ($node instanceof FloatNode) {
108+
return 'float';
109+
}
110+
111+
return \sprintf('int<%s, %s>|float', $min, $max);
112+
}
113+
114+
private static function generateInlinePhpDocForNode(BaseNode $node): string
115+
{
116+
$comment = '';
117+
if ($node->hasDefaultValue() || $node->getInfo() || $node->isDeprecated()) {
118+
if ($node->isDeprecated()) {
119+
$comment .= 'Deprecated: '.$node->getDeprecation($node->getName(), $node->getPath())['message'].' ';
120+
}
121+
122+
if ($info = $node->getInfo()) {
123+
$comment .= $info.' ';
124+
}
125+
126+
if ($node->hasDefaultValue()) {
127+
$comment .= 'Default: '.json_encode($node->getDefaultValue(), \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE | \JSON_PRESERVE_ZERO_FRACTION);
128+
}
129+
}
130+
131+
return $comment ? ' // '.rtrim(preg_replace('/\s+/', ' ', $comment)) : '';
132+
}
133+
}

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

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ public function NAME(): string
6565
return \'ALIAS\';
6666
}', ['ALIAS' => $rootNode->getPath()]);
6767

68-
$this->writeClasses();
68+
$this->writeClasses($rootNode);
6969
}
7070

7171
return function () use ($path, $rootClass) {
@@ -86,10 +86,10 @@ private function getFullPath(ClassBuilder $class): string
8686
return $directory.\DIRECTORY_SEPARATOR.$class->getFilename();
8787
}
8888

89-
private function writeClasses(): void
89+
private function writeClasses(NodeInterface $node): void
9090
{
9191
foreach ($this->classes as $class) {
92-
$this->buildConstructor($class);
92+
$this->buildConstructor($class, $node);
9393
$this->buildToArray($class);
9494
if ($class->getProperties()) {
9595
$class->addProperty('_usedProperties', null, '[]');
@@ -114,7 +114,7 @@ private function buildNode(NodeInterface $node, ClassBuilder $class, string $nam
114114
$child instanceof PrototypedArrayNode => $this->handlePrototypedArrayNode($child, $class, $namespace),
115115
$child instanceof VariableNode => $this->handleVariableNode($child, $class),
116116
$child instanceof ArrayNode => $this->handleArrayNode($child, $class, $namespace),
117-
default => throw new \RuntimeException(\sprintf('Unknown node "%s".', $child::class)),
117+
default => throw new \RuntimeException(\sprintf('Unknown node "%s".', get_debug_type($child))),
118118
};
119119
}
120120
}
@@ -503,8 +503,8 @@ private function buildToArray(ClassBuilder $class): void
503503

504504
$body .= strtr('
505505
if (isset($this->_usedProperties[\'PROPERTY\'])) {
506-
$output[\'ORG_NAME\'] = '.$code.';
507-
}', ['PROPERTY' => $p->getName(), 'ORG_NAME' => $p->getOriginalName(), 'CLASS' => $p->getType()]);
506+
$output[\'ORIG_NAME\'] = '.$code.';
507+
}', ['PROPERTY' => $p->getName(), 'ORIG_NAME' => $p->getOriginalName(), 'CLASS' => $p->getType()]);
508508
}
509509

510510
$extraKeys = $class->shouldAllowExtraKeys() ? ' + $this->_extraKeys' : '';
@@ -518,51 +518,54 @@ public function NAME(): array
518518
}');
519519
}
520520

521-
private function buildConstructor(ClassBuilder $class): void
521+
private function buildConstructor(ClassBuilder $class, NodeInterface $node): void
522522
{
523523
$body = '';
524524
foreach ($class->getProperties() as $p) {
525-
$code = '$value[\'ORG_NAME\']';
525+
$code = '$config[\'ORIG_NAME\']';
526526
if (null !== $p->getType()) {
527527
if ($p->isArray()) {
528528
$code = $p->areScalarsAllowed()
529-
? 'array_map(fn ($v) => \is_array($v) ? new '.$p->getType().'($v) : $v, $value[\'ORG_NAME\'])'
530-
: 'array_map(fn ($v) => new '.$p->getType().'($v), $value[\'ORG_NAME\'])'
529+
? 'array_map(fn ($v) => \is_array($v) ? new '.$p->getType().'($v) : $v, $config[\'ORIG_NAME\'])'
530+
: 'array_map(fn ($v) => new '.$p->getType().'($v), $config[\'ORIG_NAME\'])'
531531
;
532532
} else {
533533
$code = $p->areScalarsAllowed()
534-
? '\is_array($value[\'ORG_NAME\']) ? new '.$p->getType().'($value[\'ORG_NAME\']) : $value[\'ORG_NAME\']'
535-
: 'new '.$p->getType().'($value[\'ORG_NAME\'])'
534+
? '\is_array($config[\'ORIG_NAME\']) ? new '.$p->getType().'($config[\'ORIG_NAME\']) : $config[\'ORIG_NAME\']'
535+
: 'new '.$p->getType().'($config[\'ORIG_NAME\'])'
536536
;
537537
}
538538
}
539539

540540
$body .= strtr('
541-
if (array_key_exists(\'ORG_NAME\', $value)) {
541+
if (array_key_exists(\'ORIG_NAME\', $config)) {
542542
$this->_usedProperties[\'PROPERTY\'] = true;
543543
$this->PROPERTY = '.$code.';
544-
unset($value[\'ORG_NAME\']);
544+
unset($config[\'ORIG_NAME\']);
545545
}
546-
', ['PROPERTY' => $p->getName(), 'ORG_NAME' => $p->getOriginalName()]);
546+
', ['PROPERTY' => $p->getName(), 'ORIG_NAME' => $p->getOriginalName()]);
547547
}
548548

549549
if ($class->shouldAllowExtraKeys()) {
550550
$body .= '
551-
$this->_extraKeys = $value;
551+
$this->_extraKeys = $config;
552552
';
553553
} else {
554554
$body .= '
555-
if ([] !== $value) {
556-
throw new InvalidConfigurationException(sprintf(\'The following keys are not supported by "%s": \', __CLASS__).implode(\', \', array_keys($value)));
555+
if ($config) {
556+
throw new InvalidConfigurationException(sprintf(\'The following keys are not supported by "%s": \', __CLASS__).implode(\', \', array_keys($config)));
557557
}';
558558

559559
$class->addUse(InvalidConfigurationException::class);
560560
}
561561

562562
$class->addMethod('__construct', '
563-
public function __construct(array $value = [])
563+
/**
564+
* @param PARAM_TYPE $config
565+
*/
566+
public function __construct(array $config = [])
564567
{'.$body.'
565-
}');
568+
}', ['PARAM_TYPE' => ArrayShapeGenerator::generate($node)]);
566569
}
567570

568571
private function buildSetExtraKey(ClassBuilder $class): void

src/Symfony/Component/Config/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ CHANGELOG
88
* Add argument `$singular` to `NodeBuilder::arrayNode()` to decouple plurals/singulars from XML
99
* Add support for `defaultNull()` on `ArrayNodeDefinition`
1010
* Add `ArrayNodeDefinition::acceptAndWrap()` to list alternative types that should be accepted and wrapped in an array
11+
* Add array-shapes to generated config builders
1112

1213
7.3
1314
---

src/Symfony/Component/Config/Definition/NumericNode.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,16 @@ protected function finalizeValue(mixed $value): mixed
5050
return $value;
5151
}
5252

53+
public function getMin(): float|int|null
54+
{
55+
return $this->min;
56+
}
57+
58+
public function getMax(): float|int|null
59+
{
60+
return $this->max;
61+
}
62+
5363
protected function isValueEmpty(mixed $value): bool
5464
{
5565
// a numeric value cannot be empty

0 commit comments

Comments
 (0)