Skip to content

Commit f1daab5

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

File tree

12 files changed

+166
-8
lines changed

12 files changed

+166
-8
lines changed

src/Symfony/Component/Config/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
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

Lines changed: 15 additions & 3 deletions
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

Lines changed: 38 additions & 4 deletions
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,37 @@ 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 ($value instanceof \UnitEnum) {
97+
throw new InvalidConfigurationException(sprintf('The value should be part of the "%s" enum, got a value from the "%s" enum.', $this->enumFqcn, $value::class));
98+
}
8499

85-
throw $ex;
100+
if (\is_string($value) || \is_int($value)) {
101+
$case = $this->enumFqcn::tryFrom($value);
102+
if (null !== $case) {
103+
return $case;
104+
}
105+
}
106+
107+
throw $this->createInvalidValueException($value);
108+
}
109+
110+
if (!\in_array($value, $this->values, true)) {
111+
throw $this->createInvalidValueException($value);
86112
}
87113

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

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

Lines changed: 2 additions & 0 deletions
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

Lines changed: 23 additions & 0 deletions
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;
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
}
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 2 additions & 0 deletions
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 63 additions & 0 deletions
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 true is not allowed for path "foo". Permissible values: foo, bar');
123+
124+
$node->finalize(true);
125+
}
126+
64127
public function testWithPlaceHolderWithValidValue()
65128
{
66129
$node = new EnumNode('cookie_samesite', null, ['lax', 'strict', 'none']);
Lines changed: 9 additions & 0 deletions
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+
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy