Using Native PHP Enums

It is often common practice to use 1 or 0 values for columns in database tables. As a developer, I equate these to true and false and modern frameworks such as Laravel allow you to easily cast attributes to booleans.

However, sometimes two options are not enough. On a recent project, I noticed a value of 2 in the status column of a database table. What does it mean?! Luckily the original developer was on hand to answer; it meant "pending". If they weren't around, I would have to dig through code to try and understand what this value meant.

To solve this we can define enumerations. However, native support for enums was only recently added in PHP 8.1. Before this, developers solved the lack of support by creating classes and using constants. You could also add support for validation and helpers using reflection.

For Laravel projects, previous to PHP 8.1 use, I used the Laravel Enum package which;

allows you access helper functions such as listing keys and values, attaching descriptions to values, and validating requests which are expecting enum values.

This allowed me to make use of the benefits of enums on any Laravel project I was working on.

Upgrading to PHP 8.1

For this website, I upgraded to PHP 8.1. This meant I could start using native enums. However, I needed to add some helper methods to make them as useful as the non-native solution. For example, the following enum would make sense for the "pending" problem described above and allows to easily add more self-describing values:

<?php

declare(strict_types=1);

namespace App\Enums;

enum Status: string
{
    case INACTIVE = 0;
    case ACTIVE = 1;
    case PENDING = 2;
    case REMOVED = 3; // a new value.
}

To add useful functionality I created a trait which can be added to my native enums. I wanted to easily be able to get an array of key/value pairs as well as just the values and names. I use Laravel's Collections to build up the data I wanted using the native cases() method.

<?php

declare(strict_types=1);

namespace App\Enums;

use App\Support\Str;
use Illuminate\Support\Collection;

trait Enum
{
	abstract public function cases();
	
    public static function toCollection(): Collection
    {
        $cases = static::cases();

        return Collection::make($cases)
            ->mapWithKeys(static fn (self $enum): array => [
                strval($enum->value) => $enum->name,
            ]);
    }

    public static function toArray(): array
    {
        return static::toCollection()->toArray();
    }

    public static function values(): array
    {
        return static::toCollection()->keys()->map('strval')->toArray();
    }

    public static function names(): array
    {
        return static::toCollection()->values()->toArray();
    }
}

This allows me to easily get all the names or values as an array. The toArray() method allows for key/value pairs of the information. And the toCollection() keeps the data in useful Laravel Collections, so they can be manipulated if necessary.

Below is an example of my Movie\WatchedType enum I use. The lowercase values are what is stored in the database and the cases are what I use in the application code so there is no hardcoded text.

<?php

namespace App\Enums\Movie;

use App\Enums\Enum;

enum WatchedType: string
{
	use Enum;

	case DVD = 'dvd';
	case CINEMA = 'cinema';
	case NETFLIX = 'netflix';
	case AMAZON_PRIME = 'prime';
	case DISNEY = 'disney';
	case VOD = 'vod';
}

Using the Extended Enums

For my Eloquent models and tests, I use faker to get randomised but correct values. Using the enum trait gives the flexibility to easily generate those values. For example, you can use the following to get a random database-friendly value for your factories.

$this->faker->randomElement(Movie\WatchedType::values());

I use Laravel Nova to manage the content of my website. I can easily use the enum-defined options for Nova select fields, using the toArray() method added by the trait.

Select::make('Type')->options(Movie\WatchedType::toArray());

For Nova filters, it expects the options array to be in the opposite format, with the friendly value as the array key and the database value as the array value. I can easily provide this data by using the toCollection() method and flipping the data.

public function options(Request $request): array
{
    return Movie\WatchedType::toCollection()->flip()->toArray();
}

Adding a Description

These additions to native enums are helpful, but there is one small problem. The "name" of them in the select boxes is UPPERCASE which isn't what I wanted. To solve this I added a description() method, which converts the enum name value to something more readable.

public function description(): string
{
    return Str::of($this->name)->replace('_', ' ')->title()->value();
}

Now, when you have a backed enum, you can easily get a nice description.

Movie\WatchedType::CINEMA->description(); // Cinema
Movie\WatchedType::AMAZON_PRIME->description(); // Amazon Prime

We also need to alter the toCollection() method to use this description instead of the default name. This then propagates to the toArray() method as well as the names() array.

public static function toCollection(): Collection
{
    $cases = static::cases();

    return Collection::make($cases)
        ->mapWithKeys(static fn (self $enum): array => [
            strval($enum->value) => $enum->description(),
        ]);
}

Flexible Description

For my use case, there was a small issue with the description() method. The conversions for a few of the names in this movie type enum weren't quite right:

App\Enums\Movie\WatchedType::DVD->description(); // Dvd, should be DVD
App\Enums\Movie\WatchedType::VOD->description(); // Vod, should be VOD
App\Enums\Movie\WatchedType::DISNEY->description(); // Disney, should be Disney+

I can extend the description() method within my specific enum, calling the trait method by default, as explained in my using traits post. Here I can use the PHP 8 match syntax, which is rather succinct.

use Enum {
    description as parentDescription;
}

public function description(): string
{
    return match ($this->value) {
        'disney' => Str::of($this->name)->title()->append('+')->value(),
        'dvd', 'vod' => Str::of($this->name)->upper()->value(),
        default => $this->parentDescription(),
    };
}

Now I get the correct and nicely formatted description for DVD, VOD and Disney+.


While researching other solutions for improving enums, I came across a util package that adds useful functionality via a few different traits. The package includes a toArray() method and methods similar to what I have written, to return the names and values. The Assertable and Comparable traits provided by the package are also very useful and add nice boolean returns and comparisons that fit nicely with the Laravel ethos.