PHP Enums
What Is an Enum?
In simple terms: An enum (enumeration) defines a type with a fixed list of possible values. An order status can only be pending, paid, shipped, delivered, or cancelled — nothing else. With an enum, PHP itself guarantees that a variable holds one of those values, so typos like 'shiped' become impossible.
Technically: Enums were added in PHP 8.1. An enum is declared with the enum keyword and contains cases. Each case is a singleton object of the enum type. Enums can be pure (cases only) or backed (each case has a string or int value). Enums can have methods, constants, static methods, and implement interfaces, but they cannot have properties (state) and cannot be instantiated with new.
Why Enums Instead of Constants or Strings?
| Strings / Class Constants | Enums |
|---|---|
Any string is accepted: setStatus('shiped') | Only valid cases are accepted: setStatus(OrderStatus::Shipped) |
No type for parameters (string $status) | A real type: OrderStatus $status |
Behavior lives elsewhere (switch on strings) | Behavior lives on the enum ($status->label()) |
| No built-in list of values | OrderStatus::cases() |
Pure Enums
<?php
enum Suit
{
case Hearts;
case Diamonds;
case Clubs;
case Spades;
public function color(): string
{
return match ($this) {
Suit::Hearts, Suit::Diamonds => 'Red',
Suit::Clubs, Suit::Spades => 'Black',
};
}
}
$suit = Suit::Diamonds;
echo $suit->name . " is " . $suit->color() . "\n";
var_dump($suit === Suit::Diamonds);
var_dump($suit instanceof Suit);
echo count(Suit::cases()) . " suits\n";
Output
Diamonds is Red
bool(true)
bool(true)
4 suits
How It Works
- Each
casedefines one possible value.Suit::Diamondsis an object of typeSuit. - Every case is a singleton: there is exactly one
Suit::Diamondsobject, so===comparison works naturally. ->nameholds the case name as a string.- Methods use
$this, which is the current case;match ($this)is the idiomatic way to give each case its own behavior. Becausematchis exhaustive, forgetting a case throws anUnhandledMatchError. cases()returns all cases in declaration order.
Backed Enums
Backed enums associate each case with a scalar value — ideal for storing in databases, sending in JSON, and reading from forms:
<?php
enum OrderStatus: string
{
case Pending = 'pending';
case Paid = 'paid';
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::Shipped => 'On the way',
self::Delivered => 'Delivered',
self::Cancelled => 'Cancelled',
};
}
}
$status = OrderStatus::from('shipped');
echo $status->name . ' / ' . $status->value . ' / ' . $status->label() . "\n";
var_dump(OrderStatus::tryFrom('lost'));
try {
OrderStatus::from('lost');
} catch (ValueError $e) {
echo $e->getMessage() . "\n";
}
echo implode(', ', array_column(OrderStatus::cases(), 'value')) . "\n";
echo json_encode(['order' => 501, 'status' => OrderStatus::Paid]) . "\n";
Output
Shipped / shipped / On the way
NULL
"lost" is not a valid backing value for enum OrderStatus
pending, paid, shipped, delivered, cancelled
{"order":501,"status":"paid"}
Key Features of Backed Enums
| Feature | Description |
|---|---|
enum Name: string / : int | The backing type; every case must have a unique value of that type |
->value | The case's backing value ('shipped') |
->name | The case's name ('Shipped') |
Enum::from($value) | Returns the case, or throws ValueError if the value is invalid |
Enum::tryFrom($value) | Returns the case, or null if the value is invalid |
json_encode() | Encodes a backed case as its value |
Use from() when an invalid value indicates a bug (for example, corrupted database data) and tryFrom() when handling user input that may be invalid. Pure enums cannot be JSON-encoded, because they have no value.
Enums as Types
Enums are real types, usable for parameters, properties, and return types:
<?php
enum OrderStatus: string
{
case Pending = 'pending';
case Paid = 'paid';
case Shipped = 'shipped';
case Delivered = 'delivered';
case Cancelled = 'cancelled';
public function canTransitionTo(self $next): bool
{
return in_array($next, match ($this) {
self::Pending => [self::Paid, self::Cancelled],
self::Paid => [self::Shipped, self::Cancelled],
self::Shipped => [self::Delivered],
self::Delivered, self::Cancelled => [],
}, true);
}
}
final class Order
{
private OrderStatus $status = OrderStatus::Pending;
public function __construct(public readonly int $id) {}
public function moveTo(OrderStatus $next): void
{
if (!$this->status->canTransitionTo($next)) {
throw new LogicException("Order {$this->id}: cannot go from {$this->status->value} to {$next->value}.");
}
$this->status = $next;
echo "Order {$this->id} is now {$next->value}\n";
}
}
$order = new Order(501);
$order->moveTo(OrderStatus::Paid);
$order->moveTo(OrderStatus::Shipped);
try {
$order->moveTo(OrderStatus::Cancelled);
} catch (LogicException $e) {
echo $e->getMessage() . "\n";
}
Output
Order 501 is now paid
Order 501 is now shipped
Order 501: cannot go from shipped to cancelled.
Why This Is Better Than Strings
moveTo()accepts onlyOrderStatuscases — invalid statuses cannot even be passed.- The allowed transitions are defined on the enum, in one place;
matchforces every case to be considered. - Adding a new status (for example,
Returned) makes every exhaustivematchwithout a matching arm fail loudly until it is handled.
Interfaces, Constants, and Static Methods
<?php
interface HasLabel
{
public function label(): string;
}
enum PaymentMethod: string implements HasLabel
{
case Upi = 'upi';
case Card = 'card';
case CashOnDelivery = 'cod';
public const DEFAULT = self::Upi;
public function label(): string
{
return match ($this) {
self::Upi => 'UPI',
self::Card => 'Credit/Debit Card',
self::CashOnDelivery => 'Cash on Delivery',
};
}
public static function options(): array
{
return array_combine(
array_column(self::cases(), 'value'),
array_map(fn(self $method): string => $method->label(), self::cases())
);
}
}
print_r(PaymentMethod::options());
echo PaymentMethod::DEFAULT->label() . "\n";
var_dump(PaymentMethod::Card instanceof HasLabel);
Output
Array
(
[upi] => UPI
[card] => Credit/Debit Card
[cod] => Cash on Delivery
)
UPI
bool(true)
options() produces exactly the value => label array needed for an HTML <select>: the form can never offer a payment method that the code does not support, and the submitted value is converted back with PaymentMethod::tryFrom($_POST['payment'] ?? '').
Restrictions
| Restriction | Reason |
|---|---|
| No properties (state) | Cases are constants; they must not change |
Cannot be instantiated (new OrderStatus() → Error: Cannot instantiate enum OrderStatus) | Only the declared cases exist |
| Cannot extend or be extended | Enums are implicitly final |
| Cannot be cloned | Each case is a singleton |
Comparing with < or > is meaningless | Compare with ===, or compare backing values explicitly |
Enums can use traits (without properties), declare constants, and implement interfaces.
Enums and Databases
Store the backing value in the database ('shipped', or an integer) and convert when loading:
$status = OrderStatus::from($row['status']); // database → enum
$params = ['status' => $order->status()->value]; // enum → database
String backing values are easier to read in the database and in API responses than integers; integer values are more compact. Choose stable values: once stored, changing a backing value requires a data migration. Many ORMs (Doctrine, Laravel Eloquent) convert enums automatically through casts.
Common Mistakes
Comparing Enums With Strings
Effect: $status === 'paid' is always false, because $status is an object.
Fix: Compare with the case ($status === OrderStatus::Paid) or compare $status->value.
Using from() on User Input
Effect: Invalid input throws a ValueError.
Fix: Use tryFrom() and handle null.
Non-Exhaustive Logic With switch
Effect: New cases are silently ignored.
Fix: Use match without a default arm, so missing cases throw UnhandledMatchError.
Trying to Store State in Enums
Effect: Properties are not allowed.
Fix: Keep state in classes; enums describe fixed values and their behavior.
Best Practices
- Use enums for closed sets of values: statuses, roles, types, payment methods.
- Use backed enums for values that are stored or transmitted, with stable backing values.
- Put behavior related to each value (labels, colors, rules) on the enum, using
match ($this). - Convert external input with
tryFrom(), and trusted stored data withfrom(). - Type-hint enums in parameters and properties instead of using strings.
Official reference: https://www.php.net/manual/en/language.enumerations.php