iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

State

The State pattern represents an objects behaviour as a separate class per mode. Instead of `if (status === \"draft\")` scattered through methods, each state class implements the same interface and the host delegates to whichever state object is current. The benefit is that adding a new state means adding one file, not editing every if.

Order workflow with State classes

EXAMPLE
<?php
// ============================================================
// State interface: every concrete state implements these.
// ============================================================
interface OrderState {
    public function name(): string;
    public function pay(Order $o): OrderState;
    public function ship(Order $o): OrderState;
    public function cancel(Order $o): OrderState;
}

// ============================================================
// Concrete states — each handles only its own transitions.
// ============================================================
class NewOrder implements OrderState {
    public function name(): string { return 'new'; }
    public function pay(Order $o): OrderState   { $o->log('paid'); return new Paid(); }
    public function ship(Order $o): OrderState  { throw new LogicException('cannot ship unpaid order'); }
    public function cancel(Order $o): OrderState{ $o->log('cancelled'); return new Cancelled(); }
}

class Paid implements OrderState {
    public function name(): string { return 'paid'; }
    public function pay(Order $o): OrderState   { return $this; }   // already paid, no-op
    public function ship(Order $o): OrderState  { $o->log('shipped'); return new Shipped(); }
    public function cancel(Order $o): OrderState{ $o->log('refunded'); return new Refunded(); }
}

class Shipped implements OrderState {
    public function name(): string { return 'shipped'; }
    public function pay(Order $o): OrderState   { throw new LogicException('already paid'); }
    public function ship(Order $o): OrderState  { return $this; }   // idempotent
    public function cancel(Order $o): OrderState{ throw new LogicException('cannot cancel after shipping; start a return'); }
}

class Cancelled implements OrderState {
    public function name(): string { return 'cancelled'; }
    public function pay(Order $o): OrderState   { throw new LogicException('order cancelled'); }
    public function ship(Order $o): OrderState  { throw new LogicException('order cancelled'); }
    public function cancel(Order $o): OrderState{ return $this; }
}

class Refunded implements OrderState {
    public function name(): string { return 'refunded'; }
    public function pay(Order $o): OrderState   { throw new LogicException('refunded'); }
    public function ship(Order $o): OrderState  { throw new LogicException('refunded'); }
    public function cancel(Order $o): OrderState{ return $this; }
}

// ============================================================
// Host object — owns the current state and delegates
// ============================================================
class Order {
    private OrderState $state;
    private array $audit = [];
    public function __construct() { $this->state = new NewOrder(); }
    public function status(): string { return $this->state->name(); }
    public function pay():    void   { $this->state = $this->state->pay($this); }
    public function ship():   void   { $this->state = $this->state->ship($this); }
    public function cancel(): void   { $this->state = $this->state->cancel($this); }
    public function log(string $event): void { $this->audit[] = [date('c'), $event]; }
    public function audit(): array { return $this->audit; }
}

// ============================================================
// Usage
// ============================================================
$o = new Order();
$o->pay();
$o->ship();
echo $o->status(), "\n";          // shipped
try { $o->cancel(); } catch (LogicException $e) { echo $e->getMessage(), "\n"; }
print_r($o->audit());

Why it matters

Two flavours of state machine: explicit (State pattern as above) and table-driven (a map of (state, event) -> new state). State classes win when each state has its own behaviour beyond the transition; table-driven wins when behaviour is uniform and only the transition graph changes. Most order/booking flows are the former.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
// Object delegates behaviour to its current State.
class TrafficLight {
    constructor() { this.state = new RedLight(); }
    tick() { this.state = this.state.next(); }
}
Try it Yourself »

Discussion

Loading…