Home PHP PHP 8 Enums Deep Dive: Backed Enums, Methods, and State Machines
Intermediate 3 min · July 13, 2026

PHP 8 Enums Deep Dive: Backed Enums, Methods, and State Machines

Master PHP 8 enums with backed values, methods, and state machines.

N
Naren Founder & Principal Engineer

20+ years shipping production PHP systems at scale. Lessons pulled from things that broke in production.

Follow
Production
production tested
July 13, 2026
last updated
2,073
articles · all by Naren
Before you start⏱ 15-20 min read
  • Basic knowledge of PHP syntax
  • Understanding of classes and objects
  • Familiarity with type declarations
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer

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.

✦ Definition~90s read
What is PHP 8 Enums?

PHP 8 enums are a language feature that lets you define a type with a fixed set of named singleton values, improving type safety and code clarity.

Think of an enum like a menu at a restaurant.
Plain-English First

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.

``php enum Status { case Pending; case Approved; case Shipped; case Delivered; } ``

``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).

basic-enum.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?php

enum Status
{
    case Pending;
    case Approved;
    case Shipped;
    case Delivered;
}

function processOrder(Status $status): void
{
    echo "Processing order with status: " . $status->name . "\n";
}

processOrder(Status::Pending);
// Output: Processing order with status: Pending

// List all cases
foreach (Status::cases() as $case) {
    echo $case->name . "\n";
}
// Output:
// Pending
// Approved
// Shipped
// Delivered
Output
Processing order with status: Pending
Pending
Approved
Shipped
Delivered
🔥Enum vs Class Constants
📊 Production Insight
Use enums for any fixed set of values that appear in business logic. They make code self-documenting and prevent invalid states.
🎯 Key Takeaway
Enums define a finite set of named values, each a singleton object, providing type safety and clarity.

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 from() (throws ValueError if invalid) or 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.

backed-enum.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?php

enum Status: string
{
    case Pending = 'pending';
    case Approved = 'approved';
    case Shipped = 'shipped';
    case Delivered = 'delivered';
}

// Create from value
$status = Status::from('pending');
echo $status->name; // Pending
echo $status->value; // pending

// Safe conversion
$input = 'shipped';
$status = Status::tryFrom($input);
if ($status === null) {
    throw new InvalidArgumentException("Invalid status: $input");
}

// Use in match
$label = match ($status) {
    Status::Pending => 'Order is pending',
    Status::Approved => 'Order approved',
    Status::Shipped => 'Order shipped',
    Status::Delivered => 'Order delivered',
};
echo $label;
Output
Pending
pending
Order shipped
⚠ Backed Values Must Be Unique
📊 Production Insight
Always use tryFrom() when converting user input to avoid exceptions. Log invalid inputs for debugging.
🎯 Key Takeaway
Backed enums map cases to scalar values, making them easy to store and retrieve from databases or APIs.

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.

enum-methods.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?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;
    }

    public static function default(): self
    {
        return self::Pending;
    }
}

$status = Status::Approved;
echo $status->label() . "\n";
echo $status->isActive() ? 'Active' : 'Inactive' . "\n";
echo Status::default()->name;
Output
Approved
Active
Pending
💡Use match for Clean Logic
📊 Production Insight
Avoid heavy logic in enum methods; keep them simple. For complex business rules, consider a service class that uses the enum.
🎯 Key Takeaway
Enums can have methods to encapsulate behavior, making them self-contained and reducing scattered switch statements.

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; } } ```

``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.

state-machine.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<?php

class InvalidTransitionException extends \RuntimeException {}

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, self::Cancelled => false,
        };
    }

    public function transitionTo(self $newStatus): self
    {
        if (!$this->canTransitionTo($newStatus)) {
            throw new InvalidTransitionException("Cannot transition from {$this->value} to {$newStatus->value}");
        }
        return $newStatus;
    }
}

$status = OrderStatus::Pending;
try {
    $status = $status->transitionTo(OrderStatus::Approved);
    echo "Transitioned to: " . $status->value . "\n";
    $status = $status->transitionTo(OrderStatus::Delivered); // will throw
} catch (InvalidTransitionException $e) {
    echo $e->getMessage() . "\n";
}
Output
Transitioned to: approved
Cannot transition from approved to delivered
🔥Exhaustive Matching
📊 Production Insight
Consider using a dedicated exception class for invalid transitions to make error handling precise.
🎯 Key Takeaway
Enums with methods can implement state machines cleanly, enforcing valid transitions and reducing bugs.

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(); } ```

```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.

enum-interface-trait.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?php

interface HasLabel
{
    public function label(): string;
}

trait LabelTrait
{
    public function label(): string
    {
        return ucfirst($this->value);
    }
}

enum Status: string implements HasLabel
{
    use LabelTrait;

    case Pending = 'pending';
    case Approved = 'approved';
}

function printLabel(HasLabel $enum): void
{
    echo $enum->label() . "\n";
}

printLabel(Status::Pending);
printLabel(Status::Approved);
Output
Pending
Approved
💡Interface Segregation
📊 Production Insight
Use interfaces to define contracts for enum behavior, making your code more flexible and testable.
🎯 Key Takeaway
Enums can implement interfaces and use traits, enabling polymorphic behavior and code reuse.

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" ``

```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(); ``

``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.

serialization.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php

enum Status: string
{
    case Pending = 'pending';
    case Approved = 'approved';
}

// JSON serialization
echo json_encode(Status::Pending) . "\n"; // "pending"

// Database example with PDO
$pdo = new PDO('sqlite::memory:');
$pdo->exec('CREATE TABLE orders (status TEXT)');

