Currently Available: Need a skilled Software Developer for your next project?
Categories
Laravel PHP

PHP Enums in Laravel Applications

PHP 8.1 introduced enums as a native language feature, giving developers a new way to work with sets of related constants. Enums are an improvement over traditional approaches like class constants or arrays:

  • Ensuring only valid values can be used (type safety)
  • Providing helpful methods out of the box
  • Creating a clear, organized way to handle predefined options

In Laravel applications, enums are particularly useful for managing things like:

  • Status types (e.g., order statuses)
  • User roles
  • Configuration options
  • Any other fixed set of values your application needs

Basic Enum Structure

At its simplest, an enum is a special class that defines a set of named constants. Here's a basic example:

enum OrderStatus
{
    case PENDING;
    case PROCESSING;
    case COMPLETED;
    case CANCELLED;
}

This simple structure already gives you several advantages:

  • Your code editor can suggest valid options as you type
  • PHP checks for valid values automatically
  • You get useful built-in methods like cases()
  • It's impossible to create invalid status values by mistake

Here's how you might use it:

$status = OrderStatus::PENDING;

// The match expression ensures you handle all possible cases
$message = match ($status) {
    OrderStatus::PENDING => 'Order is waiting for processing',
    OrderStatus::PROCESSING => 'Order is being processed',
    OrderStatus::COMPLETED => 'Order has been completed',
    OrderStatus::CANCELLED => 'Order was cancelled',
};

Database Integration Example

If you're familiar with MySQL's ENUM type, you might be wondering how PHP enums work with databases. Unlike MySQL ENUMs, PHP enums are stored as regular strings or integers in your database. Here's a complete example:

// 1. First, define your enum
enum OrderStatus: string
{
    case PENDING = 'pending';
    case PROCESSING = 'processing';
    case COMPLETED = 'completed';
    case CANCELLED = 'cancelled';
}

// 2. Create your migration
class CreateOrdersTable extends Migration
{
    public function up(): void
    {
        Schema::create('orders', function (Blueprint $table) {
            $table->id();
            // Store the enum as a string column
            $table->string('status', 20);
            $table->timestamps();
        });
    }
}

// 3. Set up your model
class Order extends Model
{
    protected $fillable = ['status'];

    // This tells Laravel to automatically convert
    // between database strings and enum instances
    protected $casts = [
        'status' => OrderStatus::class
    ];
}

// 4. Now you can use it naturally in your code
$order = Order::create([
    'status' => OrderStatus::PENDING
]);

// Laravel handles the conversion automatically
$order->status = OrderStatus::PROCESSING;
$order->save();

// You can query using the enum directly
$pendingOrders = Order::where('status', OrderStatus::PENDING)->get();

The key differences from MySQL ENUMs are:

  • PHP enums are more flexible and can include methods and properties
  • The database column is a regular string or integer
  • Validation happens in your application code
  • You can easily change valid values without altering the database schema

Making Enums More Powerful

Basic enums are useful, but they really shine when you add methods and properties. Let's enhance our OrderStatus enum with some practical features:

enum OrderStatus: string
{
    case PENDING = 'pending';
    case PROCESSING = 'processing';
    case COMPLETED = 'completed';
    case CANCELLED = 'cancelled';

    // Get a user-friendly label
    public function label(): string
    {
        return match($this) {
            self::PENDING => 'Awaiting Processing',
            self::PROCESSING => 'In Progress',
            self::COMPLETED => 'Order Complete',
            self::CANCELLED => 'Order Cancelled',
        };
    }

    // Get a color for UI display
    public function color(): string
    {
        return match($this) {
            self::PENDING => 'yellow',
            self::PROCESSING => 'blue',
            self::COMPLETED => 'green',
            self::CANCELLED => 'red',
        };
    }

    // Business logic
    public function canCancel(): bool
    {
        return in_array($this, [self::PENDING, self::PROCESSING]);
    }

    // Static helper for forms
    public static function options(): array
    {
        return collect(self::cases())->mapWithKeys(
            fn (self $status) => [$status->value => $status->label()]
        )->all();
    }
}

Now you can use these methods in your views and controllers:

// In your Blade view
<span class="text-{{ $order->status->color() }}-600">
    {{ $order->status->label() }}
</span>

// In a dropdown
<select name="status">
    @foreach(OrderStatus::options() as $value => $label)
        <option value="{{ $value }}">{{ $label }}</option>
    @endforeach
</select>

// In your controller
if ($order->status->canCancel()) {
    $order->cancel();
}

Form Validation

Laravel makes it easy to validate enum values in your forms:

use Illuminate\Validation\Rules\Enum;

class UpdateOrderRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'status' => ['required', new Enum(OrderStatus::class)],

            // You can add custom rules too
            'status' => [
                'required',
                new Enum(OrderStatus::class),
                function ($attribute, $value, $fail) {
                    $newStatus = OrderStatus::from($value);
                    if (!$this->order->status->canTransitionTo($newStatus)) {
                        $fail("Cannot change status from {$this->order->status->value} to {$value}");
                    }
                }
            ]
        ];
    }
}

Advanced Usage: State Machines

Enums are perfect for implementing simple state machines. Here's an example that manages order processing:

enum OrderState: string
{
    case CART = 'cart';
    case CHECKOUT = 'checkout';
    case PAID = 'paid';
    case SHIPPED = 'shipped';

    // Define valid transitions
    public function canTransitionTo(self $newState): bool
    {
        return match($this) {
            self::CART => $newState === self::CHECKOUT,
            self::CHECKOUT => $newState === self::PAID,
            self::PAID => $newState === self::SHIPPED,
            self::SHIPPED => false, // End state
        };
    }

    // Get possible next states
    public function nextStates(): array
    {
        return match($this) {
            self::CART => [self::CHECKOUT],
            self::CHECKOUT => [self::PAID],
            self::PAID => [self::SHIPPED],
            self::SHIPPED => [],
        };
    }

    // Get action required for this state
    public function requiredAction(): string
    {
        return match($this) {
            self::CART => 'Complete your order',
            self::CHECKOUT => 'Process payment',
            self::PAID => 'Prepare for shipping',
            self::SHIPPED => 'No action needed',
        };
    }
}

// Use it in your Order model
class Order extends Model
{
    protected $casts = [
        'state' => OrderState::class
    ];

    public function transition(OrderState $newState): bool
    {
        if (!$this->state->canTransitionTo($newState)) {
            throw new InvalidStateTransitionException(
                "Cannot transition from {$this->state->value} to {$newState->value}"
            );
        }

        $this->state = $newState;
        return $this->save();
    }
}
What I'm building

Delegate tasks. Get software.

Give Vroni a GitHub issue, bug report, spec, or rough idea. It reads the repo, plans the change, writes code, runs checks, and works toward a review-ready pull request.

Take a look at vroni.com

Subscribe to my newsletter

Get new posts when I publish them.

I respect your privacy. Unsubscribe at any time.

Leave a Reply

Your email address will not be published. Required fields are marked *