Coding problem · Medium · Language Idioms

Order Status Enum with Transitions

Medium Language practicals Seen at Shopify, Booking.com, Mollie

Model order statuses with a backed enum OrderStatus: string with cases Pending = 'pending', Paid = 'paid', Shipped = 'shipped', Cancelled = 'cancelled', plus:

  • label(): string → "Pending", "Paid", "Shipped", "Cancelled";
  • canTransitionTo(self $next): bool — allowed moves: Pending → Paid or Cancelled; Paid → Shipped or Cancelled; Shipped and Cancelled are final.

And a function transition(OrderStatus $from, string $to): OrderStatus that converts $to with OrderStatus::from() (an unknown value throws ValueError) and throws DomainException if the move isn't allowed.

Hints

Hint 1

match ($this) { ... } returns a value per case and is exhaustive.

Hint 2

OrderStatus::from('x') throws ValueError; tryFrom returns null.

Write and run your solution

Loading the editor… (JavaScript is required to run code in your browser.)

Solution and explanation

Show the solution

Time O(1) · Space O(1)

Backed enums give you a closed set of values that serialize to strings (->value, from(), tryFrom()), and enum methods keep behaviour next to the data. match is exhaustive — adding a case without handling it raises an UnhandledMatchError, which is exactly what you want in a state machine. Enum cases are singletons, so compare them with === (and in_array(..., true)).

Reference solution

<?php

enum OrderStatus: string
{
    case Pending = 'pending';
    case Paid = 'paid';
    case Shipped = 'shipped';
    case Cancelled = 'cancelled';

    public function label(): string
    {
        return ucfirst($this->value);
    }

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

function transition(OrderStatus $from, string $to): OrderStatus
{
    $next = OrderStatus::from($to);
    if (!$from->canTransitionTo($next)) {
        throw new DomainException("Cannot move order from {$from->value} to {$next->value}");
    }
    return $next;
}

Common mistakes

  • Comparing statuses as strings throughout the codebase.
  • Using tryFrom and silently continuing with null.
  • Loose in_array without the strict flag.

Follow-up questions

  • Persist the enum with Doctrine/Eloquent casts.
  • Record transitions with timestamps for an audit trail.