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
- Why Enums Matter
- PHP 8.1 Enums Recap
- Defining Enums in Laravel
- Using Enums in Eloquent Models
- Form Requests & Validation with Enums
- Enums in Blade Templates
- Advanced Enum Patterns
- Backed Enums
- Methods on Enums
- Traits for Shared Behavior
- Integrating Enums with Query Scopes
- Testing Enums
- Real-World Case Study
- Best Practices & Gotchas
- 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
- Enum Definitions
Define OrderStatus and PaymentMethod enums under app/Enums. - Model Casting
Cast status and payment_method attributes in the Order model. - Form Validation
Use Illuminate\Validation\Rules\Enum in your order placement form request. - Service Integration
In the checkout service, call $paymentMethod->process($amount) on the enum instance. - Notifications
Use the enum’s label() method in email templates to display user-friendly text. - 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.