Simplify Status Handling in PHP with Backed Enums
Why enums beat magic strings
For years I reached for constants or plain strings to represent states like pending, shipped, or cancelled. The problem? A typo in 'shipped' becomes a silent bug that only surfaces in production. PHP 8.1 introduced backed enums, giving you a first‑class type that the engine can verify at compile time.
Real‑world scenario: order processing
Imagine an e‑commerce service that moves an Order through a workflow. Each step — payment, packing, shipping — depends on the current status. Using strings forces you to sprinkle if ($order->status === 'shipped') everywhere, and a new status means hunting down every comparison.
Defining a backed enum
enum OrderStatus: string
{
case Pending = 'pending';
case Paid = 'paid';
case Packed = 'packed';
case Shipped = 'shipped';
case Delivered = 'delivered';
case Cancelled = 'cancelled';
public function label(): string
{
return match ($this) {
self::Pending => 'Awaiting payment',
self::Paid => 'Payment received',
self::Packed => 'Ready for shipment',
self::Shipped => 'On the way',
self::Delivered => 'Delivered',
self::Cancelled => 'Cancelled',
};
}
}
Using the enum in a service
class OrderService
{
public function transition(Order $order, OrderStatus $next): void
{
$allowed = $this->allowedTransitions($order->status);
if (!in_array($next, $allowed, true)) {
throw new \DomainException("Invalid transition from {$order->status->value} to {$next->value}" );
}
$order->status = $next;
$order->save();
}
private function allowedTransitions(OrderStatus $current): array
{
return match ($current) {
OrderStatus::Pending => [OrderStatus::Paid, OrderStatus::Cancelled],
OrderStatus::Paid => [OrderStatus::Packed, OrderStatus::Cancelled],
OrderStatus::Packed => [OrderStatus::Shipped],
OrderStatus::Shipped => [OrderStatus::Delivered],
OrderStatus::Delivered => [],
OrderStatus::Cancelled => [],
};
}
}
Benefits you’ll notice immediately
- Type safety – the compiler rejects any value that isn’t an
OrderStatuscase. - Self‑documenting –
$order->status->label()gives a human‑readable string without a separate lookup table. - Exhaustive matching – the
matchexpression forces you to handle every case; adding a new status produces a compile‑time error until you update the logic. - Database friendliness – backed enums store the scalar value ('pending', 'paid', …) directly, so migrations stay simple.
Migration tip
If you’re refactoring a legacy codebase, start by creating the enum and a thin wrapper that accepts both the old string and the new enum. Gradually replace string checks with $status instanceof OrderStatus and let static analysis (Psalm, PHPStan) surface the remaining spots.
Backed enums turn a class of runtime surprises into compile‑time guarantees — exactly the kind of safety net that lets you ship faster with confidence.