0

State pattern — object behaviour changes with state

Intermediate5 min read·eng-04-006
compare

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
<?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