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…