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

Skip to content

Commit bc2ab61

Browse files
[Config] Allow using an enum FQCN with EnumNode
1 parent 3918524 commit bc2ab61

15 files changed

+326
-10
lines changed

src/Symfony/Component/Config/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ CHANGELOG
1717
* Add `StringNode` and `StringNodeDefinition`
1818
* Add `ArrayNodeDefinition::stringPrototype()` method
1919
* Add `NodeBuilder::stringNode()` method
20+
* Allow using an enum FQCN with `EnumNode`
2021

2122
7.1
2223
---

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

+24-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,37 @@ public function values(array $values): static
3637
return $this;
3738
}
3839

40+
/**
41+
* @param class-string<\UnitEnum> $enumFqcn
42+
*
43+
* @return $this
44+
*/
45+
public function enumFqcn(string $enumFqcn): static
46+
{
47+
if (!enum_exists($enumFqcn)) {
48+
throw new \InvalidArgumentException(\sprintf('The enum class "%s" does not exist.', $enumFqcn));
49+
}
50+
51+
$this->enumFqcn = $enumFqcn;
52+
53+
return $this;
54+
}
55+
3956
/**
4057
* Instantiate a Node.
4158
*
4259
* @throws \RuntimeException
4360
*/
4461
protected function instantiateNode(): EnumNode
4562
{
46-
if (!isset($this->values)) {
47-
throw new \RuntimeException('You must call ->values() on enum nodes.');
63+
if (!isset($this->values) && !isset($this->enumFqcn)) {
64+
throw new \RuntimeException('You must call either ->values() or ->enumFqcn() on enum nodes.');
65+
}
66+
67+
if (isset($this->values) && isset($this->enumFqcn)) {
68+
throw new \RuntimeException('You must call either ->values() or ->enumFqcn() on enum nodes but not both.');
4869
}
4970

50-
return new EnumNode($this->name, $this->parent, $this->values, $this->pathSeparator);
71+
return new EnumNode($this->name, $this->parent, $this->values ?? [], $this->pathSeparator, $this->enumFqcn ?? null);
5172
}
5273
}

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

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

