State Pattern
debt(d7/e7/b7/t5)
Closest to 'only careful code review or runtime testing' (d7). The detection_hints confirm automated detection is 'no', and while phpstan/psalm are listed as tools, they cannot automatically flag the anti-pattern (switch-on-status scattered across multiple methods) as a State pattern violation — only a careful code review recognising duplicated state-branching logic across the codebase will surface the debt.
Closest to 'cross-cutting refactor across the codebase' (e7). The quick_fix description requires creating a new interface plus one class per state, then replacing all switch-on-status branches spread across multiple methods and files. This is not a single-line patch or small local refactor — it is a structural change that touches every caller of state-dependent logic, making it a cross-cutting refactor rather than a simple parameterised fix.
Closest to 'strong gravitational pull' (b7). The applies_to scope spans web, cli, and queue-worker contexts, meaning the anti-pattern (string/int flag + scattered switch statements) or the correct pattern affects every subsystem that touches the stateful object. Every new feature or state change is shaped by whether the pattern was applied correctly — existing scattered conditionals impose a persistent productivity tax on all future work.
Closest to 'notable trap — a documented gotcha most devs eventually learn' (t5). The misconception field explicitly states that developers commonly believe a switch statement on a status field is equivalent to the State pattern. This is a well-known gotcha that most OOP developers encounter and learn over time, but it is not a catastrophic or contradictory behaviour — it is a recognisable design-level misunderstanding that competent developers typically correct once they understand the Open/Closed principle.
Also Known As
TL;DR
Explanation
The State pattern models an object whose behaviour changes based on its current state by delegating to a State object. Each concrete state implements the same interface, and the context simply forwards calls to the current state. Transitioning between states is handled by the state objects themselves or the context. This replaces sprawling if/switch chains on a status field with polymorphism, follows Open/Closed Principle (new states = new classes, not new branches), and localises state-specific behaviour. In PHP, common examples include order workflows, connection states, and document approval processes.
Common Misconception
Why It Matters
Common Mistakes
- Implementing state as a string or integer flag with switch statements — the anti-pattern the State pattern replaces.
- State classes with too much business logic — they should manage transitions and delegate business logic to the context.
- Not preventing invalid transitions — every state should only allow transitions to valid next states.
- Overusing State pattern for objects with 2-3 states where a simple boolean or enum suffices.
Code Examples
// Switch on status string — grows with every new state:
function handleOrder(Order $o): void {
switch ($o->status) {
case 'pending': /* ... */ break;
case 'paid': /* ... */ break;
case 'shipped': /* ... */ break;
// Each new status requires modifying this switch
}
}
interface OrderState {
public function pay(Order $order): void;
public function ship(Order $order): void;
public function cancel(Order $order): void;
}
class PendingState implements OrderState {
public function pay(Order $o): void { $o->setState(new PaidState()); }
public function ship(Order $o): void { throw new \LogicException('Pay first'); }
public function cancel(Order $o): void { $o->setState(new CancelledState()); }
}
class PaidState implements OrderState {
public function pay(Order $o): void { throw new \LogicException('Already paid'); }
public function ship(Order $o): void { $o->setState(new ShippedState()); }
public function cancel(Order $o): void { $o->setState(new RefundingState()); }
}