Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 8f76b52

Browse filesBrowse files
committed
[Validator] Add support for enum in Choice constraint
1 parent 04551ad commit 8f76b52
Copy full SHA for 8f76b52

11 files changed

+226
-21
lines changed

‎src/Symfony/Component/Validator/CHANGELOG.md

Copy file name to clipboardExpand all lines: src/Symfony/Component/Validator/CHANGELOG.md
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ CHANGELOG
5656
```
5757
* Add support for ratio checks for SVG files to the `Image` constraint
5858
* Add the `Slug` constraint
59+
* Add support for enums in `Choice` constraint
5960

6061
7.2
6162
---

‎src/Symfony/Component/Validator/ConstraintValidator.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Validator/ConstraintValidator.php
+1-1Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ protected function formatValue(mixed $value, int $format = 0): string
8686
}
8787

8888
if ($value instanceof \UnitEnum) {
89-
return $value->name;
89+
return $value instanceof \BackedEnum ? $value->value : $value->name;
9090
}
9191

9292
if (\is_object($value)) {

‎src/Symfony/Component/Validator/Constraints/Choice.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Validator/Constraints/Choice.php
+16-10Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
* Validates that a value is one of a given set of valid choices.
1919
*
2020
* @author Bernhard Schussek <bschussek@gmail.com>
21+
* @author Ninos Ego <me@ninosego.de>
2122
*/
2223
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
2324
class Choice extends Constraint
@@ -32,7 +33,8 @@ class Choice extends Constraint
3233
self::TOO_MANY_ERROR => 'TOO_MANY_ERROR',
3334
];
3435

35-
public ?array $choices = null;
36+
/** @var \class-string|array|null */
37+
public string|array|null $choices = null;
3638
/** @var callable|string|null */
3739
public $callback;
3840
public bool $multiple = false;
@@ -45,25 +47,27 @@ class Choice extends Constraint
4547
public string $maxMessage = 'You must select at most {{ limit }} choice.|You must select at most {{ limit }} choices.';
4648
public bool $match = true;
4749

50+
public ?\Closure $normalizer;
51+
4852
public function getDefaultOption(): ?string
4953
{
5054
return 'choices';
5155
}
5256

5357
/**
54-
* @param array|null $choices An array of choices (required unless a callback is specified)
55-
* @param callable|string|null $callback Callback method to use instead of the choice option to get the choices
56-
* @param bool|null $multiple Whether to expect the value to be an array of valid choices (defaults to false)
57-
* @param bool|null $strict This option defaults to true and should not be used
58-
* @param int<0, max>|null $min Minimum of valid choices if multiple values are expected
59-
* @param positive-int|null $max Maximum of valid choices if multiple values are expected
60-
* @param string[]|null $groups
61-
* @param bool|null $match Whether to validate the values are part of the choices or not (defaults to true)
58+
* @param \class-string|array|null $choices An enum or array of choices (required unless a callback is specified)
59+
* @param callable|string|null $callback Callback method to use instead of the choice option to get the choices
60+
* @param bool|null $multiple Whether to expect the value to be an array of valid choices (defaults to false)
61+
* @param bool|null $strict This option defaults to true and should not be used
62+
* @param int<0, max>|null $min Minimum of valid choices if multiple values are expected
63+
* @param positive-int|null $max Maximum of valid choices if multiple values are expected
64+
* @param string[]|null $groups
65+
* @param bool|null $match Whether to validate the values are part of the choices or not (defaults to true)
6266
*/
6367
#[HasNamedArguments]
6468
public function __construct(
6569
string|array $options = [],
66-
?array $choices = null,
70+
string|array|null $choices = null,
6771
callable|string|null $callback = null,
6872
?bool $multiple = null,
6973
?bool $strict = null,
@@ -73,6 +77,7 @@ public function __construct(
7377
?string $multipleMessage = null,
7478
?string $minMessage = null,
7579
?string $maxMessage = null,
80+
?callable $normalizer = null,
7681
?array $groups = null,
7782
mixed $payload = null,
7883
?bool $match = null,
@@ -100,5 +105,6 @@ public function __construct(
100105
$this->minMessage = $minMessage ?? $this->minMessage;
101106
$this->maxMessage = $maxMessage ?? $this->maxMessage;
102107
$this->match = $match ?? $this->match;
108+
$this->normalizer = null !== $normalizer ? $normalizer(...) : null;
103109
}
104110
}

‎src/Symfony/Component/Validator/Constraints/ChoiceValidator.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Validator/Constraints/ChoiceValidator.php
+14-3Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
* @author Fabien Potencier <fabien@symfony.com>
2525
* @author Florian Eckerstorfer <florian@eckerstorfer.org>
2626
* @author Bernhard Schussek <bschussek@gmail.com>
27+
* @author Ninos Ego <me@ninosego.de>
2728
*/
2829
class ChoiceValidator extends ConstraintValidator
2930
{
@@ -33,16 +34,24 @@ public function validate(mixed $value, Constraint $constraint): void
3334
throw new UnexpectedTypeException($constraint, Choice::class);
3435
}
3536

36-
if (!\is_array($constraint->choices) && !$constraint->callback) {
37+
if (null === $constraint->choices && !$constraint->callback) {
3738
throw new ConstraintDefinitionException('Either "choices" or "callback" must be specified on constraint Choice.');
3839
}
3940

41+
if (null !== $constraint->choices && !\is_array($constraint->choices) && (!\is_string($constraint->choices) || !\enum_exists($constraint->choices))) {
42+
throw new ConstraintDefinitionException('"choices" must be of type array or enum-class.');
43+
}
44+
4045
if (null === $value) {
4146
return;
4247
}
4348

44-
if ($constraint->multiple && !\is_array($value)) {
45-
throw new UnexpectedValueException($value, 'array');
49+
if ($constraint->multiple && !\is_array($value) && !$value instanceof \IteratorAggregate) {
50+
throw new UnexpectedValueException($value, 'array|IteratorAggregate');
51+
}
52+
53+
if (null !== $constraint->normalizer) {
54+
$value = ($constraint->normalizer)($value);
4655
}
4756

4857
if ($constraint->callback) {
@@ -56,6 +65,8 @@ public function validate(mixed $value, Constraint $constraint): void
5665
if (!\is_array($choices)) {
5766
throw new ConstraintDefinitionException(\sprintf('The Choice constraint callback "%s" is expected to return an array, but returned "%s".', trim($this->formatValue($constraint->callback), '"'), get_debug_type($choices)));
5867
}
68+
} elseif (\is_string($constraint->choices) && \enum_exists($constraint->choices)) {
69+
$choices = \array_map([$this, 'formatValue'], $constraint->choices::cases());
5970
} else {
6071
$choices = $constraint->choices;
6172
}

‎src/Symfony/Component/Validator/Constraints/Cidr.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Validator/Constraints/Cidr.php
+1-1Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ class Cidr extends Constraint
3333

3434
protected const ERROR_NAMES = [
3535
self::INVALID_CIDR_ERROR => 'INVALID_CIDR_ERROR',
36-
self::OUT_OF_RANGE_ERROR => 'OUT_OF_RANGE_VIOLATION',
36+
self::OUT_OF_RANGE_ERROR => 'OUT_OF_RANGE_ERROR',
3737
];
3838

3939
private const NET_MAXES = [

‎src/Symfony/Component/Validator/Tests/ConstraintValidatorTest.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Validator/Tests/ConstraintValidatorTest.php
+6Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
use PHPUnit\Framework\TestCase;
1515
use Symfony\Component\Validator\Constraint;
1616
use Symfony\Component\Validator\ConstraintValidator;
17+
use Symfony\Component\Validator\Tests\Constraints\Fixtures\EnumBackendInteger;
18+
use Symfony\Component\Validator\Tests\Constraints\Fixtures\EnumBackendString;
19+
use Symfony\Component\Validator\Tests\Constraints\Fixtures\EnumUnit;
1720
use Symfony\Component\Validator\Tests\Fixtures\TestEnum;
1821

1922
class ConstraintValidatorTest extends TestCase
@@ -50,6 +53,9 @@ public static function formatValueProvider(): array
5053
[class_exists(\IntlDateFormatter::class) ? static::normalizeIcuSpaces("Jan 1, 1970, 6:00\u{202F}AM") : '1970-01-01 06:00:00', new \DateTimeImmutable('1970-01-01T06:00:00Z'), ConstraintValidator::PRETTY_DATE],
5154
[class_exists(\IntlDateFormatter::class) ? static::normalizeIcuSpaces("Jan 1, 1970, 3:00\u{202F}PM") : '1970-01-01 15:00:00', (new \DateTimeImmutable('1970-01-01T23:00:00'))->setTimezone(new \DateTimeZone('America/New_York')), ConstraintValidator::PRETTY_DATE],
5255
['FirstCase', TestEnum::FirstCase],
56+
['A', EnumUnit::A],
57+
['a', EnumBackendString::A],
58+
['3', EnumBackendInteger::A],
5359
];
5460

5561
date_default_timezone_set($defaultTimezone);

‎src/Symfony/Component/Validator/Tests/Constraints/ChoiceTest.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Validator/Tests/Constraints/ChoiceTest.php
+31Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,20 @@
1515
use Symfony\Component\Validator\Constraints\Choice;
1616
use Symfony\Component\Validator\Mapping\ClassMetadata;
1717
use Symfony\Component\Validator\Mapping\Loader\AttributeLoader;
18+
use Symfony\Component\Validator\Tests\Constraints\Fixtures\EnumBackendInteger;
19+
use Symfony\Component\Validator\Tests\Constraints\Fixtures\EnumBackendString;
20+
use Symfony\Component\Validator\Tests\Constraints\Fixtures\EnumUnit;
1821
use Symfony\Component\Validator\Tests\Fixtures\ConstraintChoiceWithPreset;
1922

2023
class ChoiceTest extends TestCase
2124
{
25+
public function testNormalizerCanBeSet()
26+
{
27+
$choice = new Choice(normalizer: 'trim');
28+
29+
$this->assertEquals(trim(...), $choice->normalizer);
30+
}
31+
2232
public function testSetDefaultPropertyChoice()
2333
{
2434
$constraint = new ConstraintChoiceWithPreset('A');
@@ -52,6 +62,18 @@ public function testAttributes()
5262
/** @var Choice $stringIndexedConstraint */
5363
[$stringIndexedConstraint] = $metadata->properties['stringIndexed']->getConstraints();
5464
self::assertSame(['one' => 1, 'two' => 2], $stringIndexedConstraint->choices);
65+
66+
/** @var Choice $enumUnitConstraint */
67+
[$enumUnitConstraint] = $metadata->properties['enumUnit']->getConstraints();
68+
self::assertSame(EnumUnit::class, $enumUnitConstraint->choices);
69+
70+
/** @var Choice $enumBackendStringConstraint */
71+
[$enumBackendStringConstraint] = $metadata->properties['enumBackendString']->getConstraints();
72+
self::assertSame(EnumBackendString::class, $enumBackendStringConstraint->choices);
73+
74+
/** @var Choice $enumBackendIntegerConstraint */
75+
[$enumBackendIntegerConstraint] = $metadata->properties['enumBackendInteger']->getConstraints();
76+
self::assertSame(EnumBackendInteger::class, $enumBackendIntegerConstraint->choices);
5577
}
5678
}
5779

@@ -68,4 +90,13 @@ class ChoiceDummy
6890

6991
#[Choice(choices: ['one' => 1, 'two' => 2])]
7092
private $stringIndexed;
93+
94+
#[Choice(choices: EnumUnit::class)]
95+
private $enumUnit;
96+
97+
#[Choice(choices: EnumBackendString::class)]
98+
private $enumBackendString;
99+
100+
#[Choice(choices: EnumBackendInteger::class)]
101+
private $enumBackendInteger;
71102
}

‎src/Symfony/Component/Validator/Tests/Constraints/ChoiceValidatorTest.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Validator/Tests/Constraints/ChoiceValidatorTest.php
+93-6Lines changed: 93 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
1717
use Symfony\Component\Validator\Exception\UnexpectedValueException;
1818
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
19+
use Symfony\Component\Validator\Tests\Constraints\Fixtures\EnumBackendInteger;
20+
use Symfony\Component\Validator\Tests\Constraints\Fixtures\EnumBackendString;
21+
use Symfony\Component\Validator\Tests\Constraints\Fixtures\EnumUnit;
1922

2023
function choice_callback()
2124
{
@@ -119,8 +122,8 @@ public function testValidChoiceCallbackFunction(Choice $constraint)
119122

120123
public static function provideConstraintsWithCallbackFunction(): iterable
121124
{
122-
yield 'named arguments, namespaced function' => [new Choice(callback: __NAMESPACE__.'\choice_callback')];
123-
yield 'named arguments, closure' => [new Choice(callback: fn () => ['foo', 'bar'])];
125+
yield 'named arguments, namespaced function' => [new Choice(callback: __NAMESPACE__ . '\choice_callback')];
126+
yield 'named arguments, closure' => [new Choice(callback: fn() => ['foo', 'bar'])];
124127
yield 'named arguments, static method' => [new Choice(callback: [__CLASS__, 'staticCallback'])];
125128
}
126129

@@ -137,9 +140,9 @@ public function testValidChoiceCallbackFunctionDoctrineStyle(Choice $constraint)
137140

138141
public static function provideLegacyConstraintsWithCallbackFunctionDoctrineStyle(): iterable
139142
{
140-
yield 'doctrine style, namespaced function' => [new Choice(['callback' => __NAMESPACE__.'\choice_callback'])];
143+
yield 'doctrine style, namespaced function' => [new Choice(['callback' => __NAMESPACE__ . '\choice_callback'])];
141144
yield 'doctrine style, closure' => [new Choice([
142-
'callback' => fn () => ['foo', 'bar'],
145+
'callback' => fn() => ['foo', 'bar'],
143146
])];
144147
yield 'doctrine style, static method' => [new Choice(['callback' => [__CLASS__, 'staticCallback']])];
145148
}
@@ -232,8 +235,8 @@ public function testInvalidChoiceDoctrineStyle()
232235
public function testInvalidChoiceEmptyChoices()
233236
{
234237
$constraint = new Choice(
235-
// May happen when the choices are provided dynamically, e.g. from
236-
// the DB or the model
238+
// May happen when the choices are provided dynamically, e.g. from
239+
// the DB or the model
237240
choices: [],
238241
message: 'myMessage',
239242
);
@@ -262,6 +265,7 @@ public function testInvalidChoiceMultiple()
262265
->setCode(Choice::NO_SUCH_CHOICE_ERROR)
263266
->assertRaised();
264267
}
268+
265269
/**
266270
* @group legacy
267271
*/
@@ -443,4 +447,87 @@ public function testMatchFalseWithMultiple()
443447
->setInvalidValue('bar')
444448
->assertRaised();
445449
}
450+
451+
public function testValidEnumUnit()
452+
{
453+
$this->validator->validate('A', new Choice(
454+
choices: EnumUnit::class,
455+
message: 'myMessage',
456+
));
457+
458+
$this->assertNoViolation();
459+
}
460+
461+
public function testInvalidEnumUnit()
462+
{
463+
$this->validator->validate('C', new Choice(
464+
choices: EnumUnit::class,
465+
message: 'myMessage',
466+
));
467+
468+
$this->buildViolation('myMessage')
469+
->setParameter('{{ value }}', '"C"')
470+
->setParameter('{{ choices }}', '"A", "B"')
471+
->setCode(Choice::NO_SUCH_CHOICE_ERROR)
472+
->assertRaised();
473+
}
474+
475+
public function testValidEnumBackendString()
476+
{
477+
$this->validator->validate('a', new Choice(
478+
choices: EnumBackendString::class,
479+
message: 'myMessage',
480+
));
481+
482+
$this->assertNoViolation();
483+
}
484+
485+
public function testInvalidEnumBackendString()
486+
{
487+
$this->validator->validate('c', new Choice(
488+
choices: EnumBackendString::class,
489+
message: 'myMessage',
490+
));
491+
492+
$this->buildViolation('myMessage')
493+
->setParameter('{{ value }}', '"c"')
494+
->setParameter('{{ choices }}', '"a", "b"')
495+
->setCode(Choice::NO_SUCH_CHOICE_ERROR)
496+
->assertRaised();
497+
}
498+
499+
public function testValidEnumBackendInteger()
500+
{
501+
$this->validator->validate(3, new Choice(
502+
choices: EnumBackendInteger::class,
503+
message: 'myMessage',
504+
));
505+
506+
$this->assertNoViolation();
507+
}
508+
509+
public function testValidEnumBackendIntegerNormalize()
510+
{
511+
$this->validator->validate('3', new Choice(
512+
choices: EnumBackendInteger::class,
513+
message: 'myMessage',
514+
normalizer: 'intval',
515+
));
516+
517+
$this->assertNoViolation();
518+
}
519+
520+
public function testInvalidEnumBackendInteger()
521+
{
522+
$this->validator->validate(9, new Choice(
523+
choices: EnumBackendInteger::class,
524+
message: 'myMessage',
525+
));
526+
527+
$this->buildViolation('myMessage')
528+
->setParameter('{{ value }}', '9')
529+
->setParameter('{{ choices }}', '"3", "4"')
530+
->setCode(Choice::NO_SUCH_CHOICE_ERROR)
531+
->assertRaised();
532+
}
446533
}
+21Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Validator\Tests\Constraints\Fixtures;
13+
14+
/**
15+
* @author Ninos Ego <me@ninosego.de>
16+
*/
17+
enum EnumBackendInteger: int
18+
{
19+
case A = 3;
20+
case B = 4;
21+
}
+21Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Validator\Tests\Constraints\Fixtures;
13+
14+
/**
15+
* @author Ninos Ego <me@ninosego.de>
16+
*/
17+
enum EnumBackendString: string
18+
{
19+
case A = 'a';
20+
case B = 'b';
21+
}

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.