PHP 8.1: Enums

They are finally coming — built-in support for enums will be added in PHP 8.1! Some might consider them long overdue, but you don't hear me complain; I'm glad they made it! This post is dedicated to looking at the newly added feature in-depth. If you want to stay up to date about these kinds of changes and new features in PHP, make sure to subscribe to my newsletter.

As usual with my PHP feature posts, we start with a high level overview of what enums look like:

enum Status
{
    case DRAFT;
    case PUBLISHED;
    case ARCHIVED;
}

The benefit of enums is that they represent a collection of constant values, but most importantly those values can be typed, like so:

class BlogPost
{
    public function __construct(
        public Status $status, 
    ) {}
}

In this example, creating an enum and passing it to a BlogPost looks like this:

$post = new BlogPost(Status::DRAFT);

That's the basics out of the way, as you can see there's nothing complex at all about them. There are lots of side notes to be made though, let's look at enums in depth!

# Enum methods

Enums can define methods, just like classes. This is a very powerful feature, especially in combination with the match operator:

enum Status
{
    case DRAFT;
    case PUBLISHED;
    case ARCHIVED;
    
    public function color(): string
    {
        return match($this) 
        {
            Status::DRAFT => 'grey',   
            Status::PUBLISHED => 'green',   
            Status::ARCHIVED => 'red',   
        };
    }
}

Methods can be used like so:

$status = Status::ARCHIVED;

$status->color(); // 'red'

Static methods are allowed as well:

enum Status
{
    // …
    
    public static function make(): Status
    {
        // …
    }
}

And you can also use self within an enum:

enum Status
{
    // …
    
    public function color(): string
    {
        return match($this) 
        {
            self::DRAFT => 'grey',   
            self::PUBLISHED => 'green',   
            self::ARCHIVED => 'red',   
        };
    }
}

# Enum interfaces

Enums can implement interfaces, just like normal classes:

interface HasColor
{
    public function color(): string;
}
enum Status implements HasColor
{
    case DRAFT;
    case PUBLISHED;
    case ARCHIVED;
    
    public function color(): string { /* … */ }
}

# Enum values — aka "Backed enums"

Enum values are represented by objects internally, but you can assign a value to them if you want to; this is useful for eg. serializing them into a database.

enum Status: string
{
    case DRAFT = 'draft';
    case PUBLISHED = 'published';
    case ARCHIVED = 'archived';
}

Note the type declaration in the enum definition. It indicates that all enum values are of a given type. You could also make it an int. Take note that only int and string are allowed as enum values.

enum Status: int
{
    case DRAFT = 1;
    case PUBLISHED = 2;
    case ARCHIVED = 3;
}

The technical term for typed enums is called "backed enums" since they are "backed" by a simpler value. If you decide to assign enum values, all cases should have a value. You cannot mix and match them. Enums that aren't "backed" are called "pure enums".

# Backed enums with interfaces

If you're combining backed enums and interface, the enum type must come directly after the enum name, before the implements keyword.

enum Status: string implements HasColor
{
    case DRAFT = 'draft';
    case PUBLISHED = 'published';
    case ARCHIVED = 'archived';
    
    // …
}

# Serializing backed enums

If you're assigning values to enum cases, you probably want a way to serialize and deserialize them. Serializing them means you need a way to access the enum's value. That's done with a readonly public property:

$value = Status::PUBLISHED->value; // 2

Restoring an enum from a value can be done by using Enum::from:

$status = Status::from(2); // Status::PUBLISHED

There's also a tryFrom that returns null if an unknown value is passed. If you'd use from there would be an exception.

$status = Status::from('unknown'); // ValueError
$status = Status::tryFrom('unknown'); // null

Note that you can also use the built-in serialize and unserialize functions on enums. Furthermore, you can use json_encode in combination with backed enums, its result will be the enum value. This behaviour can be overridden by implementing JsonSerializable.

# Listing enum values

You can use the static Enum::cases() method to get a list of all available cases within an enum:

Status::cases();

/* [
    Status::DRAFT, 
    Status::PUBLISHED, 
    Status::ARCHIVED
] */

Note that this array contains the actual enum objects:

array_map(
    fn(Status $status) => $status->color(), 
    Status::cases()
);

# Enums are objects

I already mentioned that enums values are represented as objects, in fact those are singleton objects. That means that you can do comparisons with them like so:

$statusA = Status::PENDING;
$statusB = Status::PENDING;
$statusC = Status::ARCHIVED;

$statusA === $statusB; // true
$statusA === $statusC; // false
$statusC instanceof Status; // true

# Enums as array keys

Because enums values are actually objects, it's currently not possible to use them as array keys. The following will result in an error:

$list = [
    Status::DRAFT => 'draft',
    // …
];

There is an RFC to change this behaviour, but it hasn't been voted yet.

This means you can only use enums as keys in SplObjectStorage and WeakMaps.

# Traits

Enums can use traits just like classes, but with some more restrictions. You're not allowed to override built-in enum methods, and they can't contain class properties — those are prohibited on enums.

# Reflection and attributes

As expected, there are a few reflection classes added for dealing with enums: ReflectionEnum, ReflectionEnumUnitCase and ReflectionEnumBackedCase. There's also a new enum_exists function which does what its name suggests.

Just like normal classes and properties, enums and their cases can be annotated using attributes. Note that TARGET_CLASS filter will also include enums.

One last thing: enums also have a read only property $enum->name, which the RFC mentions is an implementation detail and should probably only be used for debugging purposes. It's still worth mentioning though.

Noticed a tpyo? You can submit a PR to fix it. If you want to stay up to date about what's happening on this blog, you can subscribe to my mailing list: send an email to brendt@stitcher.io, and I'll add you to the list.

That's about all there is to say about enums, I'm very much looking forward to using them as soon as PHP 8.1 arrives, and also to sunset my own userland implementation.