Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Validator] Introduce BackedEnumValue constraint #54226

Open
wants to merge 1 commit into
base: 7.1
Choose a base branch
from

Conversation

AurelienPillevesse
Copy link
Contributor

Q A
Branch? 7.1
Bug fix? no
New feature? yes
Deprecations? no
Issues -
License MIT

My use case: I receive data in my DTOs that we only want if this value exists in an Enum and reject the whole thing at validation otherwise.

@carsonbot carsonbot added Status: Needs Review Feature RFC RFC = Request For Comments (proposals about features that you want to be discussed) Validator labels Mar 10, 2024
@carsonbot carsonbot added this to the 7.1 milestone Mar 10, 2024
@carsonbot carsonbot changed the title [RFC][Validator] Introduce Enum constraint [Validator] Introduce Enum constraint Mar 10, 2024
@AurelienPillevesse AurelienPillevesse changed the title [Validator] Introduce Enum constraint [Validator] Introduce Enum constraint Mar 10, 2024
@derrabus
Copy link
Member

Did you read the discussion on #43047?

@AurelienPillevesse
Copy link
Contributor Author

Didn't see it before, my bad

@AurelienPillevesse
Copy link
Contributor Author

It's a dedicated constraint so why not but I understand if your point of view is to not accept it and adding the code below (as mentioned in the other discussion) if people want to accept an Enum value with #[MapRequestPayload] for example :

public static function values(): array
{
    return array_column(self::cases(), 'value');
}

Because you are speaking about this topic, can you maybe give me feedback about this PR : symfony/symfony-docs#19590 (if it's added in the documentation, we will probably less try to find a way to achieve it)

@derrabus
Copy link
Member

I understand if your point of view is to not accept it

I didn't say that. It's just that there has been a PR on that topic a while ago, and if we revisit the topic, we should take that prior discussion into account.

src/Symfony/Component/Validator/ConstraintValidator.php Outdated Show resolved Hide resolved
src/Symfony/Component/Validator/Constraints/Enum.php Outdated Show resolved Hide resolved
src/Symfony/Component/Validator/Constraints/Enum.php Outdated Show resolved Hide resolved
src/Symfony/Component/Validator/Constraints/Enum.php Outdated Show resolved Hide resolved
src/Symfony/Component/Validator/Constraints/Enum.php Outdated Show resolved Hide resolved
@AurelienPillevesse AurelienPillevesse changed the title [Validator] Introduce Enum constraint [Validator] Introduce BackedEnumValue constraint Mar 11, 2024
Copy link
Member

@derrabus derrabus left a comment

Choose a reason for hiding this comment

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

I like what we've done here. It's a simple self-contained constraint that should settle the use-cases people described in #43047.

use Symfony\Component\Validator\Exception\ConstraintDefinitionException;

/**
* Validates that a value is one of an enum.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
* Validates that a value is one of an enum.
* Validates that a backed enum can be hydrated from a value.

We should add a piece of documentation here, that directs people to Type and Choice constraints, if they want to validate actual enums instead the backing values. The whole discussion in #43047 shows that people mix this up regularly.

use Symfony\Component\Validator\Exception\UnexpectedTypeException;

/**
* BackedEnumValueValidator validates that the value is one of the expected values.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
* BackedEnumValueValidator validates that the value is one of the expected values.
* BackedEnumValueValidator validates that a backed enum case can be hydrated from a value.

@AurelienPillevesse
Copy link
Contributor Author

Thanks for your feedbacks during this PR @derrabus

$this->validator->validate(
'yes',
new BackedEnumValue(
type: MyStringEnum::class
Copy link
Member

Choose a reason for hiding this comment

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

This should not reference non-autoloadable enums defined in a different test file as this would break if the other test file has not been loaded

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed

@mdeboer
Copy link
Contributor

mdeboer commented Mar 15, 2024

This looks good, hopefully this gets through this time!

@AurelienPillevesse
Copy link
Contributor Author

If you want me to do any other change here, please tell me.

Otherwise, please review and approve this PR so maintainers can see it as ready. Thanks!

@mdeboer
Copy link
Contributor

mdeboer commented Mar 17, 2024

I looked at it and it looks good, just one minor thing about the location of the fixture enums in the tests.

Not going to reject it based on that, nor approve it. Don't know what is recommended right now.

Once that is cleared up I'll approve it if that helps 👍🏻 Great work!

return;
}

try {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
try {
if ($value instanceof \Stringable) {
$value = (string) $value;
}
if (!\is_string($value)) {
throw new UnexpectedValueException($value, 'string');
}
try {

Copy link
Contributor

Choose a reason for hiding this comment

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

backed enums can be ints, im not sure it's covered in tests

Copy link
Contributor Author

Choose a reason for hiding this comment

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

int enum case is covered in tests :

Copy link
Member

Choose a reason for hiding this comment

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

Yes, but @xabbuh's proposal wouldn't work for integers. I think that's what @ro0NL was getting at. 🙂

Copy link
Member

@derrabus derrabus Apr 25, 2024

Choose a reason for hiding this comment

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

If checking the type would free us from catching the TypeError, I'd say go for it. But given that we still need to catch it, I would not go with @xabbuh's proposal.

Copy link
Contributor

Choose a reason for hiding this comment

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

it depends if we want to expose other types than string|int as a user-error

unexpected types should throw UnexpectedValueException, following the component's design

Copy link
Member

Choose a reason for hiding this comment

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

So we catch the TypeError and upcast it?

Copy link
Contributor

@ro0NL ro0NL Apr 25, 2024

Choose a reason for hiding this comment

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

yes, we could differentiate between TypeError and ValueError, respectively translating to UnexpectedValueException and a user constraint error in symfony

but then we should use BackedEnum::from() AFAIK, because tryFrom seems really quirky: https://3v4l.org/54qFs

edit: oh tryFrom/from is prone to strict_types=1

*/
class BackedEnumValueTest extends TestCase
{
public function testAttributes()
Copy link
Member

Choose a reason for hiding this comment

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

we should also add tests covering the cases that throw exceptions

throw new UnexpectedTypeException($constraint, BackedEnumValue::class);
}

if (null === $value || '' === $value) {
Copy link
Contributor

Choose a reason for hiding this comment

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

i think the empty string case should pass through https://3v4l.org/lu4o4

Copy link
Member

Choose a reason for hiding this comment

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

Empty values are always valid and that's what this check does. This is fine.

Copy link
Contributor

Choose a reason for hiding this comment

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

i think generally so, it's an edge case

Copy link
Member

Choose a reason for hiding this comment

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

No, your "edge case" simply does not make a difference.

Copy link
Contributor

@ro0NL ro0NL Apr 25, 2024

Choose a reason for hiding this comment

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

allowing empty string, but only when it's a valid enum case, is not possible currently

Copy link
Contributor

Choose a reason for hiding this comment

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

see also Choice constraint

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Feature RFC RFC = Request For Comments (proposals about features that you want to be discussed) Status: Reviewed Validator
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

9 participants