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.
- You gain type safety and readability. An
OrderStatus $statussignature instead ofintboth 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. - Behavior lives with the value. You can attach methods like
->label(),->isFinal(),->canTransitionTo()to the enum. Dozens ofswitch/matchblocks scattered across the codebase collapse into one place — that’s the real maintenance win. - A backed enum preserves DB and serialization compatibility. As long as
enum OrderStatus: intcarries 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 tovarchar. - The cost is hydration at the boundaries. You must cast the raw int from the DB or request via
OrderStatus::from(), andfrom()throws on an unknown value. A broken/legacy status that used to pass silently now explodes — usetryFrom()at untrusted boundaries and handle thenullcase. - 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.
- 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
Comments
Sign in with your GitHub account to join the discussion. Comments are stored in GitHub Discussions.