Enums (PHP 8.1)
An enum (short for "enumeration") lets you define a fixed, closed set of possible values as a first-class type, instead of scattering loose class constants or magic strings throughout a codebase. Before PHP 8.1, developers faked enums with a class full of const declarations — that pattern compiled fine, but nothing stopped you from passing an unrelated integer or string wherever that "enum" was expected. A real enum closes that gap: PHP itself enforces that a value of the enum's type can only ever be one of the declared cases.
Pure enums: a named set of cases
The simplest form is a "pure" enum, declared with the enum keyword and a list of case values. Pure enum cases don't carry an underlying scalar value — each case is just itself.
A pure enum for order status
<?php
enum OrderStatus
{
case Pending;
case Shipped;
case Delivered;
case Cancelled;
}
function describe(OrderStatus $status): string
{
return match ($status) {
OrderStatus::Pending => 'Waiting to be processed',
OrderStatus::Shipped => 'On its way',
OrderStatus::Delivered => 'Arrived at destination',
OrderStatus::Cancelled => 'Order was cancelled',
};
}
$current = OrderStatus::Shipped;
echo describe($current);On its way
OrderStatus::Shipped is a genuine value of type OrderStatus — the function signature describe(OrderStatus $status) guarantees, at the type level, that only one of the four declared cases can ever be passed in. Pairing an enum with match is a common style because match performs strict comparison and has no fallthrough, so each case is handled exactly once and explicitly.
Backed enums: attaching a scalar value
A "backed" enum attaches a scalar of type string or int to every case, written after the enum name with a colon. Backed enums are the natural fit whenever the values need to be stored in a database column, sent through an API response, or otherwise serialized as plain data.
A string-backed enum
<?php
enum OrderStatus: string
{
case Pending = 'pending';
case Shipped = 'shipped';
case Delivered = 'delivered';
case Cancelled = 'cancelled';
}
$status = OrderStatus::Delivered;
echo $status->name, "\n";
echo $status->value, "\n";Delivered delivered
Every case of a backed enum exposes two read-only properties: name, the identifier you wrote after case (always a string), and value, the scalar you assigned to it. A pure enum only has name — there is no underlying value to expose, because you never declared one.
cases(): listing every member
Every enum, pure or backed, automatically gets a static cases() method that returns an array of all its cases in declaration order. This is useful for populating a dropdown, validating input against the full set of options, or simply iterating.
Iterating over every case
<?php
enum OrderStatus: string
{
case Pending = 'pending';
case Shipped = 'shipped';
case Delivered = 'delivered';
case Cancelled = 'cancelled';
}
foreach (OrderStatus::cases() as $case) {
echo "{$case->name} => {$case->value}\n";
}Pending => pending Shipped => shipped Delivered => delivered Cancelled => cancelled
from() and tryFrom(): converting a raw value back
Backed enums also gain two static factory methods for going the other direction — turning a plain scalar (say, a value just read out of a database row or a JSON payload) back into a proper enum instance. from() and tryFrom() do the same lookup, but they disagree on what happens when the value doesn't match any case.
from() throws, tryFrom() returns null
<?php
enum OrderStatus: string
{
case Pending = 'pending';
case Shipped = 'shipped';
case Delivered = 'delivered';
case Cancelled = 'cancelled';
}
$valid = OrderStatus::from('shipped');
echo $valid->name, "\n";
$maybe = OrderStatus::tryFrom('lost-in-transit');
var_dump($maybe);
try {
OrderStatus::from('lost-in-transit');
} catch (\ValueError $e) {
echo 'Caught: ' . $e->getMessage() . "\n";
}Shipped NULL Caught: "lost-in-transit" is not a valid backing value for enum "OrderStatus"
from() is the right choice when an unrecognized value represents a bug or corrupted data that should stop execution loudly. tryFrom() is the right choice when an unrecognized value is an expected, recoverable outcome — for example, validating user-submitted input where you want to show a friendly error message instead of letting an uncaught ValueError crash the request.
Enums can have methods and implement interfaces
An enum is a proper class-like construct: it can declare methods, implement interfaces, and use traits, though it cannot have mutable instance properties, since every case is a single, shared, immutable instance created once by PHP.
An enum with a method and an interface
<?php
interface HasColor
{
public function color(): string;
}
enum OrderStatus: string implements HasColor
{
case Pending = 'pending';
case Shipped = 'shipped';
case Delivered = 'delivered';
case Cancelled = 'cancelled';
public function color(): string
{
return match ($this) {
self::Pending => 'gray',
self::Shipped => 'blue',
self::Delivered => 'green',
self::Cancelled => 'red',
};
}
public function isFinal(): bool
{
return $this === self::Delivered || $this === self::Cancelled;
}
}
$status = OrderStatus::Delivered;
echo $status->color(), "\n";
var_dump($status->isFinal());
var_dump($status instanceof HasColor);green bool(true) bool(true)
Because $status truly implements HasColor, it can be passed to any function that type-hints HasColor, which lets calling code depend on the interface without caring that the concrete implementation happens to be an enum. Enums can also declare public const values and even static methods, though static methods on an enum can't reference $this since they aren't called on a specific case.
Why enums replace the old constants pattern
The pre-8.1 workaround looked something like a class with public const PENDING = 'pending'; and three siblings next to it. It "worked" in the sense that the values existed and had names, but PHP had no idea those constants were meant to form a closed set. A function typed to accept a string would happily accept 'pending', 'PENDING', 'penidng' (a typo), or any other string — nothing enforced membership in the intended set except code review and hope. A real enum turns that whole category of bug into a type error caught long before runtime, and it bundles behavior (via methods) and identity (via name/value) into one place instead of a loose bag of constants plus scattered match/switch statements elsewhere in the code.
Pure enums (
enum Foo { case A; }) have no underlying scalar — onlynameis available.Backed enums (
enum Foo: string { ... }or: int) expose bothnameandvalue, and require every case to declare a value.cases()returns every case in declaration order — useful for iteration and validation.from()throws aValueErrorfor an unmatched value;tryFrom()returnsnullinstead.Enums can implement interfaces and declare methods, just like classes, but cannot hold mutable instance state.