Skip to content

Commit

Permalink
[Config] Allow enum values in EnumNode
Browse files Browse the repository at this point in the history
  • Loading branch information
fancyweb committed Jan 26, 2023
1 parent e480a66 commit defd51d
Show file tree
Hide file tree
Showing 13 changed files with 86 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,7 @@ private function getComment(BaseNode $node): string
}

if ($node instanceof EnumNode) {
$comment .= sprintf(' * @param ParamConfigurator|%s $value', implode('|', array_unique(array_map(fn ($a) => var_export($a, true), $node->getValues()))))."\n";
$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";
} else {
$parameterTypes = $this->getParameterTypes($node);
$comment .= ' * @param ParamConfigurator|'.implode('|', $parameterTypes).' $value'."\n";
Expand Down
5 changes: 5 additions & 0 deletions src/Symfony/Component/Config/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
CHANGELOG
=========

6.3
---

* Allow enum values in `EnumNode`

6.2
---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ private function writeNode(NodeInterface $node, int $depth = 0, bool $root = fal
FloatNode::class,
IntegerNode::class => 'numeric value',
BooleanNode::class => 'true|false',
EnumNode::class => implode('|', array_unique(array_map('json_encode', $prototype->getValues()))),
EnumNode::class => $prototype->getPermissibleValues('|'),
default => 'value',
};
}
Expand Down Expand Up @@ -149,7 +149,7 @@ private function writeNode(NodeInterface $node, int $depth = 0, bool $root = fal
}

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

if (\count($comments)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ private function writeNode(NodeInterface $node, NodeInterface $parentNode = null
}
}
} elseif ($node instanceof EnumNode) {
$comments[] = 'One of '.implode('; ', array_unique(array_map('json_encode', $node->getValues())));
$comments[] = 'One of '.$node->getPermissibleValues('; ');
$default = $node->hasDefaultValue() ? Inline::dump($node->getDefaultValue()) : '~';
} elseif (VariableNode::class === $node::class && \is_array($example)) {
// If there is an array example, we are sure we dont need to print a default value
Expand Down
37 changes: 34 additions & 3 deletions src/Symfony/Component/Config/Definition/EnumNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,16 @@ public function __construct(?string $name, NodeInterface $parent = null, array $
}

foreach ($values as $value) {
if (null !== $value && !\is_scalar($value)) {
throw new \InvalidArgumentException(sprintf('"%s" only supports scalar or null values, "%s" given.', __CLASS__, get_debug_type($value)));
if (null === $value || \is_scalar($value)) {
continue;
}

if (!$value instanceof \UnitEnum) {
throw new \InvalidArgumentException(sprintf('"%s" only supports scalar, enum, or null values, "%s" given.', __CLASS__, get_debug_type($value)));
}

if ($value::class !== ($enumClass ??= $value::class)) {
throw new \InvalidArgumentException(sprintf('"%s" only supports one type of enum, "%s" and "%s" passed.', __CLASS__, $enumClass, $value::class));
}
}

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

/**
* @internal
*/
public function getPermissibleValues(string $separator): string
{
return implode($separator, array_unique(array_map(static function (mixed $value): string {
if (!$value instanceof \UnitEnum) {
return json_encode($value);
}

return ltrim(var_export($value, true), '\\');
}, $this->values)));
}

protected function validateType(mixed $value)
{
if ($value instanceof \UnitEnum) {
return;
}

parent::validateType($value);
}

protected function finalizeValue(mixed $value): mixed
{
$value = parent::finalizeValue($value);

if (!\in_array($value, $this->values, true)) {
$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)))));
$ex = new InvalidConfigurationException(sprintf('The value %s is not allowed for path "%s". Permissible values: %s', json_encode($value), $this->getPath(), $this->getPermissibleValues(', ')));
$ex->setPath($this->getPath());

throw $ex;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\Config\Tests\Fixtures\TestEnum;