25-
public function __construct(?string $name, ?NodeInterface $parent = null, array $values = [], string $pathSeparator = BaseNode::DEFAULT_PATH_SEPARATOR)
26+
/**
27+
* @param class-string<\UnitEnum>|null $enumFqcn
28+
*/
29+
public function __construct(?string $name, ?NodeInterface $parent = null, array $values = [], string $pathSeparator = BaseNode::DEFAULT_PATH_SEPARATOR, ?string $enumFqcn = null)
2630
{
27-
if (!$values) {
31+
if (!$values && !$enumFqcn) {
2832
throw new \InvalidArgumentException('$values must contain at least one element.');
2933
}
3034

35+
if ($values && $enumFqcn) {
36+
throw new \InvalidArgumentException('$values or $enumFqcn cannot be both set.');
37+
}
38+
39+
if (null !== $enumFqcn) {
40+
if (!enum_exists($enumFqcn)) {
41+
throw new \InvalidArgumentException(\sprintf('The "%s" enum does not exist.', $enumFqcn));
42+
}
43+
44+
$values = $enumFqcn::cases();
45+
$this->enumFqcn = $enumFqcn;
46+
}
47+
3148
foreach ($values as $value) {
3249
if (null === $value || \is_scalar($value)) {
3350
continue;
@@ -51,11 +68,20 @@ public function getValues(): array
5168
return $this->values;
5269
}
5370

71+
public function getEnumFqcn(): ?string
72+
{
73+
return $this->enumFqcn;
74+
}
75+
5476
/**
5577
* @internal
5678
*/
5779
public function getPermissibleValues(string $separator): string
5880
{
81+
if ($this->enumFqcn && is_a($this->enumFqcn, \BackedEnum::class, true)) {
82+
return implode($separator, array_column($this->enumFqcn::cases(), 'value'));
83+
}
84+
5985
return implode($separator, array_unique(array_map(static function (mixed $value): string {
6086
if (!$value instanceof \UnitEnum) {
6187
return json_encode($value);
@@ -78,13 +104,48 @@ protected function finalizeValue(mixed $value): mixed
78104
{
79105
$value = parent::finalizeValue($value);
80106

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());
107+
if ($this->enumFqcn) {
108+
if (is_a($this->enumFqcn, \BackedEnum::class, true)) {
109+
if (\is_string($value) || \is_int($value)) {
110+
try {
111+
$case = $this->enumFqcn::tryFrom($value);
112+
} catch (\TypeError) {
113+
throw new InvalidConfigurationException(\sprintf('The value could not be casted to a case of the "%s" enum. Is the value the same type as the backing type of the enum?', $this->enumFqcn));
114+
}
115+
116+
if (null !== $case) {
117+
return $case;
118+
}
119+
} elseif ($value instanceof \UnitEnum && !$value instanceof $this->enumFqcn) {
120+
throw new InvalidConfigurationException(\sprintf('The value should be part of the "%s" enum, got a value from the "%s" enum.', $this->enumFqcn, get_debug_type($value)));
121+
}
122+
}
123+
124+
if ($value instanceof $this->enumFqcn) {
125+
return $value;
126+
}
84127

85-
throw $ex;
128+
throw $this->createInvalidValueException($value);
129+
}
130+
131+
if (!\in_array($value, $this->values, true)) {
132+
throw $this->createInvalidValueException($value);
86133
}
87134

88135
return $value;
89136
}
137+
138+
private function createInvalidValueException(mixed $value): InvalidConfigurationException
139+
{
140+
if ($this->enumFqcn) {
141+
$message = \sprintf('The value %s is not allowed for path "%s". Permissible values: %s (cases of the "%s" enum).', json_encode($value), $this->getPath(), $this->getPermissibleValues(', '), $this->enumFqcn);
142+
} else {
143+
$message = \sprintf('The value %s is not allowed for path "%s". Permissible values: %s.', json_encode($value), $this->getPath(), $this->getPermissibleValues(', '));
144+
}
145+
146+
$ex = new InvalidConfigurationException($message);
147+
$ex->setPath($this->getPath());
148+
149+
return $ex;
150+
}
90151
}

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

+2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
return static function (PrimitiveTypesConfig $config) {
1515
$config->booleanNode(true);
1616
$config->enumNode('foo');
17+
$config->fqcnEnumNode('bar');
18+
$config->fqcnUnitEnumNode(\Symfony\Component\Config\Tests\Fixtures\TestEnum::Bar);
1719
$config->floatNode(47.11);
1820
$config->integerNode(1337);
1921
$config->scalarNode('foobar');

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

+2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
return [
1313
'boolean_node' => true,
1414
'enum_node' => 'foo',
15+
'fqcn_enum_node' => 'bar',
16+
'fqcn_unit_enum_node' => \Symfony\Component\Config\Tests\Fixtures\TestEnum::Bar,
1517
'float_node' => 47.11,
1618
'integer_node' => 1337,
1719
'scalar_node' => 'foobar',

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

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

1819
class PrimitiveTypes implements ConfigurationInterface
@@ -25,6 +26,8 @@ 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(StringBackedTestEnum::class)->end()
30+
->enumNode('fqcn_unit_enum_node')->enumFqcn(TestEnum::class)->end()
2831
->floatNode('float_node')->end()
2932
->integerNode('integer_node')->end()
3033
->scalarNode('scalar_node')->end()

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

+46
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ class PrimitiveTypesConfig implements \Symfony\Component\Config\Builder\ConfigBu
1212
{
1313
private $booleanNode;
1414
private $enumNode;
15+
private $fqcnEnumNode;
16+
private $fqcnUnitEnumNode;
1517
private $floatNode;
1618
private $integerNode;
1719
private $scalarNode;
@@ -44,6 +46,32 @@ public function enumNode($value): static
4446
return $this;
4547
}
4648

49+
/**
50+
* @default null
51+
* @param ParamConfigurator|\Symfony\Component\Config\Tests\Fixtures\StringBackedTestEnum::Foo|\Symfony\Component\Config\Tests\Fixtures\StringBackedTestEnum::Bar $value
52+
* @return $this
53+
*/
54+
public function fqcnEnumNode($value): static
55+
{
56+
$this->_usedProperties['fqcnEnumNode'] = true;
57+
$this->fqcnEnumNode = $value;
58+
59+
return $this;
60+
}
61+
62+
/**
63+
* @default null
64+
* @param ParamConfigurator|\Symfony\Component\Config\Tests\Fixtures\TestEnum::Foo|\Symfony\Component\Config\Tests\Fixtures\TestEnum::Bar|\Symfony\Component\Config\Tests\Fixtures\TestEnum::Ccc $value
65+
* @return $this
66+
*/
67+
public function fqcnUnitEnumNode($value): static
68+
{
69+
$this->_usedProperties['fqcnUnitEnumNode'] = true;
70+
$this->fqcnUnitEnumNode = $value;
71+
72+
return $this;
73+
}
74+
4775
/**
4876
* @default null
4977
* @param ParamConfigurator|float $value
@@ -115,6 +143,18 @@ public function __construct(array $value = [])
115143
unset($value['enum_node']);
116144
}
117145

146+
if (array_key_exists('fqcn_enum_node', $value)) {
147+
$this->_usedProperties['fqcnEnumNode'] = true;
148+
$this->fqcnEnumNode = $value['fqcn_enum_node'];
149+
unset($value['fqcn_enum_node']);
150+
}
151+
152+
if (array_key_exists('fqcn_unit_enum_node', $value)) {
153+
$this->_usedProperties['fqcnUnitEnumNode'] = true;
154+
$this->fqcnUnitEnumNode = $value['fqcn_unit_enum_node'];
155+
unset($value['fqcn_unit_enum_node']);
156+
}
157+
118158
if (array_key_exists('float_node', $value)) {
119159
$this->_usedProperties['floatNode'] = true;
120160
$this->floatNode = $value['float_node'];
@@ -153,6 +193,12 @@ public function toArray(): array
153193
if (isset($this->_usedProperties['enumNode'])) {
154194
$output['enum_node'] = $this->enumNode;
155195
}
196+
if (isset($this->_usedProperties['fqcnEnumNode'])) {
197+
$output['fqcn_enum_node'] = $this->fqcnEnumNode;
198+
}
199+
if (isset($this->_usedProperties['fqcnUnitEnumNode'])) {
200+
$output['fqcn_unit_enum_node'] = $this->fqcnUnitEnumNode;
201+
}
156202
if (isset($this->_usedProperties['floatNode'])) {
157203
$output['float_node'] = $this->floatNode;
158204
}

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

+23-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313

1414
use PHPUnit\Framework\TestCase;
1515
use Symfony\Component\Config\Definition\Builder\EnumNodeDefinition;
16+
use Symfony\Component\Config\Tests\Fixtures\StringBackedTestEnum;
17+
use Symfony\Component\Config\Tests\Fixtures\TestEnum;
1618

1719
class EnumNodeDefinitionTest extends TestCase
1820
{
@@ -25,14 +27,34 @@ public function testWithOneValue()
2527
$this->assertEquals(['foo'], $node->getValues());
2628
}
2729

30+
public function testWithUnitEnumFqcn()
31+
{
32+
$def = new EnumNodeDefinition('foo');
33+
$def->enumFqcn(TestEnum::class);
34+
35+
$node = $def->getNode();
36+
$this->assertEquals(TestEnum::class, $node->getEnumFqcn());
37+
}
38+
2839
public function testNoValuesPassed()
2940
{
3041
$this->expectException(\RuntimeException::class);
31-
$this->expectExceptionMessage('You must call ->values() on enum nodes.');
42+
$this->expectExceptionMessage('You must call either ->values() or ->enumFqcn() on enum nodes.');
3243
$def = new EnumNodeDefinition('foo');
3344
$def->getNode();
3445
}
3546

47+
public function testBothValuesAndEnumFqcnPassed()
48+
{
49+
$this->expectException(\RuntimeException::class);
50+
$this->expectExceptionMessage('You must call either ->values() or ->enumFqcn() on enum nodes but not both.');
51+
$def = new EnumNodeDefinition('foo');
52+
$def->values([123])
53+
->enumFqcn(StringBackedTestEnum::class);
54+
55+
$def->getNode();
56+
}
57+
3658
public function testWithNoValues()
3759
{
3860
$this->expectException(\InvalidArgumentException::class);

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

+4
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ 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 -->
46+
<!-- unit-enum-with-class: One of Symfony\Component\Config\Tests\Fixtures\TestEnum::Foo; Symfony\Component\Config\Tests\Fixtures\TestEnum::Bar; Symfony\Component\Config\Tests\Fixtures\TestEnum::Ccc -->
4547
<!-- variable: Example: foo, bar -->
4648
<config
4749
boolean="true"
@@ -58,6 +60,8 @@ private function getConfigurationAsString()
5860
node-with-a-looong-name=""
5961
enum-with-default="this"
6062
enum=""
63+
enum-with-class=""
64+
unit-enum-with-class=""
6165
variable=""
6266
custom-node="true"
6367
>

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

+2
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ 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
107+
unit_enum_with_class: ~ # One of Symfony\Component\Config\Tests\Fixtures\TestEnum::Foo; Symfony\Component\Config\Tests\Fixtures\TestEnum::Bar; Symfony\Component\Config\Tests\Fixtures\TestEnum::Ccc
106108
107109
# some info
108110
array:

0 commit comments

Comments
 (0)
0