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 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";
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 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');
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 20from() 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 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";
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 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()); }
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 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"; }
'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-innameandvalue) or be extended. - A backed enum's values must be unique and all of the same type,
intorstring. - 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.
Red stop Green go Amber get ready to stop Red stop