PHP Enums: Replace Constants, Replace Config Arrays, Replace Magic Strings
PHP 8.1 Enums are more powerful than class constants. Learn backed enums, enum methods, interface implementation, and practical Laravel patterns.
SenpaiDev
Author
PHP 8.1 introduced native Enums and they're genuinely transformative. Not just a nicer way to write constants — Enums can have methods, implement interfaces, and be used directly in Laravel's type system, routes, and Eloquent models.
The Problem Enums Solve
Before Enums, you handled finite states with constants or string literals:
// The old way — fragile, no type safety
class Order {
const STATUS_PENDING = 'pending';
const STATUS_SHIPPED = 'shipped';
const STATUS_DELIVERED = 'delivered';
}
// This compiles and runs fine but is logically wrong
$order->status = 'delieverd'; // Typo — silent bug
Enums make invalid states unrepresentable:
enum OrderStatus: string {
case Pending = 'pending';
case Shipped = 'shipped';
case Delivered = 'delivered';
}
$order->status = OrderStatus::Delivered; // Type-safe — IDE autocomplete works
Backed Enums and Database Storage
Backed enums store a primitive value (string or int) that's ideal for database columns. In your Eloquent model, cast the attribute:
// In your model
protected $casts = [
'status' => OrderStatus::class,
];
// Now the property is typed automatically
$order = Order::find(1);
$order->status; // Returns OrderStatus::Pending, not the string 'pending'
$order->status === OrderStatus::Pending; // true
This eliminates the entire category of "is this string correct?" bugs without any extra validation code.
Enum Methods for Business Logic
Enums can have methods, which is where they really shine. Instead of scattered if ($status === 'pending') checks, put the logic directly on the Enum:
enum OrderStatus: string {
case Pending = 'pending';
case Shipped = 'shipped';
case Delivered = 'delivered';
case Cancelled = 'cancelled';
public function label(): string
{
return match($this) {
self::Pending => 'Awaiting Shipment',
self::Shipped => 'On Its Way',
self::Delivered => 'Delivered',
self::Cancelled => 'Cancelled',
};
}
public function color(): string
{
return match($this) {
self::Pending => 'yellow',
self::Shipped => 'blue',
self::Delivered => 'green',
self::Cancelled => 'red',
};
}
public function canTransitionTo(self $new): bool
{
return match($this) {
self::Pending => in_array($new, [self::Shipped, self::Cancelled]),
self::Shipped => $new === self::Delivered,
default => false,
};
}
}
Now your Blade templates read like plain English: {{ $order->status->label() }} and class="text-{{ $order->status->color() }}-600".
Enums in Laravel Route Model Binding
Laravel automatically resolves backed enums in route parameters:
// Route definition
Route::get('/orders/{status}', [OrderController::class, 'byStatus']);
// Controller — $status is automatically an OrderStatus enum
public function byStatus(OrderStatus $status): View
{
return view('orders.index', [
'orders' => Order::where('status', $status)->paginate(),
'statusLabel' => $status->label(),
]);
}
If the URL contains an invalid status value, Laravel returns a 404 automatically — no manual validation needed.
Enum Validation in Form Requests
Use the Rule::enum() validation rule to validate enum values in API requests:
use Illuminate\Validation\Rules\Enum;
public function rules(): array
{
return [
'status' => ['required', new Enum(OrderStatus::class)],
];
}
Adopt Enums everywhere you currently use string or integer constants. Your codebase becomes self-documenting, your IDE provides better completion, and an entire class of bugs simply disappears.
Laravel field notes
How To Apply This In A Real Laravel App
Use the article as a starting point, then validate the idea against the shape of your application. In Laravel projects, the safest pattern is to make the first version small, measurable, and easy to remove if the tradeoff is wrong.
Implementation approach
Start with one route, one controller or action, and one test that proves the expected behavior. Once the path is stable, extract shared code into a service class or action only if a second caller needs it.
For production work, keep config in environment variables, cache expensive reads, and add clear failure states. A feature that works locally but fails silently in a queue, scheduler, or cached config environment is not ready for users.
Review Checklist
- Add a feature or regression test before changing shared behavior.
- Run the route through production-like cache settings with config and route caching enabled.
- Check authorization, validation, and error responses before exposing the feature publicly.
- Document any non-obvious tradeoff in the code or article notes so future edits stay honest.
Written by
SenpaiDev
Developer and publisher at SenpaiDev, writing practical notes on Laravel, PHP, browser tools, and shipping better web products.
Comments (0)
Join the conversation
Log in to commentNo comments yet. Be the first to share your thoughts!