Enums

An enumeration is a type with a fixed set of possible values: order statuses, user roles, card suits, sizes. Before PHP 8.1, developers used constants or strings, and nothing stopped a typo like "shiped". Enums make the valid values part of the type system, and they can carry values and methods too.

Pure enums

PHP
<?php

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

function colour(Suit $suit): string
{
    return match ($suit) {
        Suit::Hearts, Suit::Diamonds => "red",
        Suit::Clubs, Suit::Spades => "black",
    };
}

$s = Suit::Hearts;
echo $s->name, " is ", colour($s), "\n";
var_dump($s === Suit::Hearts);
var_dump($s instanceof Suit);
echo count(Suit::cases()), " suits\n";
Output
Hearts is red
bool(true)
bool(true)
4 suits

Each case is a single, unique object, so === compares them. A function that takes a Suit can only ever receive one of the four cases — passing a string is a TypeError. cases() lists them all.

match and exhaustiveness

match and enums work beautifully together. If a case isn't handled, PHP throws an UnhandledMatchError rather than silently doing nothing — and static analysers can warn you before that ever happens.

Backed enums

To store an enum in a database, a session or JSON, give each case a scalar value. A backed enum declares string or int values:

PHP
<?php

enum Status: string
{
    case Pending = 'pending';
    case Paid = 'paid';
    case Shipped = 'shipped';
    case Cancelled = 'cancelled';
}

$status = Status::Paid;
echo $status->name, " / ", $status->value, "\n";

$fromDb = Status::from('shipped');
echo $fromDb->name, "\n";

var_dump(Status::tryFrom('lost'));
echo json_encode(['status' => Status::Pending]), "\n";

Status::from('lost');
Output
Paid / paid
Shipped
NULL
{"status":"pending"}

Fatal error: Uncaught ValueError: "lost" is not a valid backing value for enum Status in main.php:20
Stack trace:
#0 main.php(20): Status::from('lost')
#1 {main}
  thrown in main.php on line 20

from() throws for unknown values; tryFrom() returns null, which suits input that might be invalid. json_encode uses the backing value.

Methods and constants

Enums can have methods (including static ones), implement interfaces and define constants — so the logic about a value lives with the value:

PHP
<?php

enum Status: string
{
    case Pending = 'pending';
    case Paid = 'paid';
    case Shipped = 'shipped';
    case Cancelled = 'cancelled';

    const Status DEFAULT = self::Pending;

    public function label(): string
    {
        return ucfirst($this->value);
    }

    public function canTransitionTo(Status $next): bool
    {
        return match ($this) {
            self::Pending => in_array($next, [self::Paid, self::Cancelled], true),
            self::Paid => in_array($next, [self::Shipped, self::Cancelled], true),
            self::Shipped, self::Cancelled => false,
        };
    }

    public static function open(): array
    {
        return array_filter(self::cases(), fn($s) => $s !== self::Shipped && $s !== self::Cancelled);
    }
}

echo Status::DEFAULT->label(), "\n";
var_dump(Status::Pending->canTransitionTo(Status::Paid));
var_dump(Status::Shipped->canTransitionTo(Status::Pending));
echo implode(", ", array_map(fn($s) => $s->label(), Status::open())), "\n";
Output
Pending
bool(true)
bool(false)
Pending, Paid

Enums in classes

Use enums as property and parameter types. Combined with a readonly property, an order's status can only ever be a valid one:

PHP
<?php

enum Size: string
{
    case Small = 'S';
    case Medium = 'M';
    case Large = 'L';

    public function price(): float
    {
        return match ($this) {
            self::Small => 2.40,
            self::Medium => 2.90,
            self::Large => 3.40,
        };
    }
}

final class Coffee
{
    public function __construct(public readonly string $name, public readonly Size $size) {}
}

$orders = [new Coffee("Latte", Size::Large), new Coffee("Espresso", Size::from('S'))];
foreach ($orders as $o) {
    printf("%-9s %s %.2f\n", $o->name, $o->size->value, $o->size->price());
}
Output
Latte     L 3.40
Espresso  S 2.40

Turning input into enums

Form fields and URL parameters arrive as strings. Convert them at the edge of your application with tryFrom, and the rest of the code deals only with valid enum cases:

PHP
<?php

enum Role: string
{
    case Admin = 'admin';
    case Editor = 'editor';
    case Viewer = 'viewer';
}

foreach (['editor', 'ADMIN', 'hacker', ''] as $input) {
    $role = Role::tryFrom(strtolower($input));
    echo str_pad("'$input'", 10), $role ? "-> {$role->name}" : "-> rejected", "\n";
}
Output
'editor'  -> Editor
'ADMIN'   -> Admin
'hacker'  -> rejected
''        -> rejected

Rules

  • Enum cases can't be created with new, and enums can't have properties (other than the built-in name and value) or be extended.
  • A backed enum's values must be unique and all of the same type, int or string.
  • Enums can implement interfaces and use traits (as long as the traits have no properties).

Try it yourself

Model a traffic light with an enum, with a next() method and an action() method.

Try it yourself · main.php
Edit the code, copy it into main.php, then run it with php main.php (PHP 8.5). How to install PHP
Output
Red   stop
Green go
Amber get ready to stop
Red   stop