8000 [Config] Allow using an enum FQCN with `EnumNode` · symfony/symfony@6d25b9b · GitHub
[go: up one dir, main page]

Skip to content

Commit 6d25b9b

Browse files
[Config] Allow using an enum FQCN with EnumNode
1 parent 74df71a commit 6d25b9b

File tree

12 files changed

+165
-8
lines changed

12 files changed

+165
-8
lines changed

src/Symfony/Component/Config/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ CHANGELOG
55
---
66

77
* Add `#[WhenNot]` attribute to prevent service from being registered in a specific environment
8+
* Allow using an enum FQCN with `EnumNode`
89

910
7.1
1011
---

src/Symfony/Component/Config/Definition/Builder/EnumNodeDefinition.php

+15-3
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
class EnumNodeDefinition extends ScalarNodeDefinition
2222
{
2323
private array $values;
24+
private string $enumFqcn;
2425

2526
/**
2627
* @return $this
@@ -36,17 +37,28 @@ public function values(array $values): static
3637
return $this;
3738
}
3839

40+
public function enumFqcn(string $enumFqcn): static
41+
{
42+
if (!enum_exists($enumFqcn)) {
43+
throw new \InvalidArgumentException(sprintf('The enum class "%s" does not exist.', $enumFqcn));
44+
}
45+
46+
$this->enumFqcn = $enumFqcn;
47+
48+
return $this;
49+
}
50+
3951
/**
4052
* Instantiate a Node.
4153
*
4254
* @throws \RuntimeException
4355
*/
4456
protected function instantiateNode(): EnumNode
4557
{
46-
if (!isset($this->values)) {
47-
throw new \RuntimeException('You must call ->values() on enum nodes.');
58+
if (!isset($this->values) && !isset($this->enumFqcn)) {
59+
throw new \RuntimeException('You must call either ->values() or ->enumFqcn() on enum nodes.');
4860
}
4961

50-
return new EnumNode($this->name, $this->parent, $this->values, $this->pathSeparator);
62+
return new EnumNode($this->name, $this->parent, $this->values ?? [$this->enumFqcn], $this->pathSeparator);
5163 }
5264
}

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

+37-4
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,19 @@
2121
class EnumNode extends ScalarNode
2222
{
2323
private array $values;
24+
private ?string $enumFqcn = null;
2425

2526
public function __construct(?string $name, ?NodeInterface $parent = null, array $values = [], string $pathSeparator = BaseNode::DEFAULT_PATH_SEPARATOR)
2627
{
2728
if (!$values) {
2829
throw new \InvalidArgumentException('$values must contain at least one element.');
2930
}
3031

32+
if (1 === \count($values) && \is_string($values[0]) && \enum_exists($enumFqcn = $values[0]) && \is_a($enumFqcn, \BackedEnum::class, true)) {
33+
$values = $enumFqcn::cases();
34+
$this->enumFqcn = $enumFqcn;
35+
}
36+
3137
foreach ($values as $value) {
3238
if (null === $value || \is_scalar($value)) {
3339
continue;
@@ -56,6 +62,10 @@ public function getValues(): array
5662
*/
5763
public function getPermissibleValues(string $separator): string
5864
{
65+
if ($this->enumFqcn) {
66+
return implode($separator, array_map(static fn (\BackedEnum $case) => $case->value, $this->enumFqcn::cases()));
67+
}
68+
5969
return implode($separator, array_unique(array_map(static function (mixed $value): string {
6070
if (!$value instanceof \UnitEnum) {
6171
return json_encode($value);
@@ -78,13 +88,36 @@ protected function finalizeValue(mixed $value): mixed
7888
{
7989
$value = parent::finalizeValue($value);
8090

81-
if (!\in_array($value, $this->values, true)) {
82-
$ex = new InvalidConfigurationException(\sprintf('The value %s is not allowed for path "%s". Permissible values: %s', json_encode($value), $this->getPath(), $this->getPermissibleValues(', ')));
83-
$ex->setPath($this->getPath());
91+
if ($this->enumFqcn) {
92+
if (\is_a($value, $this->enumFqcn, true)) {
93+
return $value;
94+
}
95+
96+
if (\is_string($value) || \is_int($value)) {
97+
try {
98+
$value = $this->enumFqcn::from($value);
99+
} catch (\ValueError) {
100+
throw $this->createInvalidValueException($value);
101+
}
102+
} elseif ($value instanceof \UnitEnum) {
103+
throw new InvalidConfigurationException(sprintf('The value should be part of the "%s" enum, got a value from the "%s" enum.', $this->enumFqcn, $value::class));
104+
} else {
105+
throw new InvalidConfigurationException(sprintf('The value should be either an integer, a string or be part of the "%s" enum, got "%s".', $this->enumFqcn, get_debug_type($value)));
106+
}
107+
}
84108

85-
throw $ex;
109+
if (!\in_array($value, $this->values, true)) {
110+
throw $this->createInvalidValueException($value);
86111
}
87112

88113
return $value;
89114
}
115+
116+
private function createInvalidValueException(mixed $value): InvalidConfigurationException
117+
{
118+
$ex = new InvalidConfigurationException(\sprintf('The value %s is not allowed for path "%s". Permissible values: %s', json_encode($value), $this->getPath(), $this->getPermissibleValues(', ')));
119+
$ex->setPath($this->getPath());
120+
121+
return $ex;
122+
}
90123
}

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

+2
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\BackedTestEnum;
1617
use Symfony\Component\Config\Tests\Fixtures\TestEnum;
1718

1819
class PrimitiveTypes implements ConfigurationInterface
@@ -25,6 +26,7 @@ public function getConfigTreeBuilder(): TreeBuilder
2526
->children()
2627
->booleanNode('boolean_node')->end()
2728
->enumNode('enum_node')->values(['foo', 'bar', 'baz', TestEnum::Bar])->end()
29+
->enumNode('fqcn_enum_node')->enumFqcn(BackedTestEnum::class)->end()
2830
->floatNode('float_node')->end()
2931
->integerNode('integer_node')->end()
3032
->scalarNode('scalar_node')->end()

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

+23
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ class PrimitiveTypesConfig implements \Symfony\Component\Config\Builder\ConfigBu
1212
{
1313
private $booleanNode;
1414
private $enumNode;
15+
private $fqcnEnumNode;
1516
private $floatNode;
1617
private $integerNode;
1718
private $scalarNode;
@@ -44,6 +45,19 @@ public function enumNode($value): static
4445
return $this;
F438
4546
}
4647

48+
/**
49+
* @default null
50+
* @param ParamConfigurator|\Symfony\Component\Config\Tests\Fixtures\BackedTestEnum::Foo|\Symfony\Component\Config\Tests\Fixtures\BackedTestEnum::Bar $value
51+
* @return $this
52+
*/
53+
public function fqcnEnumNode($value): static
54+
{
55+
$this->_usedProperties['fqcnEnumNode'] = true;
56+
$this->fqcnEnumNode = $value;
57+
58+
return $this;
59+
}
60+
4761
/**
4862
* @default null
4963
* @param ParamConfigurator|float $value
@@ -115,6 +129,12 @@ public function __construct(array $value = [])
115129
unset($value['enum_node']);
116130
}
117131

132+
if (array_key_exists('fqcn_enum_node', $value)) {
133+
$this->_usedProperties['fqcnEnumNode'] = true;
134+
$this->fqcnEnumNode = $value['fqcn_enum_node'];
135+
unset($value['fqcn_enum_node']);
136+
}
137+
118138
if (array_key_exists('float_node', $value)) {
119139
$this->_usedProperties['floatNode'] = true;
120140
$this->floatNode = $value['float_node'];
@@ -153,6 +173,9 @@ public function toArray(): array
153173
if (isset($this->_usedProperties['enumNode'])) {
154174
$output['enum_node'] = $this->enumNode;
155175
10000 }
176+
if (isset($this->_usedProperties['fqcnEnumNode'])) {
177+
$output['fqcn_enum_node'] = $this->fqcnEnumNode;
178+
}
156179
if (isset($this->_usedProperties['floatNode'])) {
157180
$output['float_node'] = $this->floatNode;
158181
}

src/Symfony/Component/Config/Tests/Definition/Builder/EnumNodeDefinitionTest.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public function testWithOneValue()
2828
public function testNoValuesPassed()
2929
{
3030
$this->expectException(\RuntimeException::class);
31-
$this->expectExceptionMessage('You must call ->values() on enum nodes.');
31+
$this->expectExceptionMessage('You must call either ->values() or ->enumFqcn() on enum nodes.');
3232
$def = new EnumNodeDefinition('foo');
3333
$def->getNode();
3434
}

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

+2
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ private function getConfigurationAsString()
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" -->
4444
<!-- enum: One of "this"; "that"; Symfony\Component\Config\Tests\Fixtures\TestEnum::Ccc -->
45+
<!-- enum-with-class: One of foo; bar -->
4546
<!-- variable: Example: foo, bar -->
4647
<config
4748
boolean="true"
@@ -58,6 +59,7 @@ private function getConfigurationAsString()
5859
node-with-a-looong-name=""
5960
enum-with-default="this"
6061
enum=""
62+
enum-with-class=""
6163
variable=""
6264
custom-node="true"
6365
>

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

+1
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ private function getConfigurationAsString(): string
103103
node_with_a_looong_name: ~
104104
enum_with_default: this # One of "this"; "that"
105105
enum: ~ # One of "this"; "that"; Symfony\Component\Config\Tests\Fixtures\TestEnum::Ccc
106+
enum_with_class: ~ # One of foo; bar
106107
107108
# some info
108109
array:

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

+63
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
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\BackedTestEnum;
18+
use Symfony\Component\Config\Tests\Fixtures\BackedTestEnum2;
1719
use Symfony\Component\Config\Tests\Fixtures\TestEnum;
1820
use Symfony\Component\Config\Tests\Fixtures\TestEnum2;
1921

@@ -61,6 +63,67 @@ public function testFinalizeWithInvalidValue()
6163
$node->finalize('foobar');
6264
}
6365

66+
public function testFinalizeWithOnlyUnitEnumFqcnDoesntDoAnythingSpecial()
67+
{
68+
$node = new EnumNode('foo', null, [TestEnum::class]);
69+
70+
$this->expectException(InvalidConfigurationException::class);
71+
$this->expectExceptionMessage('The value "foobar" is not allowed for path "foo". Permissible values: "Symfony\\\\Component\\\\Config\\\\Tests\\\\Fixtures\\\\TestEnum"');
72+
73+
$node->finalize('foobar');
74+
}
75+
76+
public function testFinalizeWithEnumFqcn()
77+
{
78+
$node = new EnumNode('foo', null, [BackedTestEnum::class]);
79+
80+
$this->assertSame(BackedTestEnum::Foo, $node->finalize(BackedTestEnum::Foo));
81+
}
82+
83+
public function testFinalizeAnotherEnumWithEnumFqcn()
84+
{
85+
$node = new EnumNode('foo', null, [BackedTestEnum::class]);
86+
87+
$this->expectException(InvalidConfigurationException::class);
88+
$this->expectExceptionMessage('The value should be part of the "Symfony\Component\Config\Tests\Fixtures\BackedTestEnum" enum, got a value from the "Symfony\Component\Config\Tests\Fixtures\BackedTestEnum2" enum.');
89+
90+
$node->finalize(BackedTestEnum2::Foo);
91+
}
92+
93+
public function testFinalizeWithEnumFqcnAndAnotherScalar()
94+
{
95+
$node = new EnumNode('foo', null, [BackedTestEnum::class, 'another_string']);
96+
97+
$this->assertSame(BackedTestEnum::class, $node->finalize(BackedTestEnum::class));
98+
}
99+
100+
public function testFinalizeWithEnumFqcnWorksWithPlainString()
101+
{
102+
$node = new EnumNode('foo', null, [BackedTestEnum::class]);
103+
104+
$this->assertSame(BackedTestEnum::Foo, $node->finalize('foo'));
105+
}
106+
107+
public function testFinalizeWithEnumFqcnWithWrongCase()
108+
{
109+
$node = new EnumNode('foo', null, [BackedTestEnum::class]);
110+
111+
$this->expectException(InvalidConfigurationException::class);
112+
$this->expectExceptionMessage('The value "qux" is not allowed for path "foo". Permissible values: foo, bar');
113+
114+
$node->finalize('qux');
115+
}
116+
117+
public function testFinalizeWithEnumFqcnWithWrongType()
118+
{
119+
$node = new EnumNode('foo', null, [BackedTestEnum::class]);
120+
121+
$this->expectException(InvalidConfigurationException::class);
122+
$this->expectExceptionMessage('The value should be either an integer, a string or be part of the "Symfony\Component\Config\Tests\Fixtures\BackedTestEnum" enum, got "bool".');
123+
124+
$node->finalize(true);
125+
}
126+
64127
public function testWithPlaceHolderWithValidValue()
65128
{
66129
$node = new EnumNode('cookie_samesite', null, ['lax', 'strict', 'none']);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
namespace Symfony\Component\Config\Tests\Fixtures;
4+
5+
enum BackedTestEnum: string
6+
{
7+
case Foo = 'foo';
8+
case Bar = 'bar';
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
namespace Symfony\Component\Config\Tests\Fixtures;
4+
5+
enum BackedTestEnum2: string
6+
{
7+
case Foo = 'foo';
8+
case Bar = 'bar';
9+
}

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

+2
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\BackedTestEnum;
1617
use Symfony\Component\Config\Tests\Fixtures\TestEnum;
1718

1819
class ExampleConfiguration implements ConfigurationInterface
@@ -40,6 +41,7 @@ public function getConfigTreeBuilder(): TreeBuilder
4041
->scalarNode('node_with_a_looong_name')->end()
4142
->enumNode('enum_with_default')->values(['this', 'that'])->defaultValue('this')->end()
4243
->enumNode('enum')->values(['this', 'that', TestEnum::Ccc])->end()
44+
->enumNode('enum_with_class')->enumFqcn(BackedTestEnum::class)->end()
4345
->arrayNode('array')
4446
->info('some info')
4547
->canBeUnset()

0 commit comments

Comments
 (0)
0