State pattern — object behaviour changes with state
Concept
State pattern allows an object to alter its behavior when its internal state changes. The object will appear to change its class. Instead of large if/switch blocks based on state, each state is a class that defines the object's behavior in that state.
The problem: An order has states: Pending, Processing, Shipped, Delivered, Cancelled. Different operations are valid in each state (you can't cancel a delivered order). Without State: if ($this->status === 'pending') { ... } elseif ($this->status === 'processing') { ... } in every method. With State: each state handles its allowed operations.
Structure:
- Context: The object whose behavior changes with state. Holds a reference to the current State. Delegates behavior to the State object.
- State Interface: Defines all possible operations. Each method corresponds to behavior that varies by state.
- Concrete States: Implement the State interface. Define behavior for their specific state.
State transitions: States can be responsible for transitioning to other states ($this->context->setState(new ShippedState())), or the Context can manage transitions.
State vs Strategy: Both use composition and an interface. State changes dynamically based on internal events. Strategy is set externally and doesn't change itself. State objects often hold a reference to the context (to trigger transitions). Strategy objects typically don't.
PHP real-world: Workflow engines, order management, payment processing states, user account states (active, suspended, banned).
Code Example
<?php
// State Interface
interface OrderState
{
public function confirm(Order $order): void;
public function ship(Order $order): void;
public function deliver(Order $order): void;
public function cancel(Order $order): void;
public function getLabel(): string;
}
// Concrete States
class PendingState implements OrderState
{
public function confirm(Order $order): void { $order->setState(new ProcessingState()); echo "Order confirmed!\n"; }
public function ship(Order $order): void { throw new \RuntimeException("Cannot ship unconfirmed order."); }
public function deliver(Order $order): void { throw new \RuntimeException("Cannot deliver unconfirmed order."); }
public function cancel(Order $order): void { $order->setState(new CancelledState()); echo "Order cancelled.\n"; }
public function getLabel(): string { return 'Pending'; }
}
class ProcessingState implements OrderState
{
public function confirm(Order $order): void { echo "Already confirmed.\n"; }
public function ship(Order $order): void { $order->setState(new ShippedState()); echo "Order shipped!\n"; }
public function deliver(Order $order): void { throw new \RuntimeException("Order not yet shipped."); }
public function cancel(Order $order): void { $order->setState(new CancelledState()); echo "Order cancelled during processing.\n"; }
public function getLabel(): string { return 'Processing'; }
}
class ShippedState implements OrderState
{
public function confirm(Order $order): void { echo "Already confirmed.\n"; }
public function ship(Order $order): void { echo "Already shipped.\n"; }
public function deliver(Order $order): void { $order->setState(new DeliveredState()); echo "Order delivered!\n"; }
public function cancel(Order $order): void { throw new \RuntimeException("Cannot cancel a shipped order. Contact support."); }
public function getLabel(): string { return 'Shipped'; }
}
class DeliveredState implements OrderState
{
public function confirm(Order $order): void { echo "Already delivered.\n"; }
public function ship(Order $order): void { echo "Already delivered.\n"; }
public function deliver(Order $order): void { echo "Already delivered.\n"; }
public function cancel(Order $order): void { throw new \RuntimeException("Cannot cancel delivered order. Initiate return."); }
public function getLabel(): string { return 'Delivered'; }
}
class CancelledState implements OrderState
{
public function confirm(Order $order): void { throw new \RuntimeException("Cannot confirm cancelled order."); }
public function ship(Order $order): void { throw new \RuntimeException("Cannot ship cancelled order."); }
public function deliver(Order $order): void { throw new \RuntimeException("Cannot deliver cancelled order."); }
public function cancel(Order $order): void { echo "Already cancelled.\n"; }
public function getLabel(): string { return 'Cancelled'; }
}
// Context
class Order
{
private OrderState $state;
public function __construct() { $this->state = new PendingState(); }
public function setState(OrderState $state): void { $this->state = $state; }
public function getStatus(): string { return $this->state->getLabel(); }
public function confirm(): void { $this->state->confirm($this); }
public function ship(): void { $this->state->ship($this); }
public function deliver(): void { $this->state->deliver($this); }
public function cancel(): void { $this->state->cancel($this); }
}
$order = new Order();
echo $order->getStatus(); // Pending
$order->confirm(); // Order confirmed!
echo $order->getStatus(); // Processing
$order->ship(); // Order shipped!
$order->cancel(); // RuntimeException: Cannot cancel a shipped order