class PrimitiveTypes implements ConfigurationInterface
{
Expand All @@ -23,7 +24,7 @@ public function getConfigTreeBuilder(): TreeBuilder
$rootNode
->children()
->booleanNode('boolean_node')->end()
->enumNode('enum_node')->values(['foo', 'bar', 'baz'])->end()
->enumNode('enum_node')->values(['foo', 'bar', 'baz', TestEnum::Bar])->end()
->floatNode('float_node')->end()
->integerNode('integer_node')->end()
->scalarNode('scalar_node')->end()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public function booleanNode($value): static

/**
* @default null
* @param ParamConfigurator|'foo'|'bar'|'baz' $value
* @param ParamConfigurator|'foo'|'bar'|'baz'|\Symfony\Component\Config\Tests\Fixtures\TestEnum::Bar $value
* @return $this
*/
public function enumNode($value): static
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ private function getConfigurationAsString()
<!-- scalar-deprecated: Deprecated (Since vendor/package 1.1: The child node "scalar_deprecated" at path "acme_root" is deprecated.) -->
<!-- scalar-deprecated-with-message: Deprecated (Since vendor/package 1.1: Deprecation custom message for "scalar_deprecated_with_message" at "acme_root") -->
<!-- enum-with-default: One of "this"; "that" -->
<!-- enum: One of "this"; "that" -->
<!-- enum: One of "this"; "that"; Symfony\Component\Config\Tests\Fixtures\TestEnum::Ccc -->
<!-- variable: Example: foo, bar -->
<config
boolean="true"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ private function getConfigurationAsString(): string
scalar_deprecated_with_message: ~ # Deprecated (Since vendor/package 1.1: Deprecation custom message for "scalar_deprecated_with_message" at "acme_root")
node_with_a_looong_name: ~
enum_with_default: this # One of "this"; "that"
enum: ~ # One of "this"; "that"
enum: ~ # One of "this"; "that"; Symfony\Component\Config\Tests\Fixtures\TestEnum::Ccc
# some info
array:
Expand Down
21 changes: 16 additions & 5 deletions src/Symfony/Component/Config/Tests/Definition/EnumNodeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,16 @@
use PHPUnit\Framework\TestCase;
use Symfony\Component\Config\Definition\EnumNode;
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
use Symfony\Component\Config\Tests\Fixtures\TestEnum;
use Symfony\Component\Config\Tests\Fixtures\TestEnum2;

class EnumNodeTest extends TestCase
{
public function testFinalizeValue()
{
$node = new EnumNode('foo', null, ['foo', 'bar']);
$node = new EnumNode('foo', null, ['foo', 'bar', TestEnum::Bar]);
$this->assertSame('foo', $node->finalize('foo'));
$this->assertSame(TestEnum::Bar, $node->finalize(TestEnum::Bar));
}

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

Expand Down Expand Up @@ -80,11 +83,19 @@ public function testSameStringCoercedValuesAreDifferent()
$this->assertNull($node->finalize(null));
}

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

new EnumNode('ccc', null, [new \stdClass()]);
}

public function testTwoDifferentEnumsThrows()
{
$this->expectException(\InvalidArgumentException::class);
$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.');

new EnumNode('ccc', null, [...TestEnum::cases(), TestEnum2::Ccc]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\Config\Tests\Fixtures\TestEnum;

class ExampleConfiguration implements ConfigurationInterface
{
Expand All @@ -38,7 +39,7 @@ public function getConfigTreeBuilder(): TreeBuilder
->scalarNode('scalar_deprecated_with_message')->setDeprecated('vendor/package', '1.1', 'Deprecation custom message for "%node%" at "%path%"')->end()
->scalarNode('node_with_a_looong_name')->end()
->enumNode('enum_with_default')->values(['this', 'that'])->defaultValue('this')->end()
->enumNode('enum')->values(['this', 'that'])->end()
->enumNode('enum')->values(['this', 'that', TestEnum::Ccc])->end()
->arrayNode('array')
->info('some info')
->canBeUnset()
Expand Down
10 changes: 10 additions & 0 deletions src/Symfony/Component/Config/Tests/Fixtures/TestEnum.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace Symfony\Component\Config\Tests\Fixtures;

enum TestEnum
{
case Foo;
case Bar;
case Ccc;
}
10 changes: 10 additions & 0 deletions src/Symfony/Component/Config/Tests/Fixtures/TestEnum2.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace Symfony\Component\Config\Tests\Fixtures;

enum TestEnum2
{
case Foo;
case Bar;
case Ccc;
}

0 comments on commit defd51d

Please sign in to comment.