Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/Symfony/Component/Validator/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
CHANGELOG
=========

7.3
---
* Add the `BackedEnumValue` constraint

7.2
---

Expand Down
59 changes: 59 additions & 0 deletions src/Symfony/Component/Validator/Constraints/BackedEnumValue.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Validator\Constraints;

use Symfony\Component\Validator\Attribute\HasNamedArguments;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;

/**
* Validates that a backed enum can be hydrated from a value.
*
* @author Aurélien Pillevesse <[email protected]>
*/
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
class BackedEnumValue extends Constraint
{
public const NO_SUCH_VALUE_ERROR = '53dcc1b1-a8dd-4813-baa5-b8486ff56447';
public const INVALID_TYPE_ERROR = 'aa0374f4-b3ab-4362-b48d-b5ecf0f1a02d';

protected const ERROR_NAMES = [
self::NO_SUCH_VALUE_ERROR => 'NO_SUCH_VALUE_ERROR',
self::INVALID_TYPE_ERROR => 'INVALID_TYPE_ERROR',
];

/**
* @param class-string<\BackedEnum> $type the type of the enum
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* @param class-string<\BackedEnum> $type the type of the enum
* @param class-string<\BackedEnum> $className the enum class name

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

first letter should be uppercase also

* @param \BackedEnum[] $except the cases that should be considered invalid
*/
#[HasNamedArguments]
public function __construct(
public string $type,
public array $except = [],
public string $message = 'The value you selected is not a valid choice.',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public string $message = 'The value you selected is not a valid choice.',
public string $invalidValueMessage = 'The value is not a backing value of {{ className }}.',

public string $typeMessage = 'This value should be of type {{ type }}.',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public string $typeMessage = 'This value should be of type {{ type }}.',
public string $invalidValueTypeMessage = 'The value should be of type {{ type }}.',

?array $groups = null,
mixed $payload = null,
) {
parent::__construct([], $groups, $payload);

if (!is_a($type, \BackedEnum::class, true)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (!is_a($type, \BackedEnum::class, true)) {
if (!is_subclass_of($type, \BackedEnum::class, true)) {

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is_a() looks okay to me?

throw new ConstraintDefinitionException(\sprintf('The "type" must be a \BackedEnum, got "%s".', get_debug_type($type)));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
throw new ConstraintDefinitionException(\sprintf('The "type" must be a \BackedEnum, got "%s".', get_debug_type($type)));
throw new ConstraintDefinitionException(\sprintf('The "type" must be a "\BackedEnum", got "%s".', get_debug_type($type)));

}

foreach ($except as $exceptValue) {
if (!is_a($exceptValue, $type)) {
throw new ConstraintDefinitionException(\sprintf('The "except" values must be cases of enum "%s", got "%s".', $type, get_debug_type($exceptValue)));
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Validator\Constraints;

use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;

/**
* BackedEnumValueValidator validates that a backed enum case can be hydrated from a value.
*
* @author Aurélien Pillevesse <[email protected]>
*/
class BackedEnumValueValidator extends ConstraintValidator
{
public function validate(mixed $value, Constraint $constraint): void
{
if (!$constraint instanceof BackedEnumValue) {
throw new UnexpectedTypeException($constraint, BackedEnumValue::class);
}

if (null === $value || '' === $value) {
return;
}

try {
$enumTypeValue = $constraint->type::tryFrom($value);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
$enumTypeValue = $constraint->type::tryFrom($value);
$enumValue = $constraint->type::tryFrom($value);

} catch (\TypeError) {
$this->context->buildViolation($constraint->typeMessage)
->setParameter('{{ type }}', $this->formatValue((string) (new \ReflectionEnum($constraint->type))->getBackingType()))
->setCode(BackedEnumValue::INVALID_TYPE_ERROR)
->addViolation();

return;
}

if (null === $enumTypeValue) {
$this->context->buildViolation($constraint->message)
->setParameter('{{ value }}', $this->formatValue($value))
->setParameter('{{ choices }}', $this->formatValidCases($constraint))
Comment on lines +48 to +49
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@derrabus is right, you only need to define what is in the constraint message

Copy link
Contributor Author

@AurelienPillevesse AurelienPillevesse Dec 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's build in a similar way as Choice constraint.

Message :

public string $message = 'The value you selected is not a valid choice.';

Validator :

$this->context->buildViolation($constraint->message)
->setParameter('{{ value }}', $this->formatValue($value))
->setParameter('{{ choices }}', $this->formatValues($choices))
->setCode(Choice::NO_SUCH_CHOICE_ERROR)
->addViolation();

The value or choice values are not used by default. If you override the default message and use {{ value }} or {{ choice }} on it, it will be replaced in your new message.

->setCode(BackedEnumValue::NO_SUCH_VALUE_ERROR)
->addViolation();

return;
}

if (\count($constraint->except) > 0 && \in_array($enumTypeValue, $constraint->except, true)) {
$this->context->buildViolation($constraint->message)
->setParameter('{{ value }}', $this->formatValue($enumTypeValue->value))
->setParameter('{{ choices }}', $this->formatValidCases($constraint))
->setCode(BackedEnumValue::NO_SUCH_VALUE_ERROR)
->addViolation();
}
}

private function formatValidCases(BackedEnumValue $constraint): string
{
return $this->formatValues(array_map(
static fn (\BackedEnum $case) => $case->value,
array_filter(
$constraint->type::cases(),
static fn (\BackedEnum $currentValue) => !\in_array($currentValue, $constraint->except, true),
)
));
Comment on lines +67 to +73
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return $this->formatValues(array_map(
static fn (\BackedEnum $case) => $case->value,
array_filter(
$constraint->type::cases(),
static fn (\BackedEnum $currentValue) => !\in_array($currentValue, $constraint->except, true),
)
));
return $this->formatValues(array_column(
array_filter(
$constraint->type::cases(),
static fn (\BackedEnum $currentValue) => !\in_array($currentValue, $constraint->except, true),
),
'value',
));

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Validator\Tests\Constraints;

use PHPUnit\Framework\TestCase;
use Symfony\Component\Validator\Constraints\BackedEnumValue;
use Symfony\Component\Validator\Mapping\ClassMetadata;
use Symfony\Component\Validator\Mapping\Loader\AttributeLoader;

/**
* @author Aurélien Pillevesse <[email protected]>
*/
class BackedEnumValueTest extends TestCase
{
public function testAttributes()
{
$metadata = new ClassMetadata(EnumDummy::class);
$loader = new AttributeLoader();
self::assertTrue($loader->loadClassMetadata($metadata));

/** @var BackedEnumValue $aConstraint */
[$aConstraint] = $metadata->properties['a']->getConstraints();
self::assertSame(MyStringEnum::class, $aConstraint->type);

/** @var BackedEnumValue $bConstraint */
[$bConstraint] = $metadata->properties['b']->getConstraints();
self::assertSame(MyStringEnum::class, $aConstraint->type);
self::assertSame('myMessage', $bConstraint->message);

/** @var BackedEnumValue $cConstraint */
[$cConstraint] = $metadata->properties['c']->getConstraints();
self::assertSame(MyStringEnum::class, $aConstraint->type);
self::assertSame(['my_group'], $cConstraint->groups);
self::assertSame('some attached data', $cConstraint->payload);

/** @var BackedEnumValue $dConstraint */
[$dConstraint] = $metadata->properties['d']->getConstraints();
self::assertSame(MyStringEnum::class, $dConstraint->type);
self::assertSame([MyStringEnum::YES], $dConstraint->except);
}
}

class EnumDummy
{
#[BackedEnumValue(type: MyStringEnum::class)]
private $a;

#[BackedEnumValue(type: MyStringEnum::class, message: 'myMessage')]
private $b;

#[BackedEnumValue(type: MyStringEnum::class, groups: ['my_group'], payload: 'some attached data')]
private $c;

#[BackedEnumValue(type: MyStringEnum::class, except: [MyStringEnum::YES])]
private $d;
}

enum MyStringEnum: string
{
case YES = 'yes';
case NO = 'no';
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Validator\Tests\Constraints;

use Symfony\Component\Validator\Constraints\BackedEnumValue;
use Symfony\Component\Validator\Constraints\BackedEnumValueValidator;
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;

/**
* @author Aurélien Pillevesse <[email protected]>
*/
class BackedEnumValueValidatorTest extends ConstraintValidatorTestCase
{
protected function createValidator(): BackedEnumValueValidator
{
return new BackedEnumValueValidator();
}

public function testExpectEnumForTypeAttribute()
{
$this->expectException(ConstraintDefinitionException::class);
$this->expectExceptionMessage('The "type" must be a \BackedEnum, got "string".');
new BackedEnumValue(
type: self::class
);
}

public function testNullIsValid()
{
$this->validator->validate(
null,
new BackedEnumValue(
type: MyStringBackedEnum::class
)
);

$this->assertNoViolation();
}

public function testEmptyStringIsValid()
{
$this->validator->validate(
'',
new BackedEnumValue(
type: MyStringBackedEnum::class
)
);

$this->assertNoViolation();
}

public function testStringEnumValid()
{
$this->validator->validate(
'yes',
new BackedEnumValue(
type: MyStringBackedEnum::class
)
);

$this->assertNoViolation();
}

public function testStringEnumWrongValue()
{
$this->validator->validate('wrongvalue', new BackedEnumValue(type: MyStringBackedEnum::class));

$this->buildViolation('The value you selected is not a valid choice.')
->setParameter('{{ value }}', '"wrongvalue"')
->setParameter('{{ choices }}', '"yes", "no"')
->setCode(BackedEnumValue::NO_SUCH_VALUE_ERROR)
->assertRaised();
}

public function testStringEnumWrongValueWithExcept()
{
$this->validator->validate('no', new BackedEnumValue(type: MyStringBackedEnum::class, except: [MyStringBackedEnum::NO]));

$this->buildViolation('The value you selected is not a valid choice.')
->setParameter('{{ value }}', '"no"')
->setParameter('{{ choices }}', '"yes"')
->setCode(BackedEnumValue::NO_SUCH_VALUE_ERROR)
->assertRaised();
}

public function testIntEnumValid()
{
$this->validator->validate(
1,
new BackedEnumValue(
type: MyIntBackedEnum::class
)
);

$this->assertNoViolation();
}

public function testIntEnumWithStringIntSubmitted()
{
$this->validator->validate(
'1',
new BackedEnumValue(
type: MyIntBackedEnum::class
)
);

$this->assertNoViolation();
}

public function testIntEnumNotValidWithBoolValue()
{
$this->validator->validate(
'bonjour',
new BackedEnumValue(
type: MyIntBackedEnum::class
)
);

$this->buildViolation('This value should be of type {{ type }}.')
->setParameter('{{ type }}', '"int"')
->setCode(BackedEnumValue::INVALID_TYPE_ERROR)
->assertRaised();
}
}

enum MyStringBackedEnum: string
{
case YES = 'yes';
case NO = 'no';
}

enum MyIntBackedEnum: int
{
case YES = 1;
case NO = 0;
}