Skip to content

Commit 1655d17

Browse files
committed
feature #49098 [Config] Allow enum values in EnumNode (fancyweb)
This PR was merged into the 6.3 branch. Discussion ---------- [Config] Allow enum values in EnumNode | Q | A | ------------- | --- | Branch? | 6.3 | Bug fix? | no | New feature? | yes | Deprecations? | no | Tickets | - | License | MIT | Doc PR | - This PR allows adding 1..N case(s) from the same enum to the allowed values of `EnumNode`: ```php // Allow all cases ->enumNode('foo', FooEnum::cases()) // Allow some cases with some other scalar values ->enumNode('foo', [true, false, FooEnum::Foo, FooEnum::Bar]) ``` The two benefits are not having to reconstruct the enum instance at process time and being able to use the real enum directly in PHP config files: ```php $config ->foo(FooEnum::Bar); ``` YAML config files can also pass an enum value: ```yaml foo: !php/enum \App\Enum\FooEnum::Bar ``` AFAIK, XML config files can't pass an enum value, which would mean they could not be used in this case. Commits ------- defd51d [Config] Allow enum values in EnumNode
2 parents 6d00b0c + defd51d commit 1655d17

File tree

13 files changed

+86
-17
lines changed

13 files changed

+86
-17
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -426,7 +426,7 @@ private function getComment(BaseNode $node): string
426426
}
427427

428428
if ($node instanceof EnumNode) {
429-
$comment .= sprintf(' * @param ParamConfigurator|%s $value', implode('|', array_unique(array_map(fn ($a) => var_export($a, true), $node->getValues()))))."\n";
429+
$comment .= sprintf(' * @param ParamConfigurator|%s $value', implode('|', array_unique(array_map(fn ($a) => !$a instanceof \UnitEnum ? var_export($a, true) : '\\'.ltrim(var_export($a, true), '\\'), $node->getValues()))))."\n";
430430
} else {
431431
$parameterTypes = $this->getParameterTypes($node);
432432
$comment .= ' * @param ParamConfigurator|'.implode('|', $parameterTypes).' $value'."\n";

src/Symfony/Component/Config/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
6.3
5+
---
6+
7+
* Allow enum values in `EnumNode`
8+
49
6.2
510
---
611

src/Symfony/Component/Config/Definition/Dumper/XmlReferenceDumper.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ private function writeNode(NodeInterface $node, int $depth = 0, bool $root = fal
107107
FloatNode::class,
108108
IntegerNode::class => 'numeric value',
109109
BooleanNode::class => 'true|false',
110-
EnumNode::class => implode('|', array_unique(array_map('json_encode', $prototype->getValues()))),
110+
EnumNode::class => $prototype->getPermissibleValues('|'),
111111
default => 'value',
112112
};
113113
}
@@ -149,7 +149,7 @@ private function writeNode(NodeInterface $node, int $depth = 0, bool $root = fal
149149
}
150150

151151
if ($child instanceof EnumNode) {
152-
$comments[] = 'One of '.implode('; ', array_unique(array_map('json_encode', $child->getValues())));
152+
$comments[] = 'One of '.$child->getPermissibleValues('; ');
153153
}
154154

