Enums (PHP 8.1+) — pure enums, backed enums, enum methods
Concept
Enums (enumerations) were added in PHP 8.1 as a first-class language construct. Before enums, PHP developers used class constants or string/integer conventions to represent a fixed set of values — both approaches allowed invalid states and provided no type safety. Enums solve this: an enum is a type that can only hold one of its defined cases, enforced at the engine level.
PHP has two flavors. Pure enums have no backing value — the cases exist purely as named singletons. Backed enums associate each case with a scalar value (int or string), enabling serialization, deserialization, and database storage. A backed enum implements the BackedEnum interface and gains from(scalar) and tryFrom(scalar) methods: from() throws ValueError on an invalid input while tryFrom() returns null, mirroring the match/null-coalescing pattern common in safe parsing.
Enums are not classes but they can implement interfaces, use traits, and define methods. This makes them powerful: a Status enum can carry a method label() that returns a human-readable string, eliminating a parallel switch statement elsewhere in the application. Enum cases can be used as default parameter values and as attribute arguments. Each case is a singleton — Status::Active === Status::Active is always true.
Enums integrate naturally with Laravel. Eloquent supports enum casting: protected $casts = ['status' => Status::class] will automatically cast the database column to the enum case and back. Laravel's routing, validation, and form requests all support enum-typed parameters. Artisan command arguments can be typed to enums. Livewire and Filament both handle backed enums in forms out of the box.
A subtle gotcha: enums cannot be instantiated with new — you access cases via EnumName::CaseName. They also cannot have mutable state (no regular properties, only constants). If you need to associate extra data with a case at runtime, use a method that derives the data from the case name, or pair the enum with a value object.
Code Example
<?php
declare(strict_types=1);
// Pure enum — no backing scalar
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',
};
}
}
echo Suit::Hearts->color(); // red
// Backed enum — backed by string (stored in DB, serialized to JSON)
enum OrderStatus: string
{
case Pending = 'pending';
case Confirmed = 'confirmed';
case Shipped = 'shipped';
case Delivered = 'delivered';
case Cancelled = 'cancelled';
public function label(): string
{
return match($this) {
self::Pending => 'Awaiting confirmation',
self::Confirmed => 'Order confirmed',
self::Shipped => 'On the way',
self::Delivered => 'Delivered',
self::Cancelled => 'Cancelled',
};
}
public function canTransitionTo(OrderStatus $next): bool
{
$allowed = [
self::Pending->value => [self::Confirmed, self::Cancelled],
self::Confirmed->value => [self::Shipped, self::Cancelled],
self::Shipped->value => [self::Delivered],
self::Delivered->value => [],
self::Cancelled->value => [],
];
return in_array($next, $allowed[$this->value], strict: true);
}
// Implement an interface on an enum
public function isFinal(): bool
{
return in_array($this, [self::Delivered, self::Cancelled], strict: true);
}
}
// from() — throws ValueError for unknown values
$status = OrderStatus::from('shipped'); // OrderStatus::Shipped
// tryFrom() — returns null for unknown values (safe parsing)
$unknown = OrderStatus::tryFrom('refunded'); // null
// Transition guard
$current = OrderStatus::Confirmed;
if ($current->canTransitionTo(OrderStatus::Shipped)) {
echo "Transitioning to shipped\n";
}
// Enum cases() — returns all cases as array
$allStatuses = OrderStatus::cases();
foreach ($allStatuses as $case) {
echo "{$case->value}: {$case->label()}\n";
}
// In Laravel Eloquent model (conceptual — not runnable standalone):
// class Order extends Model {
// protected $casts = ['status' => OrderStatus::class];
// }
// $order->status // OrderStatus instance
// $order->status === OrderStatus::Shipped // trueInterview Q&A
Q: What is the difference between from() and tryFrom() on a backed enum, and when should you use each?
from($value) attempts to find the enum case matching the scalar and throws a \ValueError if none exists — appropriate when the caller guarantees the input is valid (e.g., reading from your own database column where a constraint ensures only valid values are stored). tryFrom($value) returns null on failure — appropriate for external input like JSON payloads, query strings, or third-party API responses where an unexpected value should degrade gracefully rather than cause an uncaught exception. A common pattern is OrderStatus::tryFrom($input) ?? throw new InvalidArgumentException("Bad status: {$input}"), which gives you both the null-safety of tryFrom and an explicit, descriptive error message.
Q: How does Laravel's Eloquent enum casting work internally, and what does it require of the enum?
When you declare protected $casts = ['status' => OrderStatus::class], Eloquent's CastsInboundAttributes system detects that the class implements BackedEnum. On read, it calls OrderStatus::from($rawDatabaseValue) to hydrate the PHP enum case. On write (setting the attribute), it calls ->value on the enum instance to extract the scalar before storing. The enum must be a backed enum — pure enums cannot be stored and retrieved this way because they have no scalar representation. If you use tryFrom() behavior (i.e., tolerate missing values), you need to write a custom cast class that wraps tryFrom().
Q: Can an enum implement an interface? What about using traits?
Yes to both. An enum can implement one or more interfaces and provide the required method bodies inside the enum body — this is the primary mechanism for adding polymorphic behavior to enum cases. For example, a HasLabel interface with label(): string can be implemented by multiple enums. Traits work too: a trait can add concrete methods to an enum, but the trait itself cannot define properties (since enums cannot have instance properties). Constants are allowed in enums, and constant expressions can reference enum cases. The one restriction is that the trait cannot define abstract methods that conflict with the enum's own restrictions.