Skip to content
Carl Victor Fontanos
Carl Victor Fontanos

Carl Victor Fontanos

Software Engineer

I build web applications and share what I learn along the way.

© 2026

Backed Enums Are Your Status Fields' Best Friend

C
Carlo Fontanos
· 2 min read

Every app has status fields, and every status-as-string codebase has the same scars: a 'peding' typo that passed review, label translations duplicated in four templates, and an if-chain somewhere that silently ignores the status added last month. Enums fix all three - not by being fancy, but by giving the values a home.

enum OrderStatus: string
{
    case Pending  = 'pending';
    case Paid     = 'paid';
    case Shipped  = 'shipped';
    case Refunded = 'refunded';

    public function label(): string
    {
        return match ($this) {
            self::Pending  => 'Awaiting payment',
            self::Paid     => 'Paid',
            self::Shipped  => 'Shipped',
            self::Refunded => 'Refunded',
        };
    }

    public function isFinal(): bool
    {
        return match ($this) {
            self::Shipped, self::Refunded => true,
            default => false,
        };
    }

    public function canTransitionTo(self $next): bool
    {
        return match ($this) {
            self::Pending => in_array($next, [self::Paid, self::Refunded]),
            self::Paid    => in_array($next, [self::Shipped, self::Refunded]),
            default       => false,
        };
    }
}

The string backing (: string) is the database bridge: store $status->value, rehydrate with OrderStatus::from($row['status']) - or tryFrom(), which returns null instead of throwing for unknown values (the right choice when reading data older than your enum).

The exhaustiveness dividend

Because match without default throws on unhandled values, an enum-plus-match codebase has a superpower: add a case Disputed next quarter, and every match that forgot to handle it fails loudly at the exact line instead of falling through an else branch into wrong behavior. You want those errors - they're a to-do list generated by the type system. (Corollary: skip default arms in enum matches unless you truly mean "all others, forever".)

Patterns that round it out

// Dropdowns from cases() - no hardcoded option arrays
foreach (OrderStatus::cases() as $status) {
    echo "<option value=\"{$status->value}\">{$status->label()}</option>";
}

// Enums implement interfaces - polymorphism without classes
interface HasColor { public function color(): string; }
enum OrderStatus: string implements HasColor { /* ... */ }

// Constants and static factories live happily inside
enum Currency: string
{
    case USD = 'usd';
    public static function default(): self { return self::USD; }
}

Boundaries worth respecting

  • Enums are value definitions, not state machines with data - cases can't hold instance properties. The canTransitionTo() pattern above is the sweet spot; workflows with metadata per state deserve real objects.
  • Validation at the edges: everything arriving from $_POST or an API is a string until tryFrom() says otherwise. Enums make the conversion point explicit, which is the security posture you wanted anyway.

Migration is mechanical - constants become cases, string comparisons become identity checks (=== on cases works; they're singletons) - and each converted status field retires a small family of bugs.

C
Written by Carlo Fontanos

Full-stack web developer sharing practical tutorials and building tools that ship.

Got something on your mind?

My inbox is open - no forms disappearing into the void here.

  • Just say hello Found a tutorial useful? Spotted a mistake? Tell me.
  • Hire me for a project Have something custom in mind? Let's talk scope and timelines.
  • Product support Bought something here? I'll help you get it running.

I usually reply within 1-2 business days.

Message sent!

Your details are only used to reply to you.

Keep reading