155155
if (\count($comments)) {

src/Symfony/Component/Config/Definition/Dumper/YamlReferenceDumper.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ private function writeNode(NodeInterface $node, NodeInterface $parentNode = null
9898
}
9999
}
100100
} elseif ($node instanceof EnumNode) {
101-
$comments[] = 'One of '.implode('; ', array_unique(array_map('json_encode', $node->getValues())));
101+
$comments[] = 'One of '.$node->getPermissibleValues('; ');
102102
$default = $node->hasDefaultValue() ? Inline::dump($node->getDefaultValue()) : '~';
103103
} elseif (VariableNode::class === $node::class && \is_array($example)) {
104104
// If there is an array example, we are sure we dont need to print a default value

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

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,16 @@ public function __construct(?string $name, NodeInterface $parent = null, array $
2929
}
3030

3131
foreach ($values as $value) {
32-
if (null !== $value && !\is_scalar($value)) {
33-
throw new \InvalidArgumentException(sprintf('"%s" only supports scalar or null values, "%s" given.', __CLASS__, get_debug_type($value)));
32+
if (null === $value || \is_scalar($value)) {
33+
continue;
34+
}
35+
36+
if (!$value instanceof \UnitEnum) {
37+
throw new \InvalidArgumentException(sprintf('"%s" only supports scalar, enum, or null values, "%s" given.', __CLASS__, get_debug_type($value)));
38+
}
39+
40+
if ($value::class !== ($enumClass ??= $value::class)) {
41+
throw new \InvalidArgumentException(sprintf('"%s" only supports one type of enum, "%s" and "%s" passed.', __CLASS__, $enumClass, $value::class));
3442
}
3543
}
3644

@@ -43,12 +51,35 @@ public function getValues()
4351
return $this->values;
4452
}
4553

54+
/**
55+
* @internal
56+
*/
57+
public function getPermissibleValues(string $separator): string
58+
{
59+
return implode($separator, array_unique(array_map(static function (mixed $value): string {
60+
if (!$value instanceof \UnitEnum) {
61+
return json_encode($value);
62+
}
63+
64+
return ltrim(var_export($value, true), '\\');
65+
}, $this->values)));
66+
}
67+
68+
protected function validateType(mixed $value)
69+
{
70+
if ($value instanceof \UnitEnum) {
71+
return;
72+
}
73+
74+
parent::validateType($value);
75+
}
76+
4677
protected function finalizeValue(mixed $value): mixed
4778
{
4879
$value = parent::finalizeValue($value);
4980

5081
if (!\in_array($value, $this->values, true)) {
51-
$ex = new InvalidConfigurationException(sprintf('The value %s is not allowed for path "%s". Permissible values: %s', json_encode($value), $this->getPath(), implode(', ', array_unique(array_map('json_encode', $this->values)))));
82+
$ex = new InvalidConfigurationException(sprintf('The value %s is not allowed for path "%s". Permissible values: %s', json_encode($value), $this->getPath(), $this->getPermissibleValues(', ')));
5283
$ex->setPath($this->getPath());
5384

5485
throw $ex;

src/Symfony/Component/Config/Tests/Builder/Fixtures/PrimitiveTypes.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
1515
use Symfony\Component\Config\Definition\ConfigurationInterface;
16+
use Symfony\Component\Config\Tests\Fixtures\TestEnum;
1617

1718
class PrimitiveTypes implements ConfigurationInterface
1819
{
@@ -23,7 +24,7 @@ public function getConfigTreeBuilder(): TreeBuilder
2324
$rootNode
2425
->children()
2526
->booleanNode('boolean_node')->end()
26-
->enumNode('enum_node')->values(['foo', 'bar', 'baz'])->end()
27+
->enumNode('enum_node')->values(['foo', 'bar', 'baz', TestEnum::Bar])->end()
2728
->floatNode('float_node')->end()
2829
->integerNode('integer_node')->end()
2930
->scalarNode('scalar_node')->end()

src/Symfony/Component/Config/Tests/Builder/Fixtures/PrimitiveTypes/Symfony/Config/PrimitiveTypesConfig.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public function booleanNode($value): static
3333

3434
/**
3535
* @default null
36-
* @param ParamConfigurator|'foo'|'bar'|'baz' $value
36+
* @param ParamConfigurator|'foo'|'bar'|'baz'|\Symfony\Component\Config\Tests\Fixtures\TestEnum::Bar $value
3737
* @return $this
3838
*/
3939
public function enumNode($value): static

src/Symfony/Component/Config/Tests/Definition/Dumper/XmlReferenceDumperTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ private function getConfigurationAsString()
4141
<!-- scalar-deprecated: Deprecated (Since vendor/package 1.1: The child node "scalar_deprecated" at path "acme_root" is deprecated.) -->
4242
<!-- scalar-deprecated-with-message: Deprecated (Since vendor/package 1.1: Deprecation custom message for "scalar_deprecated_with_message" at "acme_root") -->
4343
<!-- enum-with-default: One of "this"; "that" -->
44-
<!-- enum: One of "this"; "that" -->
44+
<!-- enum: One of "this"; "that"; Symfony\Component\Config\Tests\Fixtures\TestEnum::Ccc -->
4545
<!-- variable: Example: foo, bar -->
4646
<config
4747
boolean="true"

src/Symfony/Component/Config/Tests/Definition/Dumper/YamlReferenceDumperTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ private function getConfigurationAsString(): string
102102
scalar_deprecated_with_message: ~ # Deprecated (Since vendor/package 1.1: Deprecation custom message for "scalar_deprecated_with_message" at "acme_root")
103103
node_with_a_looong_name: ~
104104
enum_with_default: this # One of "this"; "that"
105-
enum: ~ # One of "this"; "that"
105+
enum: ~ # One of "this"; "that"; Symfony\Component\Config\Tests\Fixtures\TestEnum::Ccc
106106
107107
# some info
108108
array:

src/Symfony/Component/Config/Tests/Definition/EnumNodeTest.php

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,16 @@
1414
use PHPUnit\Framework\TestCase;
1515
use Symfony\Component\Config\Definition\EnumNode;
1616
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
17+
use Symfony\Component\Config\Tests\Fixtures\TestEnum;
18+
use Symfony\Component\Config\Tests\Fixtures\TestEnum2;
1719

1820
class EnumNodeTest extends TestCase
1921
{
2022
public function testFinalizeValue()
2123
{
22-
$node = new EnumNode('foo', null, ['foo', 'bar']);
24+
$node = new EnumNode('foo', null, ['foo', 'bar', TestEnum::Bar]);
2325
$this->assertSame('foo', $node->finalize('foo'));
26+
$this->assertSame(TestEnum::Bar, $node->finalize(TestEnum::Bar));
2427
}
2528

2629
public function testConstructionWithNoValues()
@@ -51,8 +54,8 @@ public function testConstructionWithNullName()
5154
public function testFinalizeWithInvalidValue()
5255
{
5356
$this->expectException(InvalidConfigurationException::class);
54-
$this->expectExceptionMessage('The value "foobar" is not allowed for path "foo". Permissible values: "foo", "bar"');
55-
$node = new EnumNode('foo', null, ['foo', 'bar']);
57+
$this->expectExceptionMessage('The value "foobar" is not allowed for path "foo". Permissible values: "foo", "bar", Symfony\Component\Config\Tests\Fixtures\TestEnum::Foo');
58+
$node = new EnumNode('foo', null, ['foo', 'bar', TestEnum::Foo]);
5659
$node->finalize('foobar');
5760
}
5861

@@ -80,11 +83,19 @@ public function testSameStringCoercedValuesAreDifferent()
8083
$this->assertNull($node->finalize(null));
8184
}
8285

83-
public function testNonScalarOrNullValueThrows()
86+
public function testNonScalarOrEnumOrNullValueThrows()
8487
{
8588
$this->expectException(\InvalidArgumentException::class);
86-
$this->expectExceptionMessage('"Symfony\Component\Config\Definition\EnumNode" only supports scalar or null values, "stdClass" given.');
89+
$this->expectExceptionMessage('"Symfony\Component\Config\Definition\EnumNode" only supports scalar, enum, or null values, "stdClass" given.');
8790

8891
new EnumNode('ccc', null, [new \stdClass()]);
8992
}
93+
94+
public function testTwoDifferentEnumsThrows()
95+
{
96+
$this->expectException(\InvalidArgumentException::class);
97+
$this->expectExceptionMessage('"Symfony\Component\Config\Definition\EnumNode" only supports one type of enum, "Symfony\Component\Config\Tests\Fixtures\TestEnum" and "Symfony\Component\Config\Tests\Fixtures\TestEnum2" passed.');
98+
99+
new EnumNode('ccc', null, [...TestEnum::cases(), TestEnum2::Ccc]);
100+
}
90101
}

src/Symfony/Component/Config/Tests/Fixtures/Configuration/ExampleConfiguration.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
1515
use Symfony\Component\Config\Definition\ConfigurationInterface;
16+
use Symfony\Component\Config\Tests\Fixtures\TestEnum;
1617

1718
class ExampleConfiguration implements ConfigurationInterface
1819
{
@@ -38,7 +39,7 @@ public function getConfigTreeBuilder(): TreeBuilder
3839
->scalarNode('scalar_deprecated_with_message')->setDeprecated('vendor/package', '1.1', 'Deprecation custom message for "%node%" at "%path%"')->end()
3940
->scalarNode('node_with_a_looong_name')->end()
4041
->enumNode('enum_with_default')->values(['this', 'that'])->defaultValue('this')->end()
41-
->enumNode('enum')->values(['this', 'that'])->end()
42+
->enumNode('enum')->values(['this', 'that', TestEnum::Ccc])->end()
4243
->arrayNode('array')
4344
->info('some info')
4445
->canBeUnset()
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace Symfony\Component\Config\Tests\Fixtures;
4+
5+
enum TestEnum
6+
{
7+
case Foo;
8+
case Bar;
9+
case Ccc;
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace Symfony\Component\Config\Tests\Fixtures;
4+
5+
enum TestEnum2
6+
{
7+
case Foo;
8+
case Bar;
9+
case Ccc;
10+
}

0 commit comments

Comments
 (0)