PHP 8 Enums Deep Dive: Backed Enums, Methods, and State Machines
Master PHP 8 enums with backed values, methods, and state machines.
20+ years shipping production PHP systems at scale. Lessons pulled from things that broke in production.
- ✓Basic knowledge of PHP syntax
- ✓Understanding of classes and objects
- ✓Familiarity with type declarations
PHP 8.1 enums let you define a fixed set of possible values with methods and backed (string/int) cases. Use backed enums when you need to map to a database or API, and add methods to encapsulate behavior—like a state machine—instead of scattering switch statements across your codebase.
Think of an enum like a menu at a restaurant. The menu lists only the dishes available (the cases). If you order something not on the menu, the waiter says 'sorry, we don't have that.' In programming, enums restrict a variable to only a set of predefined values, preventing invalid states. Backed enums are like having a dish number (e.g., #1 for burger) that you can use to quickly reference the dish.
Have you ever found yourself using a bunch of constants or strings to represent a fixed set of states, like order statuses or user roles? You might have used class constants, but they lack type safety—any string can be passed, leading to bugs. PHP 8 introduced enums, a first-class language feature that solves this elegantly. Enums allow you to define a type with a finite set of possible values, each being a singleton object. This brings clarity, safety, and expressiveness to your code.
In this deep dive, we'll explore backed enums (with scalar values), adding methods to enums, and implementing state machines. You'll see how enums can replace boolean flags, magic strings, and complex if-else chains. We'll also cover real-world production incidents where enums prevented bugs, and a debugging guide for when things go wrong. By the end, you'll be ready to refactor your legacy code with enums and build robust applications.
What Are PHP Enums?
PHP 8.1 introduced enums as a built-in type. An enum is a special kind of class that can only have a fixed number of instances, called cases. Each case is a singleton object. Enums are perfect for representing a set of possible values that don't change, like days of the week, order statuses, or user roles.
Basic syntax:
``php enum Status { case Pending; case Approved; case Shipped; case Delivered; } ``
You can use an enum as a type hint:
``php function processOrder(Status $status): void { // $status is guaranteed to be one of the four cases } ``
This prevents passing invalid strings like 'pending' or 'shipped'. Enums also have built-in methods like name (returns the case name as a string) and cases() (returns an array of all cases).
Backed Enums: Mapping to Scalars
Often you need to store enum values in a database or send them via API. Backed enums associate each case with a scalar value (int or string). This is done by adding a type to the enum declaration and assigning values to each case.
``php enum Status: string { case Pending = 'pending'; case Approved = 'approved'; case Shipped = 'shipped'; case Delivered = 'delivered'; } ``
Now each case has a value property. You can create an enum from a backed value using (throws ValueError if invalid) or from()tryFrom() (returns null if invalid).
``php $status = Status::from('pending'); // Returns Status::Pending $status = Status::tryFrom('unknown'); // Returns null ``
Backed enums are ideal for database columns. You can store the scalar value and convert to enum on read.
```php // Storing $stmt->bindValue(':status', $order->status->value);
// Reading $row = $stmt->fetch(); $status = Status::tryFrom($row['status']); ```
This ensures that only valid values are stored and retrieved.
tryFrom() when converting user input to avoid exceptions. Log invalid inputs for debugging.Adding Methods to Enums
Enums can have methods, just like classes. This allows you to encapsulate behavior related to the enum. Methods can be instance methods (using $this) or static methods.
Instance methods can access the case's name and value. For example, you can add a label method:
```php enum Status: string { case Pending = 'pending'; case Approved = 'approved'; case Shipped = 'shipped'; case Delivered = 'delivered';
public function label(): string { return match ($this) { self::Pending => 'Pending', self::Approved => 'Approved', self::Shipped => 'Shipped', self::Delivered => 'Delivered', }; }
public function isActive(): bool { return $this === self::Approved || $this === self::Shipped; } }
$status = Status::Pending; echo $status->label(); // Pending echo $status->isActive() ? 'Active' : 'Inactive'; // Inactive ```
Static methods can be used for factory methods or utility functions:
``php echo Status::default(); // Returns Status::Pending ``
Methods make enums more powerful, allowing you to keep related logic together.
Implementing a State Machine with Enums
State machines are a common pattern where an object transitions between a finite set of states. Enums are a natural fit. You can define allowed transitions directly in the enum.
Example: Order status state machine.
```php enum OrderStatus: string { case Pending = 'pending'; case Approved = 'approved'; case Shipped = 'shipped'; case Delivered = 'delivered'; case Cancelled = 'cancelled';
public function canTransitionTo(self $newStatus): bool { return match ($this) { self::Pending => in_array($newStatus, [self::Approved, self::Cancelled], true), self::Approved => in_array($newStatus, [self::Shipped, self::Cancelled], true), self::Shipped => $newStatus === self::Delivered, self::Delivered => false, // final state self::Cancelled => false, // final state }; }
public function transitionTo(self $newStatus): self { if (!$this->canTransitionTo($newStatus)) { throw new InvalidTransitionException("Cannot transition from {$this->value} to {$newStatus->value}"); } return $newStatus; } } ```
Now you can safely transition:
``php $status = OrderStatus::Pending; $status = $status->transitionTo(OrderStatus::Approved); // works $status = $status->transitionTo(OrderStatus::Delivered); // throws exception ``
This pattern ensures invalid transitions are caught early.
Enums with Interfaces and Traits
Enums can implement interfaces and use traits, just like classes. This allows you to define common behavior across different enums.
Example: An interface for enums that have a label.
```php interface HasLabel { public function label(): string; }
enum Status: string implements HasLabel { case Pending = 'pending'; case Approved = 'approved';
public function label(): string { return ucfirst($this->value); } }
function printLabel(HasLabel $enum): void { echo $enum->label(); } ```
Traits can be used to share method implementations:
```php trait LabelTrait { public function label(): string { return ucfirst($this->value); } }
enum Status: string { use LabelTrait;
case Pending = 'pending'; case Approved = 'approved'; } ```
This promotes code reuse and keeps your enums DRY.
Serialization and Database Storage
When working with enums in production, you often need to serialize them (JSON, session) or store them in a database. Backed enums serialize to their scalar value by default when using json_encode:
``php echo json_encode(Status::Pending); // "pending" ``
For non-backed enums, you need to implement JsonSerializable:
```php enum Status implements \JsonSerializable { case Pending; case Approved;
public function jsonSerialize(): string { return $this->name; } } ```
For database storage, use the backed value. With PDO, you can bind the value:
``php $stmt = $pdo->prepare('INSERT INTO orders (status) VALUES (:status)'); $stmt->bindValue(':status', $order->status->value, PDO::PARAM_STR); $stmt->execute(); ``
When reading, convert back:
``php $row = $stmt->fetch(PDO::FETCH_ASSOC); $status = Status::tryFrom($row['status']); if ($status === null) { // handle invalid data } ``
Always validate data from the database, as it may have been corrupted or changed.
The Case of the Phantom Order Status
- Never use free-form strings for fixed sets of values; use enums or database constraints.
- Validate input at the boundary (API, form) and convert to enum immediately.
- Use backed enums to map to database values for consistency.
- Add unit tests that cover all enum cases to ensure no case is missed.
tryFrom() to safely convert and log failures.var_dump($enum) or log the case name. Check if the method uses $this->name or $this->value correctly.match ($status) { Status::Pending => ... } and include a default case to catch unexpected values.$enum->value for JSON responses.php -r "var_dump(Status::tryFrom('invalid'));"php -r "echo Status::tryFrom('pending')?->name ?? 'unknown';"$status = Status::tryFrom($input) ?? throw new InvalidArgumentException();| File | Command / Code | Purpose |
|---|---|---|
| basic-enum.php | enum Status | What Are PHP Enums? |
| backed-enum.php | enum Status: string | Backed Enums |
| enum-methods.php | enum Status: string | Adding Methods to Enums |
| state-machine.php | class InvalidTransitionException extends \RuntimeException {} | Implementing a State Machine with Enums |
| enum-interface-trait.php | interface HasLabel | Enums with Interfaces and Traits |
| serialization.php | enum Status: string | Serialization and Database Storage |
Key takeaways
tryFrom() for input conversion and === for comparison.Common mistakes to avoid
3 patternsUsing `from()` without catching ValueError for invalid input.
Comparing enums with `==` instead of `===`.
Storing enum names in the database instead of backed values.
Interview Questions on This Topic
What is the difference between a backed enum and a non-backed enum?
from() or tryFrom(), and they have a value property.Frequently Asked Questions
20+ years shipping production PHP systems at scale. Lessons pulled from things that broke in production.
That's Advanced PHP. Mark it forged?
3 min read · try the examples if you haven't