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

> Add a backed enum on the same integer values, use tryFrom() at the boundaries, and keep the old constants as deprecated aliases while you move call sites.

- Asked: 2026-07-01
- Answered: 2026-07-03
- Asked by: Mert
- Tags: php, refactoring
- Source: https://muhammetsafak.com/just-ask/what-do-i-gain-and-lose-by-moving-my-status-class/
- Language: en-US
- Author: Muhammet Şafak

---
**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?


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.

## Short answer

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. I separated out which of constants, enums and readonly fits which job in [the eight-tool decision tree for PHP](/blog/php-constants-enums-readonly-decision-tree/); this answer is about what the "backed enum" branch there actually costs in practice.

## Why

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.
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.
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.

## What to do

1. **Put the enum next to the constants, on the same integer values.** Don't try to flip the column to `varchar`; move the behavior into the enum too. In Laravel, Eloquent's native enum cast does most of the heavy lifting.

   ```php
   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];
   ```

2. **Use `tryFrom()` at untrusted boundaries.** For values coming from a request or a legacy table, call `tryFrom()` instead of `from()` and handle the `null` case explicitly, so the transition doesn't produce a surprise exception.
3. **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.

**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

- [PHP Constants, Enums, and Readonly: One Decision Tree for 10 Tools](/blog/php-constants-enums-readonly-decision-tree/) — Blog
- [PHP 8.1 Is Coming: Safer Domain Models with Enums and Readonly Properties](/blog/php-8-1-safer-domain-models-with-enums-and-readonly/) — Blog
- [For concurrent HTTP calls, should I reach for raw Fibers, ReactPHP/AMPHP, or Swoole?](https://muhammetsafak.com/just-ask/concurrent-http-calls-raw-fibers-reactphp-amphp-swoole/) — Just Ask
- [How do I cleanly implement "withX" copy methods on readonly value objects?](https://muhammetsafak.com/just-ask/how-do-i-cleanly-implement-withx-copy-methods-on-readonly-value/) — Just Ask
- [Should I bind a Money value object to Eloquent with a custom cast or with accessors/mutators?](https://muhammetsafak.com/just-ask/should-i-bind-a-money-value-object-to-eloquent-with-a/) — Just Ask
