Enums in Laravel: Learn how to leverage PHP

Enums in Laravel: Learn how to leverage PHP

PHP 8.1 introduced native enumeration support, a long-awaited feature that brings type safety, clarity, and self-documentation capabilities to PHP applications. Laravel, being a cutting-edge PHP framework, has embraced enums and offers several convenient ways to integrate them seamlessly into your application. This article delves deep into Laravel enums: why they matter, how to create and use them, advanced patterns, and real-world case studies. By the end, you’ll understand how to leverage enums for cleaner, more maintainable, and self-documenting code.

Table of Contents

  1. Why Enums Matter
  2. PHP 8.1 Enums Recap
  3. Defining Enums in Laravel
  4. Using Enums in Eloquent Models
  5. Form Requests & Validation with Enums
  6. Enums in Blade Templates
  7. Advanced Enum Patterns
    • Backed Enums
    • Methods on Enums
    • Traits for Shared Behavior
  8. Integrating Enums with Query Scopes
  9. Testing Enums
  10. Real-World Case Study
  11. Best Practices & Gotchas
  12. Conclusion

Why Enums Matter

In large codebases, developers often rely on “magic strings” or constants to represent discrete sets of values—think user roles ('admin', 'editor', 'subscriber'), order statuses ('pending', 'shipped', 'delivered'), or payment methods ('credit_card', 'paypal', 'bank_transfer'). While constants in PHP classes can offer some degree of safety, they lack:

  • Type Safety: You can accidentally pass an invalid string.
  • Discoverability: IDEs can’t always infer available values.
  • Self-Documentation: It’s not immediately clear where a set of constants belong.

Enums solve all of these by bundling related values into a dedicated, strongly-typed construct. They make invalid values impossible to pass at compile time (or throw errors at runtime), improve code clarity, and allow IDEs and static analysis tools to offer better autocomplete and error-checking.

PHP 8.1 Enums Recap

Before diving into Laravel-specific usage, let’s recap native PHP enums:

<?php
// Basic Enum (Pure Enum)
enum UserRole
{
    case Admin;
    case Editor;
    case Subscriber;
}

// Backed Enum (String)
enum OrderStatus: string
{
    case Pending   = 'pending';
    case Shipped   = 'shipped';
    case Delivered = 'delivered';
}

// Backed Enum (Int)
enum HttpStatus: int
{
    case OK       = 200;
    case NotFound = 404;
    case Error    = 500;
}

Key points:

  • Pure enums have cases without underlying values.
  • Backed enums associate each case with a string or integer value.
  • Enums can have methods, implement interfaces, and even use traits.

Defining Enums in Laravel

In a Laravel project, you might organize your enums under app/Enums. Create the directory if it doesn’t exist:

mkdir app/Enums

Then define an enum:

<?php

namespace App\Enums;

enum PaymentMethod: string
{
    case CreditCard   = 'credit_card';
    case PayPal       = 'paypal';
    case BankTransfer = 'bank_transfer';
}

Laravel’s autoloader will pick this up automatically. Use PSR-4 conventions: namespace matches directory structure.

Using Enums in Eloquent Models

Eloquent model attributes can be cast to enum instances, allowing you to interact with them as objects rather than raw strings:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use App\Enums\OrderStatus;

class Order extends Model
{
    protected $casts = [
        'status' => OrderStatus::class,
    ];
}

Now, when you retrieve an order:

$order = Order::find(1);

// $order->status is an instance of OrderStatus
if ($order->status === OrderStatus::Shipped) {
    // ...
}

// When assigning:
$order->status = OrderStatus::Delivered;
$order->save();

Under the hood, Laravel serializes the enum’s underlying value to the database and rehydrates it back into an enum instance.

Form Requests & Validation with Enums

Validating request input against enums is straightforward. In your FormRequest or controller validation:

use Illuminate\Validation\Rules\Enum;

public function rules(): array
{
    return [
        'payment_method' => ['required', new Enum(PaymentMethod::class)],
    ];
}

This rule ensures the incoming payment_method matches one of the defined enum values, reducing boilerplate and improving error messages.

Enums in Blade Templates

Enums integrate nicely with Blade. You can loop over enum cases to generate select options, radio buttons, or any UI element that reflects available choices.

<select name="status">
    @foreach(\App\Enums\OrderStatus::cases() as $status)
        <option value="{{ $status->value }}" @selected($order->status === $status)>
            {{ ucfirst($status->value) }}
        </option>
    @endforeach
</select>

Alternatively, you can define a helper on the enum for user-friendly labels:

enum OrderStatus: string
{
    case Pending   = 'pending';
    case Shipped   = 'shipped';
    case Delivered = 'delivered';

    public function label(): string
    {
        return match($this) {
            self::Pending   => 'Pending Shipment',
            self::Shipped   => 'Shipped to Customer',
            self::Delivered => 'Delivered Successfully',
        };
    }
}

