14

In PHP 8.1, BackedEnum offer a from and tryFrom method to get an enum from a value. How can the same be achieved by non backed enums?

Example BackedEnum:

enum MainType: string
{
    case Full = 'a';
    case Major = 'b';
    case Minor = 'c';
}

var_dump(MainType::tryFrom('a')); // MainType::Full
var_dump(MainType::tryFrom('d')); // null

However this doesn't exist for regular enums.

How would I retrieve a "normal" Enum by name, like:

enum MainType
{
    case Full;
    case Major;
    case Minor;
}

$name = (MainType::Full)->name
var_dump(name); // (string) Full

One option I've found is to simply add a tryFromName function, accepting a string and looping over all the cases like this:

enum MainType
{
    case Full;
    case Major;
    case Minor;

    public static function tryFromName(string $name): ?static
    {
        foreach (static::cases() as $case) {
            if ($case->name === $name) {
                return $case;
            }
        }

        return null;
    }
}

$name = (MainType::Full)->name
var_dump(name); // (string) Full
var_dump(MainType::tryFromName($name)); // MainType::Full

This works, however it seams counter intuitive to enable a foreach loop going over all possibilities just to create an enum.

Therefore the question is, what is the right way to get an Enum in PHP from the name.

5
  • 4
    Are you looking for var_dump(constant("MainType::$name")); which will show enum(MainType::Full) if $name = "Full". Commented Feb 8, 2022 at 15:40
  • 1
    I've just tried it and while it works, it requires the full name of the class, like "App\\Config\\MainType", which is fine, however when $name is set to something not existing an error is thrown instead of the null I want. I could wrap the whole call in a try/catch block but is that any better than simply looping over a hand full of values? Commented Feb 8, 2022 at 16:18
  • 1
    Yea the error is by design. Starting to sound like a x/y problem. If you're not sure if the enum exist, why use enum's in the first place? Maybe you could give a real-world example why you need something like this? Commented Feb 8, 2022 at 16:21
  • Sure @0stone0 in my case I'm using the EnumType of Symfony Form and in one case the selected value can be set by a optional get parameter. In my case I simply cast the parameter to string, have a default value of 'none' and if it's not set (or invalid) I just want a null Commented Feb 8, 2022 at 16:52
  • 2
    Worth mentioning that it is not a good practise to leak app-related names (i.e. case names) outside (e.g. into HTML) and use it as a value. It's what the Enum value is designed for. Commented Jun 23, 2022 at 6:34

4 Answers 4

12

You can use Reflection:

trait Enum {

    public static function tryFromName(string $name): ?static
    {
        $reflection = new ReflectionEnum(static::class);

        if (!$reflection->hasCase($name)) {
            return null;
        }

        /** @var static */
        return $reflection->getCase($name)->getValue();
    }

}

enum Foo {
    use Enum;

    case ONE;
    case TWO;
}

var_dump( Foo::tryFromName('TWO')   ); // enum(Foo::TWO)
var_dump( Foo::tryFromName('THREE') ); // null

Works also for Backed Enums.

Sign up to request clarification or add additional context in comments.

Comments

8

I love the use of ReflectionEnum in @hejdav's answer and it works PHP8.1+, and this would be my preference.

Unfortunately that time it failed with Phpstan tests saying Method EnumClass::tryFromName() should return EnumClass|null but returns UnitEnum|null., so I resorted to using the following:

trait EnumFromName
{
    /**
     * To mirror backed enums tryFrom - returns null on failed match.
     */
    public static function tryFromName(string $name): ?static
    {
        return array_column(static::cases(), null, 'name')[$name] ?? null;
    }

    /**
     * To mirror backed enums from - throws ValueError on failed match.
     */
    public static function fromName(string $name): static
    {
        return self::tryFromName($name)
            ?? throw new ValueError(sprintf(
                   '%s is not a valid case for enum %s',
                   $name, 
                   static::class
               ));

    }
}

Comments

2

I can't comment on Duncanmoo's response since I have no reputation yet.

But in PHP 8.4, you can simplify it to

public static function tryFromName(string $name): ?static
{
  return array_find(self::cases(), fn($case) => $case->name === $name);
}

1 Comment

This does not provide an answer to the question. Once you have sufficient reputation you will be able to comment on any post; instead, provide answers that don't require clarification from the asker. - From Review
0

One more PHP 8.1 solution like @Duncanmoo answer, but with speed optimization using static varibale to store indexed cases list

trait EnumFromName {

    public static function tryFromName(string $name): ?static 
    {
        /** @var ?array<non-empty-string, static> */
        static $cache;

        $cache ??= array_column(static::cases(), null, 'name');

        return $cache[$name] ?? null;
    }

    public static function fromName(string $name): static 
    {
        /** @var static */
        return static::tryFromName($name);
    }
}

With optimization: Execution time: 0.401827s

Without optimization: Execution time: 1.832551s

Difference will grow with large number of cases.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.