Good, bad and ugly of enums in PHP

PHP 8.1 introduced long awaited enumerations (enum) that allow to store list of discrete values (e.g. statuses or types).

Before enum type the common practice was listing all possible values as class constants:

class Order
{
    public const STATUS_PLACED = 'new;
    public const STATUS_CONFIRMED = 'confirmed';
    public const STATUS_CANCELLED = 'cancelled';
    public const STATUS_CLOSED = 'closed';

    // array of all values that can be used for validation or displaying dropdown list of possible statuses
    public const STATUSES = [
        self::STATUS_PLACED,
        self::STATUS_CONFIRMED,
        self::STATUS_CANCELLED,
        self::STATUS_CLOSED,
    ];
}

Alternatively developers could use existing open source libraries or custom classes providing extended enum alike functionality:

class OrderStatus extends Enum {
    ...
}

Good #

Extendability #

Enums can implement Interfaces, use traits and have methods, that allow extending functionality and customising.

enum OrderStatus implements MyStatus {
    use MyTrait;

    case Placed = 'new;
    case Confirmed = 'confirmed';
    case Cancelled = 'cancelled';
    case Closed = 'closed';

    public static function isConfirmed(string $status): bool
    {
        return self::Confirmed->value === $status;
    }
}

Validation #

One huge benefit of using enums is validation as we can declare enum name as a type of function arguments:

class Order
{
    protected OrderStatus $status;

    public function setOrderStatus(OrderStatus $status): void
    {
        $this->status = $status;
    }
}

$order = new Order();
$order->setOrderStatus(OrderStatus::Confirmed);

Compare with the code that is using constants instead:

class Order
{
    public const STATUS_PLACED = 'new;
    public const STATUS_CONFIRMED = 'confirmed';

    public const STATUSES = [
        self::STATUS_PLACED,
        self::STATUS_CONFIRMED,
    ];

    protected string $status;

    public function setOrderStatus(string $status): void
    {
        if (!in_array($status, self::STATUSES)) {
            throw new \Exception("Invalid order status.");
        }

        $this->status = $status;
    }
}

$order = new Order();
$order->setOrderStatus(Order::STATUS_CONFIRMED);

Bad #

Implementation missing some obvious methods like listing all values of a backed enum, or getting all names.

Thanks mentioned above extendability, it's currently possible by adding a static method in the enum:

enum OrderStatus {
    ...

    public static function values(): array
    {
        return array_column(self::cases(), 'value');
    }
}

...or including this method as part of a trait.

Ugly #

Internal type of enum is similar to object, so even for backed enums familiar and more obvious constants syntax won't work:

echo OrderStatus::Confirmed; // Fatal error: Object of class OrdeStatus could not be converted to string
OrderStatus::Confirmed == 'confirmed' // false

An enum object cannot be casted to a string!

__toString() like pretty much all other "magic" methods is dissallowed for enums explicitly by the RFC.

Proper syntax of getting value of enum is different from constants and feels a bit excessive:

echo OrderStatus::Confirmed->value; // prints "new"
echo OrderStatus::Confirmed->name; // prints "Confirmed"

It means it won't be possible to just swap old custom enum classes with new built in enums in the code, and the need of refactoring seems to be a common frustration, although there are totally valid points about difference in type and avoiding confusion.

One solution to avoid confusion with constants is using a different naming convention, e.g.

I hope there'll be some kind of shorted syntax available in next versions of PHP for getting a value of a baked enum, e.g. OrderStatus.Cancelled.

Links: