What is the State pattern?
The State pattern lets an object change its behavior when its internal state changes - so it appears to switch class. Each state becomes its own object, and the behavior for that state lives there instead of in conditionals sprinkled everywhere.
The problem: conditionals on a status field
An order moves through a lifecycle - pending, paid, shipped, cancelled - and what each action does depends on where it is. The straightforward version checks the status in every method:
final class Order
{
public string $status = 'pending';
public function pay(): void
{
if ($this->status === 'pending') {
$this->status = 'paid';
} elseif ($this->status === 'cancelled') {
throw new LogicException("Cannot pay a cancelled order");
}
// ...
}
public function ship(): void
{
if ($this->status === 'paid') {
$this->status = 'shipped';
} elseif ($this->status === 'pending') {
throw new LogicException("Pay before shipping");
}
// the same status ladder, repeated in every method...
}
}
The rules for each state are smeared across every method. Adding a "refunded" state means
hunting down and editing every one of these if ladders.
The state version
Define a contract for what an order can do, then one class per state that answers for itself:
interface OrderState
{
public function pay(Order $order): void;
public function ship(Order $order): void;
}
final class Pending implements OrderState
{
public function pay(Order $order): void { $order->setState(new Paid()); }
public function ship(Order $order): void { throw new LogicException("Pay first"); }
}
final class Paid implements OrderState
{
public function pay(Order $order): void { throw new LogicException("Already paid"); }
public function ship(Order $order): void { $order->setState(new Shipped()); }
}
The order holds a state object and delegates to it - no conditionals:
final class Order
{
public function __construct(private OrderState $state = new Pending()) {}
public function setState(OrderState $state): void { $this->state = $state; }
public function pay(): void { $this->state->pay($this); }
public function ship(): void { $this->state->ship($this); }
}
$order = new Order();
$order->pay(); // Pending -> Paid
$order->ship(); // Paid -> Shipped
Each state knows its own allowed transitions and hands the order its next state. Adding a new state is a new class, not edits scattered across every method - the same Open/Closed win you saw with Strategy.
When to use it
Use State when an object's behavior depends on a status that changes over time, and you have the same status checks repeated across several methods. Order and payment lifecycles, document workflows (draft/review/published), and connection states (open/closed) fit well. If there are only two states and one method, a boolean is plenty - don't build a class hierarchy for a light switch.
One thing the toy example hides: real orders come back from a database as a status string,
so you need a single place that maps 'paid' to a Paid object when you load one. Keep that
mapping in one function. If it's scattered, a legacy or unknown status string fails in a
different, confusing way each place it's rebuilt - the one spot that can't map cleanly should
fail loudly, in one spot.
Common mistake
Putting the transition rules in the wrong place - having the Order decide which state
comes next with a switch, which just reintroduces the conditional you were removing. Let
each state own its transitions. The other trap is giving states access to too much of the
context; pass them only what they need to do their job.
FAQ
What is the difference between the state and strategy pattern?
The code shapes are twins - both delegate to an injected interface - but the intent is opposite. A strategy is chosen from outside and usually stays put; the object doesn't change it. A state changes itself: states typically decide the next state and swap it in, driving the object through a lifecycle. Strategy is about how; State is about where in the flow.
Where do the transitions belong - in the state or the context?
In the states, almost always. Each state knows which moves are legal from itself, so it's the natural owner of "what happens next". If the context decides, you're back to a central conditional and lose the pattern's benefit.
Isn't a database status column enough?
The column stores which state you're in; the pattern decides how each state behaves. You often keep both: persist a status string, and rebuild the matching state object when you load the order. The pattern replaces the behavior conditionals, not the stored value.