Enums. With PHP < 8.1

Recently I had to build something where an Enum would have been perfect.

But…

The Challenge

It needed to run on PHP 8.0. Of course.

So what to do? I decided to build an Enum like thingy that I can easily upgrade into a real Enum once we are on PHP8.1 with that project.

Why not use a library for that? There are plenty of libraries on packagist that already provide you with the basics!

For one thing: I only needed one Enum. Not a multitude. And Adding a further dependency to make creating one enum easier that will (hopefully) converted into a “eral” enum in about half a year? That sounds bit like taking the sledgehammer to crack a nut.

And on the other hand it turned out that creating an enum from scratch isn’t rocket science.

The Beginning

So let’s start.

First of all an Enum is an instance of an entity that resembles one certain value. Each instance of an entity resembles a different value. instances of different entities have nothing to do with one another.

In the example I’ll take an entity that resembles a state of something. It can be “To do”, “in progress” and “done”. So the entity is a state and the three different instances resemble “To do”, “in Progress” and “Done”.

So I started out with a class that can only have three instances that resemble the different states.

<?php
final class State
{
    private const TO_DO = 'to do';
    private const IN_PROGRESS = 'in progress';
    private const DONE = 'done';

    private const STATES = [
        self::TO_DO,
        self::IN_PROGRESS,
        self::DONE,
    ];

    private static array $instances = [
        self::TO_DO => null,
        self::IN_PROGRESS => null,
        self::DONE => null,
    ];

    private function __construct(private string $state)
    {
        if (! in_array($state, self::STATES)) {
            throw new \RuntimeException(sprintf(
                'The provided state "%1$s" is not valid',
                $state
        ));
    }

    public function getName(): string
    {
        return match($this->state) {
            self::TO_DO => 'TO_DO';
            self::IN_PROGRESS => 'IN_PROGRESS';
            self::DONE => 'DONE';
            default => throw new RuntimeException('this should never happen');
        };
    }

    public static function TO_DO(): self
    {
        if (null === self::$instances[self::TO_DO]) {
            self::$instances[self::TO_DO] = new self(self::TO_DO);
        }
        return self::$instances[self::TO_DO];
    }

    public static function IN_PROGRESS(): self
    {
        if (null === self::$instances[self::IN_PROGRESS]) {
            self::$instances[self::IN_PROGRESS] = new self(self::IN_PROGRESS);
        }
        return self::$instances[self::IN_PROGRESS];
    }

    public static function DONE(): self
    {
        if (null === self::$instances[self::DONE]) {
            self::$instances[self::DONE] = new self(self::DONE);
        }
        return self::$instances[self::DONE];
    }
}

There! That’s that!

The Result

A class that can be used almost like an Enum. We can only have three different instances around (thanks to named constructors). We can compare them using instanceof or ===. And we can reference them almost the same as enums.

Yes, the code is a bit more verbose than a real enum which would look like this:

enum State {
    case TO_DO;
    case IN_PROGRESS;
    case DONE;
}

And let’s see how the Enum is called differently from our class:

// With PHP8.1 and a  real enum:
$value = State::TO_DO;

// With PHP8.0 and our "fake" enum
$value = State::TO_DO();

Yes. A real enum is referenced like a class-constant, the fake is referenced like a method.

But the nice thing is: That is about all that is different! And as that can be replaced rather elegantly using search-and-replace of a text editor (Search for “State::TO_DO()” and replace it with “State::TO_DO”) that is – at least in my case – rather forward-compatible. Yes! I will have to make code changes to be ready for Enums (not to be ready for PHP8.1 as the workaround works there as well). But I will need to create the enum in the first place anyhow so there will definitely need to be code-changes.

In essence: That’s it.

We can compare them like this:

$state = State::TO_DO();
if ($state === State::TO_DO()){
    //do something
}

We can check whether something is a state:

$state = State::TO_DO();
if ($state instanceof State) {
    // do something
}

The Topping

What is missing now is converting this to a “backed enum”. Well. Actually we are already half way there. Our constants are already referencing a string (or an int if you want to).

What is missing is a method to actually retrieve the value an done to create an instance from a value.

So let’s extend the class like this:

final class State
{
    ...
 
    public function getValue(): string
    {
        return $this->value;
    }

    public static function fromValue(string $value): self
    {
        $enum = new self($value);
        if (null === self::$instances[$enum->getValue()]) {
            self::$instances[$enum->getValue()] = $enum;
        }

        return self::$instances[$enum->getValue()];
    }
} 

Now we can also retrieve the value of the enum and restore it from a given value.

Finally

So all in all not really rocket-science. I found that a rather interesting approach to handle my special use-case.

When you need more such enums, then refactoring and extracting parts into a parent-class or a trait start to make sense and from that point to using an existing library to handle the boilerplate is then not much anymore.

But for a one-shot that seemed like a great way to go for me.

Update 1:

Alexander Turek rightfully pointed out on twitter, that the three main functions can be simplified even more when are using PHPs null coalescing operators (PHP7.0) or even asignements (PHP7.4).

Let’s check that:

final class State
{
    ...
    public static function TO_DO(): self
    {
        return self::$instances[self::TO_DO] ??= new self(self::TO_DO);
    }

    public static function IN_PROGRESS(): self
    {
        return self::$instances[self::IN_PROGRESS] ??= new self(self::IN_PROGRESS);
    }

    public static function DONE(): self
    {
        return self::$instances[self::DONE] ??= new self(self::DONE);
    }
}

As I used the PHP8.0 parameter constructor property promotion I decided to go for the PHP7.4 null Coalescing Assignment Operator.

Thanks Alexander for the hint!