Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

oikodomo's avatar

Enum Validation Rules only works with the BackedEnum Interface

Description

Normal Enums don't work with the new validation rule 'Illuminate\Validation\Rules\Enum'.

Example

As an example you have enum like the following definition:

enum Status
{
    case OK;
    case ERROR;
}

you also have a select HTML field like the following in your form:

<select name='status' >
    <option value='OK'>Ok</option>
    <option value='ERROR'>Error</option>
</select>

In your controller the request gets validated like that

use Illuminate\Validation\Rules\Enum;

$request->validate([
    ...
    'status' => new Enum(Status::class)
]);

This won't work. Sadly, the code in Illuminate\Validation\Rules\Enum validate the value with tryFrom and since its a method only available when using BackedEnum's, it fails and returns false.

A Solution

You could add the following, so you have a BackedEnum, and everything works fine:

enum Status: string
{
    case OK = "OK";
    case ERROR = "ERROR";
}

Opinion

What I don't like about that approach, is the duplication of the value. There are definitly usecases to go with BackedEnum's, but if I have a list of a couple of enum cases and every one of them has the same value as the name of the case and I misspell one value wrong (e.g. miss a 'R' in 'EROR') the code won't work (but a case like ERROR would still exist). The added string information also doesn't add any value to the code, if it's allways the same name. The Laravel Doc should be mentioning that the rule only works for BackedEnum's. The rule could also be renamed to BackedEnum, like the interface (php.net/manual/de/class.backedenum.php) that is needed for it to work.

My current solution

For the above problem I did go back to the Rule Validation:

$request->validate([
    ...
    'status' => Rule::in([Status::OK->name, Status::ERROR->name])
]);

But this is a pain in the a**, if you add new cases or rename them. So what i did, since you can't extend an enum, was adding a trait, which adds methods that are equivalent to the methods that backed enums have. Like from and tryFrom and also an equivalent cases method, that is called all, that returns all case names as an array, instead of all cases as enum objects. My trait looks like the following:

namespace App\Classes;

trait FooBar
{
    public static function all()
    {
        return array_map(function($c)
        {
            return $c->name;
        },self::cases());
    }

    public static function get(string $name)
    {
        if(defined("self::$name"))
            return constant("self::$name");

        throw new ValueError("{$name} is not a valid case in enum '".get_called_class()."'");
    }

    public static function tryGet(string $name)
    {
        return defined("self::$name") ? constant("self::$name") : null;
    }
}

In all my enums I just use the trait:

use App\Classes\FooBar;

enum Status
{
    use FooBar;

    case OK;
    case ERROR;
}

Now in the validation i go back and change the list of all cases to the following:

$request->validate([
    ...
    'status' => Rule::in(Status::all())
]);

Do you have a different approach? A better approach?

0 likes
6 replies
oikodomo's avatar

Different use cases for the trait

The trait for the enums also helped me on other occasions. For example in migrations. With the example above, you could use the enum with trait like the following:

$table->enum('status',Status::all())->default(Status::OK->name);

The get and tryGet functions are handy for conditions. I could imagine a use case where you wanna check the role of an user:

if(UserRole::get(auth()->user()->role) !== UserRole::ADMIN)
    return false;
1 like
oikodomo's avatar

@SomeOne01 sounds like a great package. I will definitely check it out, if I finally find time for it. From the first look it seems very powerful. Thank you, for your hard work!

Rikaelus's avatar

What I don't like about that approach, is the duplication of the value.

I'd push back on that argument a bit. You're not actually duplicating the value; you're mapping an ENUM value to a string value and I'd say your situation is a prime example of when and why you'd want to use a string-backed ENUM. I use string-backed ENUMs all the time for the exact kind of situation you're in where I want to control possible values going through code but need a more portable way of representing that value outside of my PHP code (in form values, storing in the database, etc.)

The flexibility you have in making the string representation different than the ENUM value is a perk on top of that. I actually don't think I have any instances where the string value and ENUM value are exact; at a minimum my ENUM values are semantic UpperCamel where the string values are usually all lower or underscored (usually for database storage)

oikodomo's avatar

@Rikaelus Actually it is a duplication of value, when both, the case name and the case value, mean the same thing and have no differences. If you want a string representation of an enum case, you can do that by simply calling the name property of that case ( Status::ERROR->name ) without the need of a BackedEnum definition. Its the same value but with a string representation. The duplication of value is very clear when you have a BackedEnum in place and compare both, the enum name with the enum value (Status::ERROR->name == Status::ERROR->value).

Both, value & name, can also be different. You are talking about a situation, where you want to have a different purpose for the value as for the name of an enum case (db column definition). And thats fine. I'm not argueing about that. But if you find yourself in a situation where you write exactly the same, for the enum name and for the enum value, and the only reason is mapping, I think you are on a wrong path with error's ahead.

When you have the enum laravel rule in your laravel request validation, and one misspelled value to an enum case you get a error that this case is not support but in reality you have the correct one in place just with a misspelled value. I find that a really stupid error, that can be solved with the solution I gave in my post.

Please or to participate in this conversation.