Skip to content
Muhammet Şafak
tr
Asked by: Mert Answered:

What do I gain and lose by moving my status class constants to backed enums?


Question

A legacy order module is littered with dozens of `const STATUS_PENDING = 1`, `const STATUS_PAID = 2` integer constants. They're scattered from controllers to services to event listeners, and status checks repeat as small `switch` blocks all over the place. I'm on PHP 8.2 and Laravel 11. I'm considering moving these constants to a backed enum with helper methods. What do I actually gain, what does it make harder, and how do I do it without breaking the existing integer values in the database?

Answer

Short answer: for a module you still actively touch, this migration is a clear win — but it isn’t free, and it shouldn’t be a big-bang rewrite.

Enums aren’t a silver bullet; if you go in without knowing what they change relative to constants, you’ll pay back the type safety you gained with errors that now blow up at the boundaries.

  1. You gain type safety and readability. An OrderStatus $status signature instead of int both states intent and lets PHPStan/Psalm catch an invalid value before the code runs; IDE autocomplete hands you every case. None of that is possible with raw constants.
  2. Behavior lives with the value. You can attach methods like ->label(), ->isFinal(), ->canTransitionTo() to the enum. Dozens of switch/match blocks scattered across the codebase collapse into one place — that’s the real maintenance win.
  3. A backed enum preserves DB and serialization compatibility. As long as enum OrderStatus: int carries the same integer values, neither the DB column nor the JSON output changes. That’s exactly what makes the migration safe; don’t try to flip the column to varchar.
  4. The cost is hydration at the boundaries. You must cast the raw int from the DB or request via OrderStatus::from(), and from() throws on an unknown value. A broken/legacy status that used to pass silently now explodes — use tryFrom() at untrusted boundaries and handle the null case.
  5. Enums aren’t extensible. You can’t add cases at runtime, there’s no inheritance, no dynamic instantiation. If you need tenant-specific dynamic statuses, an enum is the wrong tool; a lookup table fits better there.
  6. The migration must be incremental. Put the enum alongside the constants, keep the old constants as deprecated aliases for one release, and move call sites piece by piece. In Laravel, Eloquent’s native enum cast does most of the heavy lifting.
enum OrderStatus: int
{
    case Pending = 1;
    case Paid = 2;
    case Shipped = 3;
    case Cancelled = 9;

    public function isFinal(): bool
    {
        return in_array($this, [self::Shipped, self::Cancelled], true);
    }
}

// In the Eloquent model:
protected $casts = ['status' => OrderStatus::class];

Bottom line: personally I’d go incremental — a backed enum with the same integer values, tryFrom() at the boundaries, and the old constants kept as deprecated aliases for a release. I’d add the enum and wire up the Eloquent cast first, then migrate call sites one service per PR. The one real risk is “orphan” status values in legacy data; scan for them with a SELECT DISTINCT status before the migration and the transition won’t surprise you.

Related Reading

Tags: #php#enum#refactoring
Share:

Comments

Sign in with your GitHub account to join the discussion. Comments are stored in GitHub Discussions.

More Questions

All questions

Search the site

Start typing to search posts, projects and pages.

Esc to close Powered by Pagefind