Profile    Mohammed Shiroz Status   Loading  
Logo
Share This
Back to blog
Filter by:
Tags
//Article title

PHP Enums in Laravel: Replacing Magic Strings for Good

About Post

Somewhere in your codebase there's a line like if ($contract->status === 'actve'). It has been there for months. It has never been true, nobody noticed, and no test, linter or type checker had any reason to complain. It's just a string.

That's the problem with magic strings. 'active', 'pending', 'terminated' are spread across controllers, Blade views, queries, validation rules and JavaScript, each one a typo away from a silent bug. PHP has had a proper fix since 8.1: enums. And Laravel supports them almost everywhere you'd want.

What magic strings cost you

Before the fix, the symptoms. If any of these sound familiar, enums will help:

  • Searching the project for 'pending' returns 40 results, and you're not sure which ones are contract statuses and which are payment statuses.
  • The list of allowed values lives in three places: a validation rule, a dropdown and a comment in the migration. They disagree.
  • The "nice" label for each status (Waiting for signature instead of pending_signature) is built with an if chain in a Blade file.
  • Adding a new status means hunting through the code and hoping you found every switch.

Step 1: a backed enum

A backed enum has a scalar value for each case, which is what you store in the database. Here's a contract status, with a label method attached:

enum ContractStatus: string
{
    case Draft = 'draft';
    case PendingSignature = 'pending_signature';
    case Active = 'active';
    case Terminated = 'terminated';

    public function label(): string
    {
        return match ($this) {
            self::Draft => 'Draft',
            self::PendingSignature => 'Waiting for signature',
            self::Active => 'Active',
            self::Terminated => 'Terminated',
        };
    }
}

Notice the match has no default. That's deliberate. If someone adds a new case and forgets the label, PHP throws an UnhandledMatchError the first time it's used, instead of quietly showing an empty string. Static analysers like PHPStan can flag it before that.

Step 2: cast it on the model

Tell Eloquent that the column is an enum, and you never touch the raw string again:

class Contract extends Model
{
    protected function casts(): array
    {
        return [
            'status' => ContractStatus::class,
        ];
    }
}

Now $contract->status is a ContractStatus object, not a string:

if ($contract->status === ContractStatus::Active) {
    // a typo here is a fatal error, not a silent false
}

echo $contract->status->label(); // "Active"

$contract->update(['status' => ContractStatus::Terminated]);

Contract::where('status', ContractStatus::PendingSignature)->get();

Laravel converts the enum to its value when saving and querying, and back to an enum when reading. Your IDE autocompletes the cases, and "find usages" on ContractStatus::PendingSignature finds exactly the right places.

Step 3: validate against the enum

The allowed values now come from one place. Validation should use it too:

use Illuminate\Validation\Rule;

$request->validate([
    'status' => ['required', Rule::enum(ContractStatus::class)],
]);

$status = $request->enum('status', ContractStatus::class); // ContractStatus or null

Add a case to the enum and the validation accepts it automatically. Remove one and the validation rejects it. No second list to forget.

Step 4: put behaviour where it belongs

This is the part people miss. Enums can have methods, constants and interfaces, which makes them a natural home for small rules about the value itself:

public function isEditable(): bool
{
    return $this === self::Draft;
}

public static function options(): array
{
    $options = [];
    foreach (self::cases() as $case) {
        $options[$case->value] = $case->label();
    }
    return $options;
}

Now the Blade template asks $contract->status->isEditable() instead of repeating === 'draft', and every dropdown in the app is built from ContractStatus::options(). You can add a color() method for badges in the same way. Keep it to rules about the status itself, though; anything that needs a database or a service belongs elsewhere.

The gotchas

  • from() vs tryFrom(). ContractStatus::from('nope') throws a ValueError. tryFrom('nope') returns null. Use tryFrom() for anything that came from outside your code.
  • Store it as a string column, not a MySQL ENUM. A plain string column with the PHP enum as the source of truth means adding a case is a code change, not a schema change.
  • The values are a contract with your data. Renaming a case (Active to Live) is free. Changing a value ('active' to 'live') means migrating every existing row. Pick values carefully and then leave them alone.
  • Enum cases can't be array keys. They're objects. Use $status->value as the key.
  • APIs and JavaScript still see strings. json_encode() outputs the backing value, so your React or React Native app receives "active". If you use TypeScript, mirror the values in a union type so the front end gets some of the same safety.

When not to use an enum: if the list of values is something an admin should be able to edit (property types, maintenance categories, document types), it's data, not code. Put it in a table. Enums are for values your code makes decisions about.

A bonus: enums in routes

Laravel can bind a route parameter straight to a backed enum. Type-hint it, and an invalid value returns a 404 before your controller even runs:

Route::get('/contracts/status/{status}', function (ContractStatus $status) {
    return Contract::where('status', $status)->paginate();
});

The migration path

You don't need to convert the whole app in one go. Pick the status column that causes the most confusion, create the enum with the exact values already in the database, add the cast, and fix what breaks. The type errors you get along the way are the bugs that were already hiding there.

Which magic string in your codebase would you turn into an enum first? Mine is always the status column.

Comments (0)
Leave your review

Thanks for your valuable comments. Your comments has been updated and appreciate your getting in touch...

01. About Shiroz

Mohammed Shiroz

Hi, I'm Mohammed Shiroz, a software engineer and AI enthusiast from Sri Lanka who turns ideas into intelligent, real-world solutions. With over 9 years of hands-on experience, I currently lead real estate ERP development at Kate Group, a...

03.My Projects

04. Categories

Ready To order Your Project ?

Get in Touch
Close