If you enjoy reading my blog, you could consider supporting me on Patreon.

PHP Enums

An enumeration type, "enum" for short, is a data type to categorise named values. Enums can be used instead of hard coded strings to represent, for example, the status of a blog post in a structured and typed way.

PHP doesn't have a native enum type. It offers a very basic SPL implementation, but this really doesn't cut the chase.

There's a popular package written by Matthieu Napoli called myclabs/php-enum. It's a package I and many others have been using in countless projects. It's really awesome.

Today I want to explore some of the difficulties we encounter when solving problems like enums in userland. I'll talk about my personal take on enums, and we'll ponder on core support.

One last note: I will assume that you know what enums are, and that you know on how to use them in real life projects.

# Imagine if:

We could write something like this in PHP…

class Post
{
    public function setStatus(PostStatus $status): void
    {
        $this->status = $status;
    }
}

… and be sure that the value of Post::$status is always one of three strings: draft, published or archived.

Say we'd save this Post in a database, its status would automatically be represented as a string.

The myclabs/php-enum package allows us to write this:

class PostStatus extends Enum
{
    const DRAFT = 'draft';
    const PUBLISHED = 'published';
    const ARCHIVED = 'archived';
}

We could use the constant values directly like so:

class Post
{
    public function setStatus(string $status): void
    {
        $this->status = $status;
    }
}

// …

$post->setStatus(PostStatus::DRAFT);

But this prevents us to do proper type checking, as every string could be passed to Post::setStatus().

A better approach is to use a little magic introduced by the library:

class PostStatus extends Enum
{
    private const DRAFT = 'draft';
    private const PUBLISHED = 'published';
    private const ARCHIVED = 'archived';
}

$post->setStatus(PostStatus::DRAFT());

Using the magic method __callStatic() underneath, an object of the class PostStatus is constructed, with the 'draft' value in it.

Now we can type check for PostStatus and ensure the input is one of the three things defined by the "enum".

Here's the problem with the myclabs/php-enum package though: by relying on __callStatic(), we lose static analysis benefits like auto completion and refactoring:

As you can see in this case, your IDE is unaware of the PostsStatus::DRAFT() method.

Luckily, this problem is solvable with docblock type hints:

/**
 * @method static self DRAFT()
 * @method static self PUBLISHED()
 * @method static self ARCHIVED()
 */
class PostStatus extends Enum
{
    private const DRAFT = 'draft';
    private const PUBLISHED = 'published';
    private const ARCHIVED = 'archived';
}

$post->setStatus(PostStatus::DRAFT());

But now we're in trouble when refactoring an enum's value. Say we want to rename DRAFT to NEW:

Also we're maintaining duplicate code: there's the constant values, and the doc blocks.

At this point it's time to stop and think. In an ideal world, we'd have built-in enums in PHP:

enum PostStatus {
    DRAFT, PUBLISHED, ARCHIVED;
}

Since that's not the case right now, we're stuck with userland implementations.

Extending PHP's type system in userland most likely means two things: magic and reflection.

If we're already relying on these two elements, why not go full-out and make our lives as simple as possible?

Here's how I write enums today:

/**
 * @method static self DRAFT()
 * @method static self PUBLISHED()
 * @method static self ARCHIVED()
 */
class PostStatus extends Enum
{
}

Opinionated, right? It's less code to maintain though, with more benefits.

I know this is far from an ideal situation. It would be amazing to see built-in support for enums in PHP one day. But until then, this has to do.

If you want to, you can try out my implementation here.

So, what's your take on enums? Do you want them in core PHP? Let's talk about it on Twitter!