Back

PHP enums

The case of PHP ENUM's

Enums short for Enumerated datatypes is/are a pretty standard feature in many programming languages. But in PHP this was a recent introduction. It was introduced in PHP 8.1. A nice write-up about PHP enums is here. Before the built-in support came through, there were a lot of 3rd party packages that mimicked Enums like in other languages. a simple google for PHP enums will give you enough results.

In PHP Enums are still classes although a special kind of classes and Enum cases (that's the variants of enums) are objects of that class. Many languages have enums with different capabilities, for example, In Rust, the Enums are heavily leveraged, and a huge part of its type of safety techniques roam around Enums. In Rust, the type safety is ensured by enforcing checks on every variant of an Enum so that no unexpected value comes out in any case, this is ensured by its type checker. Let's not go super deep in Rust rather focus on PHP's enums.

In PHP the enums are implemented as backed enums meaning their cases can have scalar value. My idea is that scalar values are like int, float, char, etc... unlike an Object or an array where they hold multiple values, in PHP strings are also scalar even though they are character arrays internally.

By default, PHP enums are pure enums. But I rarely have a case to write a pure enum. let's look at their syntax a bit.

Pure Enums


enum Suit
{
    case Hearts;
    case Diamonds;
    case Clubs;
    case Spades;
}

Backed Enums


/** Represents the status of a book in a library */
enum BookStatus: string // as you can see the enum now has a type
{
    case BORROWED = 'borrowed'; // and their cases also have the same type
    case IN_HAND = 'in_hand'; 
}

I would like to write a little about how I/we at work use it and how it helps us to get things done in a better way. Let's also look into some syntax as well.

Example of Enum use cases

A big use case of an enum is to represent a definitive list of items. an example is boolean values, why? because there are only two booleans true and false. I usually take Enums when I want to represent a set of things, a list of states, steps in a process, etc... even though they sound a bit abstract, let's look into some samples.

In one of the projects I work on, we have a feature where a user could upload a CSV file and the processing is done in the background. the file goes through a set of stages and in the end, we notify the user with an email. But meanwhile, the user should be able to see the stage currently it is in once they visit the dashboard.

These are the stages.

  1. uploaded
  2. parse-initialized
  3. parse-started
  4. parse-succeeded
  5. parse-failed
  6. import-initialized
  7. import-started
  8. import-succeeded
  9. import-failed
  10. delete-requested
  11. delete-initialized
  12. delete-started
  13. delete-recalculate
  14. delete-succeeded
  15. delete-failed

Imagine comparing all these string values everywhere in the codebase, let's look if we can improve it. Out of these 15 let's take only a few for the sake of simplicity.

  1. uploaded
  2. parse-started
  3. parse-succeeded
  4. parse-failed

Once I see this kind of list then Enums comes into my mind automatically. In the database, we have to store these states and we can display them to the user. So we can have a status column in database, and for each of these values, we can have a mapping of an integer (meaning uploaded -> 1, parse-started -> 2 etc..) or the string itself. Mysql and Postgres databases have built-in enums support. I am sure others will have something similar.

Let's create a column to store these values and using string to store the enum value

CREATE TABLE upload
(
    id      INT AUTO_INCREMENT PRIMARY KEY,
    status  ENUM ('uploaded', 'parse-started', 'parse-succeeded', 'parse-failed') DEFAULT 'uploaded' NOT NULL  
);

Now let's create our string-backed ENUM to represent these in our codebase

enum UploadStatus: string
{
    case UPLOADED = 'uploaded';
    case PARSE_STARTED = 'parse_started'; 
    case PARSE_SUCCEEDED = 'parse_succeeded';
    case parse_failed = 'parse_failed';
}

I am using capital case for the names of ENUM cases. Just to remaind myself that I am dealing with something similar to a constant.

Now you might want to read a record from the database and check its status. Modern frameworks like Symfony/Doctrine have built-in support for enums read here to find out how to set up enum bindings for your entity in symfony. But let's say you have read the value from DB and want to check its status with the help of Enum.

// ....
// some logic to connect and read from the upload table and you have the status as in the following variable

$status = 'uploaded';

without an enum you will have to check it against a string every time like $status === 'uploaded' and tbh string comparisons are not my favorite.

have a look at how we can improve

$uploadStatus = UploadStatus::tryFrom($status);

echo $uploadStatus == UploadStatus::UPLOADED; // outputs true

if the $status was not found in the enum we will be getting a null as the result of tryFrom(). With this improvement we no longer need string comparisons everywhere in our codebase. We are leveraging the type of safety the enum provides us.

Accessing Enum case label and its backed value

This is a trivial case and we can access them with the following syntax


var_dump(UploadStatus::UPLOADED->name);
var_dump(UploadStatus::UPLOADED->value);

//outputs
string(8) "UPLOADED"
string(8) "uploaded"

Accessing all the cases

var_dump(UploadStatus::cases());

//output
array(4) {
  [0]=>
  enum(UploadStatus::UPLOADED)
  [1]=>
  enum(UploadStatus::PARSE_STARTED)
  [2]=>
  enum(UploadStatus::PARSE_SUCCEEDED)
  [3]=>
  enum(UploadStatus::parse_failed)
}

Looping through Enums

Sometimes we have to loop through enums. the cases() method comes again to our rescue

foreach (UploadStatus::cases() as $status) { 
    echo $status->name . " => " . $status->value . PHP_EOL;
}

//output
UPLOADED => uploaded
PARSE_STARTED => parse_started
PARSE_SUCCEEDED => parse_succeeded
PARSE_FAILED => parse_failed

Custom functions in an enum

In our case where we display the status of our upload, we can currently show the name or value but both look cryptic to the user's eye. see for yourself

Especially because of the underscore and their casing. Unfortunately, we can't have space in our enum value or in its names. One way to fix it is to write a custom function to return a user-readable label.

enum UploadStatus: string
{
    case UPLOADED = 'uploaded';
    case PARSE_STARTED = 'parse_started';
    case PARSE_SUCCEEDED = 'parse_succeeded';
    case PARSE_FAILED = 'parse_failed';

    public function label(): string
    {
        return match ($this) {
            self::UPLOADED  => 'Uploaded',
            self::PARSE_STARTED => 'Parse Started',
            self::PARSE_SUCCEEDED => 'Parse Succeeded',
            self::PARSE_FAILED => 'Parse Failed',
        };
    }
}

echo UploadStatus::PARSE_SUCCEEDED->label();

//output
Parse Succeeded

Enums and interfaces

Enums can implement interfaces. Say we have an interface Labelable which ensures a label() method in its implementors

interface Labelable
{
    public function label(): string
}

we can modify our enum as follows

// the syntax is slightly off as the `implements` keyword comes after the return type.
enum UploadStatus: string implements Labelable
{
    case UPLOADED = 'uploaded';
    case PARSE_STARTED = 'parse_started';
    case PARSE_SUCCEEDED = 'parse_succeeded';
    case PARSE_FAILED = 'parse_failed';

    public function label(): string
    {
        return match ($this) {
            self::UPLOADED  => 'Uploaded',
            self::PARSE_STARTED => 'Parse Started',
            self::PARSE_SUCCEEDED => 'Parse Succeeded',
            self::PARSE_FAILED => 'Parse Failed',
        };
    }
}

Native enums are a wonderful addition to PHP. I use them quite often and was of great help. you can read more about enums here too.