Skip to content

Commit 6529b5a

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

File tree

15 files changed

+337
-11
lines changed

15 files changed

+337
-11
lines changed

src/Symfony/Component/Config/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ CHANGELOG
66

77
* Add `ExprBuilder::ifFalse()`
88
* Add support for info on `ArrayNodeDefinition::canBeEnabled()` and `ArrayNodeDefinition::canBeDisabled()`
9+
* Allow using an enum FQCN with `EnumNode`
910

1011
7.2
1112
---

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

Lines changed: 24 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,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

Lines changed: 78 additions & 7 deletions
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 (is_subclass_of($this->enumFqcn, \BackedEnum::class)) {
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,58 @@ 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 (!\in_array($value, $this->values, true)) {
109+
throw $this->createInvalidValueException($value);
110+
}
84111

85-
throw $ex;
112+
return $value;
86113
}
87114

88-
return $value;
115+
if ($value instanceof $this->enumFqcn) {
116+
return $value;
117+
}
118+
119+
if (!is_subclass_of($this->enumFqcn, \BackedEnum::class)) {
120+
// value is not an instance of the enum, and the enum is not
121+
// backed, meaning no cast is possible
122+
throw $this->createInvalidValueException($value);
123+
}
124+
125+
if (\is_string($value) || \is_int($value)) {
126+
try {
127+
$case = $this->enumFqcn::tryFrom($value);
128+
} catch (\TypeError) {
129+
throw new InvalidConfigurationException(\sprintf('The value could not be cast to a case of the "%s" enum. Is the value the same type as the backing type of the enum?', $this->enumFqcn));
130+
}
131+
132+
if (null !== $case) {
133+
return $case;
134+
}
135+
} elseif ($value instanceof \UnitEnum && !$value instanceof $this->enumFqcn) {
136+
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)));
137+
}
138+
139+
throw $this->createInvalidValueException($value);
140+
}
141+
142+
private function createInvalidValueException(mixed $value): InvalidConfigurationException
143+
{
144+
$displayValue = match (true) {
145+
\is_int($value) => $value,
146+
\is_string($value) => \sprintf('"%s"', $value),
147+
default => \sprintf('of type "%s"', get_debug_type($value)),
148+
};
149+
150+
if ($this->enumFqcn) {
151+
$message = \sprintf('The value %s is not allowed for path "%s". Permissible values: %s (cases of the "%s" enum).', $displayValue, $this->getPath(), $this->getPermissibleValues(', '), $this->enumFqcn);
152+
} else {
153+
$message = \sprintf('The value %s is not allowed for path "%s". Permissible values: %s.', $displayValue, $this->getPath(), $this->getPermissibleValues(', '));
154+
}
155+
156+
$e = new InvalidConfigurationException($message);
157+
$e->setPath($this->getPath());
158+
159+
return $e;
89160
}
90161
}

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

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

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

Lines changed: 3 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\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

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

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

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

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