$stmt = $pdo->prepare('INSERT INTO orders (status) VALUES (:status)');
$stmt->bindValue(':status', Status::Pending->value, PDO::PARAM_STR);
$stmt->execute();

$stmt = $pdo->query('SELECT status FROM orders');
$row = $stmt->fetch(PDO::FETCH_ASSOC);
$status = Status::tryFrom($row['status']);
echo $status?->name ?? 'Invalid';
Output
"pending"
Pending
⚠ Database Migration
📊 Production Insight
Consider using a custom cast in your ORM (like Laravel's casts) to automatically convert between enum and database value.
🎯 Key Takeaway
Backed enums serialize to their scalar value automatically. Always validate data when converting from external sources.
● Production incidentPOST-MORTEMseverity: high

The Case of the Phantom Order Status

Symptom
Orders with status 'shipped' were not being processed for payment capture, leading to revenue loss.
Assumption
The developer assumed the status was being set correctly from the frontend.
Root cause
The status was stored as a string in the database, and a typo ('shipped' instead of 'shipped') caused the backend to treat it as an unknown status.
Fix
Replaced the string column with an enum column (or backed enum) to enforce valid values at the database and application level.
Key lesson
  • 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.
Production debug guideSymptom to Action4 entries
Symptom · 01
Unexpected enum case value (e.g., 'pending' instead of 'pending')
Fix
Check the source of the value (database, API input). Ensure it matches exactly one of the backed values. Use tryFrom() to safely convert and log failures.
Symptom · 02
Method on enum returns unexpected result
Fix
Verify the enum case is the one you expect. Use var_dump($enum) or log the case name. Check if the method uses $this->name or $this->value correctly.
Symptom · 03
Enum not found in switch/match statement
Fix
Ensure you are using the enum instance, not the backed value. Use match ($status) { Status::Pending => ... } and include a default case to catch unexpected values.
Symptom · 04
Serialization issues (JSON, session)
Fix
Enums are serializable by default. If using backed enums, ensure the backed value is used for storage. Use $enum->value for JSON responses.
★ Quick Debug Cheat SheetCommon enum issues and immediate fixes
Invalid enum case passed to function
Immediate action
Use `tryFrom()` to safely convert and handle null.
Commands
php -r "var_dump(Status::tryFrom('invalid'));"
php -r "echo Status::tryFrom('pending')?->name ?? 'unknown';"
Fix now
Add input validation: $status = Status::tryFrom($input) ?? throw new InvalidArgumentException();
Enum method not working as expected+
Immediate action
Check if method is static or instance. Use `$enum->method()` for instance, `EnumClass::method()` for static.
Commands
php -r "echo Status::Pending->label();"
php -r "echo Status::cases()[0]->label();"
Fix now
Ensure method is defined correctly and uses $this for instance methods.
Comparing enums with == instead of ===+
Immediate action
Use === for strict comparison. Enum instances are singletons, so === works.
Commands
php -r "var_dump(Status::Pending === Status::Pending);"
php -r "var_dump(Status::Pending == Status::Pending);"
Fix now
Always use === for enum comparison.
FeatureClass ConstantsEnums
Type safetyNo (any string accepted)Yes (dedicated type)
Singleton per valueNoYes
MethodsStatic onlyInstance and static
Backed valuesManual mappingBuilt-in
Exhaustive matchingNoYes with match
SerializationManualAutomatic for backed
⚙ Quick Reference
6 commands from this guide
FileCommand / CodePurpose
basic-enum.phpenum StatusWhat Are PHP Enums?
backed-enum.phpenum Status: stringBacked Enums
enum-methods.phpenum Status: stringAdding Methods to Enums
state-machine.phpclass InvalidTransitionException extends \RuntimeException {}Implementing a State Machine with Enums
enum-interface-trait.phpinterface HasLabelEnums with Interfaces and Traits
serialization.phpenum Status: stringSerialization and Database Storage

Key takeaways

1
Enums provide type-safe, self-documenting code for fixed sets of values.
2
Backed enums map to scalar values, ideal for database storage and API communication.
3
Methods on enums encapsulate behavior, reducing scattered switch statements.
4
State machines can be implemented cleanly with enums and transition methods.
5
Always use tryFrom() for input conversion and === for comparison.

Common mistakes to avoid

3 patterns
×

Using `from()` without catching ValueError for invalid input.

×

Comparing enums with `==` instead of `===`.

×

Storing enum names in the database instead of backed values.

INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What is the difference between a backed enum and a non-backed enum?
Q02SENIOR
How would you implement a state machine using enums?
Q03SENIOR
Can enums implement interfaces? Give an example.
Q01 of 03JUNIOR

What is the difference between a backed enum and a non-backed enum?

ANSWER
A backed enum associates each case with a scalar value (int or string), while a non-backed enum has no associated value. Backed enums can be created from a scalar using from() or tryFrom(), and they have a value property.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
Can I add properties to an enum?
02
Can I extend an enum?
03
How do I compare enums?
04
Can I use enums as array keys?
05
What happens if I try to instantiate an enum with `new`?
N
Naren Founder & Principal Engineer

20+ years shipping production PHP systems at scale. Lessons pulled from things that broke in production.

Follow
Verified
production tested
July 13, 2026
last updated
2,073
articles · all by Naren
🔥

That's Advanced PHP. Mark it forged?

3 min read · try the examples if you haven't

Previous
PHP 8 Attributes Deep Dive: Validation, Routing, and Metadata
17 / 29 · Advanced PHP
Next
PHP DTOs and Value Objects with Readonly Classes