From 3d385ac515a9a908ef2ad01f7d2f806076706e5a Mon Sep 17 00:00:00 2001 From: Martin Bruna Date: Thu, 1 May 2025 21:41:08 +0200 Subject: [PATCH] [Config] Fix generating PHP config for keyed list of scalars --- .../Config/Builder/ConfigBuilderGenerator.php | 9 ++-- .../Fixtures/ScalarNormalizedTypes.config.php | 7 +++ .../Fixtures/ScalarNormalizedTypes.output.php | 11 ++++ .../Fixtures/ScalarNormalizedTypes.php | 17 ++++++ .../KeyedListScalarConfig.php | 52 +++++++++++++++++++ .../ScalarNormalizedTypes/NestedConfig.php | 3 +- .../Config/ScalarNormalizedTypesConfig.php | 43 ++++++++++++++- 7 files changed, 135 insertions(+), 7 deletions(-) create mode 100644 src/Symfony/Component/Config/Tests/Builder/Fixtures/ScalarNormalizedTypes/Symfony/Config/ScalarNormalizedTypes/KeyedListScalarConfig.php diff --git a/src/Symfony/Component/Config/Builder/ConfigBuilderGenerator.php b/src/Symfony/Component/Config/Builder/ConfigBuilderGenerator.php index d43d814ebd38b..fae3e02ddbb09 100644 --- a/src/Symfony/Component/Config/Builder/ConfigBuilderGenerator.php +++ b/src/Symfony/Component/Config/Builder/ConfigBuilderGenerator.php @@ -284,7 +284,8 @@ public function NAME(string $VAR, TYPE $VALUE): static if ($hasNormalizationClosures) { $comment = sprintf(" * @template TValue\n * @param TValue \$value\n%s", $comment); $comment .= sprintf(' * @return %s|$this'."\n", $childClass->getFqcn()); - $comment .= sprintf(' * @psalm-return (TValue is array ? %s : static)'."\n ", $childClass->getFqcn()); + $comment .= sprintf(' * @psalm-return (TValue is array ? %s : static)'."\n", $childClass->getFqcn()); + $comment .= sprintf(' * @phpstan-return ($value is array ? %s : $this)'."\n ", $childClass->getFqcn()); } if ('' !== $comment) { $comment = "/**\n$comment*/\n"; @@ -292,7 +293,7 @@ public function NAME(string $VAR, TYPE $VALUE): static if (null === $key = $node->getKeyAttribute()) { $body = $hasNormalizationClosures ? ' -COMMENTpublic function NAME(PARAM_TYPE $value = []): CLASS|static +COMMENTpublic function NAME(PARAM_TYPE $value = []): CLASS|self { $this->_usedProperties[\'PROPERTY\'] = true; if (!\is_array($value)) { @@ -317,7 +318,7 @@ public function NAME(string $VAR, TYPE $VALUE): static ]); } else { $body = $hasNormalizationClosures ? ' -COMMENTpublic function NAME(string $VAR, PARAM_TYPE $VALUE = []): CLASS|static +COMMENTpublic function NAME(string $VAR, PARAM_TYPE $VALUE = []): CLASS|self { if (!\is_array($VALUE)) { $this->_usedProperties[\'PROPERTY\'] = true; @@ -352,7 +353,7 @@ public function NAME(string $VAR, TYPE $VALUE): static 'CLASS' => $childClass->getFqcn(), 'VAR' => '' === $key ? 'key' : $key, 'VALUE' => 'value' === $key ? 'data' : 'value', - 'PARAM_TYPE' => \in_array('mixed', $prototypeParameterTypes, true) ? 'mixed' : implode('|', $prototypeParameterTypes), + 'PARAM_TYPE' => \in_array('mixed', $prototypeParameterTypes, true) ? 'mixed' : implode('|', [...$prototypeParameterTypes, $childClass->getFqcn()]), ]); } diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/ScalarNormalizedTypes.config.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/ScalarNormalizedTypes.config.php index d0c6d320709c2..4b2062bf59474 100644 --- a/src/Symfony/Component/Config/Tests/Builder/Fixtures/ScalarNormalizedTypes.config.php +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/ScalarNormalizedTypes.config.php @@ -9,6 +9,7 @@ * file that was distributed with this source code. */ +use Symfony\Config\ScalarNormalizedTypes\KeyedListScalarConfig; use Symfony\Config\ScalarNormalizedTypesConfig; return static function (ScalarNormalizedTypesConfig $config) { @@ -28,4 +29,10 @@ 'nested_object' => true, 'nested_list_object' => ['one', 'two'], ]); + + $config->keyedListScalar('Foo\\Bar')->list(['one', 'two']); + $config->keyedListScalar('Foo\\Baz', ['list' => ['one', 'two']]); + $keyedListScalarConfig = new KeyedListScalarConfig(); + $keyedListScalarConfig->list(['one', 'two']); + $config->keyedListScalar('Foo\\Foo', $keyedListScalarConfig); }; diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/ScalarNormalizedTypes.output.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/ScalarNormalizedTypes.output.php index 9dcf3a9f3d335..30dbf8b77ef8e 100644 --- a/src/Symfony/Component/Config/Tests/Builder/Fixtures/ScalarNormalizedTypes.output.php +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/ScalarNormalizedTypes.output.php @@ -30,4 +30,15 @@ 'nested_object' => true, 'nested_list_object' => ['one', 'two'], ], + 'keyed_list_scalar' => [ + 'Foo\\Bar' => [ + 'list' => ['one', 'two'], + ], + 'Foo\\Baz' => [ + 'list' => ['one', 'two'], + ], + 'Foo\\Foo' => [ + 'list' => ['one', 'two'], + ], + ], ]; diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/ScalarNormalizedTypes.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/ScalarNormalizedTypes.php index 25d385d736fae..95b6e58fa5696 100644 --- a/src/Symfony/Component/Config/Tests/Builder/Fixtures/ScalarNormalizedTypes.php +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/ScalarNormalizedTypes.php @@ -136,6 +136,23 @@ public function getConfigTreeBuilder(): TreeBuilder ->end() ->end() ->end() + ->arrayNode('keyed_list_scalar') + ->normalizeKeys(false) + ->useAttributeAsKey('class') + ->beforeNormalization() + ->always() + ->then(function ($config) { + return $config; + }) + ->end() + ->prototype('array') + ->children() + ->arrayNode('list') + ->prototype('scalar')->end() + ->end() + ->end() + ->end() + ->end() ->end() ; diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/ScalarNormalizedTypes/Symfony/Config/ScalarNormalizedTypes/KeyedListScalarConfig.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/ScalarNormalizedTypes/Symfony/Config/ScalarNormalizedTypes/KeyedListScalarConfig.php new file mode 100644 index 0000000000000..c0497f49dd687 --- /dev/null +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/ScalarNormalizedTypes/Symfony/Config/ScalarNormalizedTypes/KeyedListScalarConfig.php @@ -0,0 +1,52 @@ + $value + * + * @return $this + */ + public function list(ParamConfigurator|array $value): static + { + $this->_usedProperties['list'] = true; + $this->list = $value; + + return $this; + } + + public function __construct(array $value = []) + { + if (array_key_exists('list', $value)) { + $this->_usedProperties['list'] = true; + $this->list = $value['list']; + unset($value['list']); + } + + if ([] !== $value) { + throw new InvalidConfigurationException(sprintf('The following keys are not supported by "%s": ', __CLASS__).implode(', ', array_keys($value))); + } + } + + public function toArray(): array + { + $output = []; + if (isset($this->_usedProperties['list'])) { + $output['list'] = $this->list; + } + + return $output; + } + +} diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/ScalarNormalizedTypes/Symfony/Config/ScalarNormalizedTypes/NestedConfig.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/ScalarNormalizedTypes/Symfony/Config/ScalarNormalizedTypes/NestedConfig.php index 2cc1fb3275e78..4448a7a7fce34 100644 --- a/src/Symfony/Component/Config/Tests/Builder/Fixtures/ScalarNormalizedTypes/Symfony/Config/ScalarNormalizedTypes/NestedConfig.php +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/ScalarNormalizedTypes/Symfony/Config/ScalarNormalizedTypes/NestedConfig.php @@ -47,8 +47,9 @@ public function nestedObject(mixed $value = []): \Symfony\Config\ScalarNormalize * @param TValue $value * @return \Symfony\Config\ScalarNormalizedTypes\Nested\NestedListObjectConfig|$this * @psalm-return (TValue is array ? \Symfony\Config\ScalarNormalizedTypes\Nested\NestedListObjectConfig : static) + * @phpstan-return ($value is array ? \Symfony\Config\ScalarNormalizedTypes\Nested\NestedListObjectConfig : $this) */ - public function nestedListObject(mixed $value = []): \Symfony\Config\ScalarNormalizedTypes\Nested\NestedListObjectConfig|static + public function nestedListObject(mixed $value = []): \Symfony\Config\ScalarNormalizedTypes\Nested\NestedListObjectConfig|self { $this->_usedProperties['nestedListObject'] = true; if (!\is_array($value)) { diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/ScalarNormalizedTypes/Symfony/Config/ScalarNormalizedTypesConfig.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/ScalarNormalizedTypes/Symfony/Config/ScalarNormalizedTypesConfig.php index 1794ede72e18c..51086f36a5868 100644 --- a/src/Symfony/Component/Config/Tests/Builder/Fixtures/ScalarNormalizedTypes/Symfony/Config/ScalarNormalizedTypesConfig.php +++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/ScalarNormalizedTypes/Symfony/Config/ScalarNormalizedTypesConfig.php @@ -6,6 +6,7 @@ require_once __DIR__.\DIRECTORY_SEPARATOR.'ScalarNormalizedTypes'.\DIRECTORY_SEPARATOR.'ListObjectConfig.php'; require_once __DIR__.\DIRECTORY_SEPARATOR.'ScalarNormalizedTypes'.\DIRECTORY_SEPARATOR.'KeyedListObjectConfig.php'; require_once __DIR__.\DIRECTORY_SEPARATOR.'ScalarNormalizedTypes'.\DIRECTORY_SEPARATOR.'NestedConfig.php'; +require_once __DIR__.\DIRECTORY_SEPARATOR.'ScalarNormalizedTypes'.\DIRECTORY_SEPARATOR.'KeyedListScalarConfig.php'; use Symfony\Component\Config\Loader\ParamConfigurator; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; @@ -21,6 +22,7 @@ class ScalarNormalizedTypesConfig implements \Symfony\Component\Config\Builder\C private $listObject; private $keyedListObject; private $nested; + private $keyedListScalar; private $_usedProperties = []; /** @@ -78,8 +80,9 @@ public function object(mixed $value = []): \Symfony\Config\ScalarNormalizedTypes * @param TValue $value * @return \Symfony\Config\ScalarNormalizedTypes\ListObjectConfig|$this * @psalm-return (TValue is array ? \Symfony\Config\ScalarNormalizedTypes\ListObjectConfig : static) + * @phpstan-return ($value is array ? \Symfony\Config\ScalarNormalizedTypes\ListObjectConfig : $this) */ - public function listObject(mixed $value = []): \Symfony\Config\ScalarNormalizedTypes\ListObjectConfig|static + public function listObject(mixed $value = []): \Symfony\Config\ScalarNormalizedTypes\ListObjectConfig|self { $this->_usedProperties['listObject'] = true; if (!\is_array($value)) { @@ -96,8 +99,9 @@ public function listObject(mixed $value = []): \Symfony\Config\ScalarNormalizedT * @param TValue $value * @return \Symfony\Config\ScalarNormalizedTypes\KeyedListObjectConfig|$this * @psalm-return (TValue is array ? \Symfony\Config\ScalarNormalizedTypes\KeyedListObjectConfig : static) + * @phpstan-return ($value is array ? \Symfony\Config\ScalarNormalizedTypes\KeyedListObjectConfig : $this) */ - public function keyedListObject(string $class, mixed $value = []): \Symfony\Config\ScalarNormalizedTypes\KeyedListObjectConfig|static + public function keyedListObject(string $class, mixed $value = []): \Symfony\Config\ScalarNormalizedTypes\KeyedListObjectConfig|self { if (!\is_array($value)) { $this->_usedProperties['keyedListObject'] = true; @@ -128,6 +132,32 @@ public function nested(array $value = []): \Symfony\Config\ScalarNormalizedTypes return $this->nested; } + /** + * @template TValue + * @param TValue $value + * @return \Symfony\Config\ScalarNormalizedTypes\KeyedListScalarConfig|$this + * @psalm-return (TValue is array ? \Symfony\Config\ScalarNormalizedTypes\KeyedListScalarConfig : static) + * @phpstan-return ($value is array ? \Symfony\Config\ScalarNormalizedTypes\KeyedListScalarConfig : $this) + */ + public function keyedListScalar(string $class, array|\Symfony\Config\ScalarNormalizedTypes\KeyedListScalarConfig $value = []): \Symfony\Config\ScalarNormalizedTypes\KeyedListScalarConfig|self + { + if (!\is_array($value)) { + $this->_usedProperties['keyedListScalar'] = true; + $this->keyedListScalar[$class] = $value; + + return $this; + } + + if (!isset($this->keyedListScalar[$class]) || !$this->keyedListScalar[$class] instanceof \Symfony\Config\ScalarNormalizedTypes\KeyedListScalarConfig) { + $this->_usedProperties['keyedListScalar'] = true; + $this->keyedListScalar[$class] = new \Symfony\Config\ScalarNormalizedTypes\KeyedListScalarConfig($value); + } elseif (1 < \func_num_args()) { + throw new InvalidConfigurationException('The node created by "keyedListScalar()" has already been initialized. You cannot pass values the second time you call keyedListScalar().'); + } + + return $this->keyedListScalar[$class]; + } + public function getExtensionAlias(): string { return 'scalar_normalized_types'; @@ -171,6 +201,12 @@ public function __construct(array $value = []) unset($value['nested']); } + if (array_key_exists('keyed_list_scalar', $value)) { + $this->_usedProperties['keyedListScalar'] = true; + $this->keyedListScalar = array_map(fn ($v) => \is_array($v) ? new \Symfony\Config\ScalarNormalizedTypes\KeyedListScalarConfig($v) : $v, $value['keyed_list_scalar']); + unset($value['keyed_list_scalar']); + } + if ([] !== $value) { throw new InvalidConfigurationException(sprintf('The following keys are not supported by "%s": ', __CLASS__).implode(', ', array_keys($value))); } @@ -197,6 +233,9 @@ public function toArray(): array if (isset($this->_usedProperties['nested'])) { $output['nested'] = $this->nested->toArray(); } + if (isset($this->_usedProperties['keyedListScalar'])) { + $output['keyed_list_scalar'] = array_map(fn ($v) => $v instanceof \Symfony\Config\ScalarNormalizedTypes\KeyedListScalarConfig ? $v->toArray() : $v, $this->keyedListScalar); + } return $output; }