Then in Blade:

<option value="{{ $status->value }}" @selected($order->status === $status)>
    {{ $status->label() }}
</option>

Advanced Enum Patterns

Backed Enums

Backed enums are essential when you need to persist values:

enum UserRole: string
{
    case Admin      = 'admin';
    case Editor     = 'editor';
    case Subscriber = 'subscriber';
}

Use ->value to get the underlying string/int, and UserRole::from('admin') or UserRole::tryFrom('admin') to rehydrate.

Methods on Enums

Enums can contain methods to encapsulate behavior:

enum PaymentMethod: string
{
    case CreditCard   = 'credit_card';
    case PayPal       = 'paypal';
    case BankTransfer = 'bank_transfer';

    public function process(float $amount): bool
    {
        return match($this) {
            self::CreditCard   => $this->processCreditCard($amount),
            self::PayPal       => $this->processPayPal($amount),
            self::BankTransfer => $this->processBankTransfer($amount),
        };
    }

    protected function processCreditCard(float $amount): bool
    {
        // Integration with gateway...
    }

    // ...
}

This approach keeps payment logic encapsulated in the enum, preventing sprawling switch statements across services.

Traits for Shared Behavior

If multiple enums share behavior—such as providing labels or JSON serialization—you can use traits:

<?php

namespace App\Traits;

trait EnumHelpers
{
    public static function toSelectArray(): array
    {
        return array_map(fn($case) => $case->value, self::cases());
    }

    public function label(): string
    {
        // Default label generation
        return ucwords(str_replace('_', ' ', $this->value));
    }
}

Then in your enum:

enum OrderStatus: string
{
    use \App\Traits\EnumHelpers;

    case Pending   = 'pending';
    case Shipped   = 'shipped';
    case Delivered = 'delivered';
}

Integrating Enums with Query Scopes

You can define query scopes that accept enums directly:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use App\Enums\OrderStatus;

class Order extends Model
{
    protected $casts = [
        'status' => OrderStatus::class,
    ];

    public function scopeOfStatus(Builder $query, OrderStatus $status): Builder
    {
        return $query->where('status', $status->value);
    }
}

Usage:

$shippedOrders = Order::ofStatus(OrderStatus::Shipped)->get();

This provides a fluent, type-safe way to filter by enum.

Testing Enums

When writing tests, you can assert enum-based responses:

public function test_order_status_can_be_set()
{
    $order = Order::factory()->create();

    $order->status = OrderStatus::Delivered;
    $order->save();

    $this->assertTrue($order->fresh()->status->equals(OrderStatus::Delivered));
}

For API responses that return enums as JSON:

public function test_api_returns_order_status()
{
    $order = Order::factory()->create(['status' => OrderStatus::Pending]);

    $this->getJson("/api/orders/{$order->id}")
         ->assertJson([
            'status' => OrderStatus::Pending->value,
         ]);
}

Real-World Case Study

Scenario: E-Commerce Order Workflow

  1. Enum Definitions
    Define OrderStatus and PaymentMethod enums under app/Enums.
  2. Model Casting
    Cast status and payment_method attributes in the Order model.
  3. Form Validation
    Use Illuminate\Validation\Rules\Enum in your order placement form request.
  4. Service Integration
    In the checkout service, call $paymentMethod->process($amount) on the enum instance.
  5. Notifications
    Use the enum’s label() method in email templates to display user-friendly text.
  6. Reporting
    Group orders by OrderStatus::cases() to generate order distribution charts.

This structured approach reduces bugs (no more typos in strings), centralizes logic, and drastically improves code readability.

Best Practices & Gotchas

  • Use Backed Enums when persisting to the database.
  • Never Use Magic Strings: Always refer to EnumType::Case instead of 'case'.
  • Be Mindful of Serialization: When converting models to JSON, enums serialize to their underlying values by default. If you need the case name, you can call $enum->name.
  • Avoid Overloading Enums: Don’t jam too much unrelated behavior into enums—keep them focused on representing discrete values and their immediate logic.
  • Consider Legacy Support: If supporting PHP versions before 8.1, use packages like MyCLabs/php-enum or BenSampo/laravel-enum.

Conclusion

Enums in Laravel unlock powerful type-safety and clarity benefits, transforming how you represent discrete value sets in your code. From simple status flags to complex behavior encapsulation, enums keep your codebase organized, predictable, and self-documenting. Embrace enums in your next Laravel project, and enjoy safer, more maintainable code.

#Laravel enums #PHP 8.1 enums #enum casting #Eloquent enum #backed enums #enum validation #Enum cases #Enum methods #Laravel best practices

Subscribe to our newsletter

12k+

Subscribers

Weekly

Frequency

Free

Always