Jeff Ochoa

Best Practices for Using PHP Enumerations

Best Practices for Using PHP Enumerations - JeffOchoa.me

Although PHP Enumerations are meant to provide an easy and elegant solution to work with a predefined set of values, these classes are often misused, resulting in a lack of consistency due to the introduction of all sorts of methods that increase their complexity. In this article, we are going to go through some practices (both good and bad) with the goal of clarifying the understanding of these types of objects to help you build a more resilient and consistent codebase.

PHP Enumerations were introduced in PHP 8.1; they provide a way to define a set of named, distinct values, making your code more readable and expressive when working with a limited, predefined set of options. These enumerations are often used for representing choices like days of the week, months, error states, or any other scenario where a fixed, well-defined list of values is applicable.

With PHP Enumerations, you can define a clear and concise set of options, which improves code clarity, eliminates "magic values," and helps prevent errors when working with such values. They also offer type safety, making sure that only valid enumeration values can be used in your code, which can help catch bugs at an early stage of development.

A Simple Example

Let's take the simplest example:

enum Status
{
    case ACTIVE;
    case INACTIVE;
}

$status = Status::ACTIVE;

The $status variable in this case is an instance of the Status class with a reserved property $name holding the constant case we just defined for each value:

Status {
    +name: "ACTIVE",
}

To get the constant as a string, you can access the name property on the status instance, for example:

Status::ACTIVE->name; // prints: 'ACTIVE'

Getting current case values

In many cases, we use Enumerations not only to hold certain values in memory during runtime but also to extract and store some information in our database. But what happens if the value we are trying to store in the database differs from the name of the case we are using in our codebase? For example, storing 0 when the status is INACTIVE and 1 when the status is ACTIVE.

❌ Adding a value() function

One way of achieving this is by introducing a new method in the Enum to cast the current status into a specific value, for example:

enum Status
{
    case ACTIVE;
    case INACTIVE;

    public function value()
    {
        return match($this) {
            self::ACTIVE => 1,
            self::INACTIVE => 0
        };
    }
}

Status::INACTIVE->value();

Although this approach will give us the result we are looking for, there is another mechanism available in the Enumeration classes that is already built-in to solve this issue.

✅ Switching to backed enums

enum Status: int
{
    case ACTIVE = 1;
    case INACTIVE = 0;
}

Status::INACTIVE;

Backed enumerations solve this for us, and as you can see, introducing a value() method only adds confusion and inconsistencies around the core functionalities.

A case that has a scalar equivalent is called a Backed Case, as it is "Backed" by a simpler value... A Backed Enum may be backed by types of int or string, and a given enumeration supports only a single type at a time (that is, no union of int|string). Learn more

Notice that in this case, our Backed Enum instance will contain a new value attribute that we can access to extract the required information:

Status {
    +name: "ACTIVE",
    +value: 1,
}

You can use this as follows:

Status::ACTIVE->value; // prints: 1

Getting a new instance from a Given Value

Let's go the other way around, what if we want to get a new instance from a given value, in this case, 0 or 1?

❌ Evaluate each case to return the right instance

enum Status
{
    case ACTIVE;
    case INACTIVE;

    public static function fromValue(bool $value): self
    {
        return $value ? Status::ACTIVE : Status::INACTIVE;
    }
}

Status::fromValue(1);

✅ Using reserved from() and tryFrom() Enum Methods:

In this case, we found the same situation as before, Backed enumerations already offer a solution for this case, and the following code will produce the same result as the one above:

enum Status: int
{
    case ACTIVE = 1;
    case INACTIVE = 0;
}

Status::from(1);

If the value being passed to the from() method is defined, then a new instance will be returned; otherwise, an exception will be thrown:

Status::from(5);

ERROR: 5 is not a valid backing value for enum Status

There is a second reserved method you can use to suppress the exception and get null instead:

Status::tryFrom(5); // NULL

from(int|string): self will take a scalar and return the corresponding Enum Case. If one is not found, it will throw a ValueError. tryFrom(int|string): ?self will take a scalar and return the corresponding Enum Case. If one is not found, it will return null.Learn more Running comparisons against Enums

To compare enums you could potentially use both the case name and case value to compare one instance agains the other, but even better you can compare an entire instance agains the other making the code more expressive and easier to understand.

❌ Comparing an Enum instance against scalar values

Try avoiding comparisons between Enum instances and it's scalar values, for example:

$value = 1;
$status = Status::INACTIVE;

if ($status->value === $value) {
    // do something
}

It seems a reasonable approach and some may be tempted to add a function to encapsulate such logic:

enum Status: int
{
    case ACTIVE = 1;
    case INACTIVE = 0;

    public function is($value): bool
    {
        return $this->value === $value;
    }
}

$value = 1;

if ($status->is($value)) {
    // do something
}

✅ Get an Enum instance from a given value and use to compare agains other Enum:

Instead trying to compare two different type of variables (boolean vs Enum), it would be better if you get an Enum instance from the comparison value and then do the comparison:

$value = 1;

$previous = Status::ACTIVE;
$current = Status::from($value);

if ($previous === $current) {
    // do something
}

If after all you still want to encapsulate the comparison logic within the Enum class, you can do the following:

enum Status: int
{
    case ACTIVE = 1;
    case INACTIVE = 0;

    public function is(Status $value): bool
    {
        return $this->value === $value;
    }
}

Getting Presentation Label for current Enum case

When building the user interface, you may want to present the values of these Enumerations in a slightly different manner.

❌ Formatting the current case name or value

It would be fine to assume that decisions around presentation are not responsibility of the Enum object itself and therefore it is fine if that sedition get's delegated to the presentation layer, for example, consider the following template:

<span class="status">{{ ucfirst(Status::ACTIVE->name)  }}</span>

This could also introduce a lot of duplication in your code or will require a new formatting layer between the Enum class and the presentation layer adding more complexity to the code.

✅ Using custom methods to format presentation labels

In such cases, it is perfectly fine to introduce a new method to define presentation labels for each value, for example:

enum Status: int
{
    case ACTIVE = 1;
    case INACTIVE = 0;

    public function label(): string
    {
        return match ($this) {
            self::ACTIVE => 'Active',
            self::INACTIVE => 'Inactive',
        };
    }
}

Status::ACTIVE->label(); // 'Active'

If you are using localization in your application to support multiple languages, you could even apply the logic to return the right translation. For example, if you are using Laravel, there is a method to retrieve translation strings that you can implement as follows:

enum Status: int
{
    case ACTIVE = 1;
    case INACTIVE = 0;

    public function label(): string
    {
        return match ($this) {
            self::ACTIVE => __('Active'),
            self::INACTIVE => __('Inactive'),
        };
    }
}

Status::ACTIVE->label(); // 'Active'

Case listing

Use the reserved cases() method to get all the registered options from your Enum class:

foreach(Status::cases() as $case) {
    echo $case->name . PHP_EOL;
}

// ACTIVE
// INACTIVE

You can also access any custom function when iterating over each case:

foreach(Status::cases() as $case) {
    echo $case->label() . PHP_EOL;
}

// Active
// Inactive

Some Rules of Thumb when Implementing PHP Enumerations

Conclusion

With PHP Enumerations, you can define a clear and concise set of options, which improves code clarity, eliminates "magic values," and helps prevent errors when working with such values. They also offer type safety, making sure that only valid enumeration values can be used in your code, which can help catch bugs at an early stage of development.

The examples used in this article and the rules of thumb provided here are, of course, a matter of personal preference. It is up to you whether to follow them or not. The final recommendation in this subject would be to follow the same standard across your application to keep your codebase clean and consistent.

Share This Article

Continue reading