BehavioralPHPverifiedVerified
State Pattern in PHP
Allows an object to alter its behavior when its internal state changes, appearing as if the object has changed its class.
How to Implement the State Pattern in PHP
1Step 1: Define the state interface
interface State
{
public function handle(Context $context): void;
public function getName(): string;
}2Step 2: Implement the context that delegates to the current state
class Context
{
private State $state;
public function __construct(State $initialState)
{
$this->state = $initialState;
}
public function setState(State $state): void
{
echo "Transitioning to: {$state->getName()}\n";
$this->state = $state;
}
public function getState(): State { return $this->state; }
public function request(): void
{
$this->state->handle($this);
}
}3Step 3: Implement concrete states with transitions
class IdleState implements State
{
public function handle(Context $context): void
{
echo "Idle: starting processing...\n";
$context->setState(new ProcessingState());
}
public function getName(): string { return 'Idle'; }
}
class ProcessingState implements State
{
public function handle(Context $context): void
{
echo "Processing: work complete\n";
$context->setState(new DoneState());
}
public function getName(): string { return 'Processing'; }
}
class DoneState implements State
{
public function handle(Context $context): void
{
echo "Done: resetting...\n";
$context->setState(new IdleState());
}
public function getName(): string { return 'Done'; }
}
// Usage
$context = new Context(new IdleState());
$context->request(); // Idle -> Processing
$context->request(); // Processing -> Done
$context->request(); // Done -> Idle<?php
declare(strict_types=1);
// [step] Define order states using an enum for type safety
enum OrderStatus: string
{
case Draft = 'draft';
case Pending = 'pending';
case Paid = 'paid';
case Shipped = 'shipped';
case Delivered = 'delivered';
case Cancelled = 'cancelled';
}
interface OrderStateInterface
{
public function pay(OrderContext $order): void;
public function ship(OrderContext $order): void;
public function deliver(OrderContext $order): void;
public function cancel(OrderContext $order): void;
public function getStatus(): OrderStatus;
}
interface LoggerInterface
{
public function info(string $message, array $context = []): void;
public function warning(string $message, array $context = []): void;
}
// [step] Implement the order context
final class OrderContext
{
private OrderStateInterface $state;
/** @var array<array{from: string, to: string, at: float}> */
private array $transitions = [];
public function __construct(
public readonly string $orderId,
private readonly LoggerInterface $logger,
) {
$this->state = new DraftState();
}
public function setState(OrderStateInterface $newState): void
{
$from = $this->state->getStatus()->value;
$to = $newState->getStatus()->value;
$this->transitions[] = ['from' => $from, 'to' => $to, 'at' => microtime(true)];
$this->logger->info("Order {$this->orderId}: {$from} -> {$to}");
$this->state = $newState;
}
public function getStatus(): OrderStatus { return $this->state->getStatus(); }
public function getTransitions(): array { return $this->transitions; }
public function pay(): void { $this->state->pay($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); }
}
// [step] Implement concrete states with valid transitions
final class DraftState implements OrderStateInterface
{
public function pay(OrderContext $order): void
{
$order->setState(new PaidState());
}
public function ship(OrderContext $order): void
{
throw new \LogicException('Cannot ship a draft order');
}
public function deliver(OrderContext $order): void
{
throw new \LogicException('Cannot deliver a draft order');
}
public function cancel(OrderContext $order): void
{
$order->setState(new CancelledState());
}
public function getStatus(): OrderStatus { return OrderStatus::Draft; }
}
final class PaidState implements OrderStateInterface
{
public function pay(OrderContext $order): void
{
throw new \LogicException('Order is already paid');
}
public function ship(OrderContext $order): void
{
$order->setState(new ShippedState());
}
public function deliver(OrderContext $order): void
{
throw new \LogicException('Cannot deliver before shipping');
}
public function cancel(OrderContext $order): void
{
// Refund logic would go here
$order->setState(new CancelledState());
}
public function getStatus(): OrderStatus { return OrderStatus::Paid; }
}
final class ShippedState implements OrderStateInterface
{
public function pay(OrderContext $order): void
{
throw new \LogicException('Order is already paid');
}
public function ship(OrderContext $order): void
{
throw new \LogicException('Order is already shipped');
}
public function deliver(OrderContext $order): void
{
$order->setState(new DeliveredState());
}
public function cancel(OrderContext $order): void
{
throw new \LogicException('Cannot cancel a shipped order');
}
public function getStatus(): OrderStatus { return OrderStatus::Shipped; }
}
final class DeliveredState implements OrderStateInterface
{
public function pay(OrderContext $order): void
{
throw new \LogicException('Order is already complete');
}
public function ship(OrderContext $order): void
{
throw new \LogicException('Order is already delivered');
}
public function deliver(OrderContext $order): void
{
throw new \LogicException('Order is already delivered');
}
public function cancel(OrderContext $order): void
{
throw new \LogicException('Cannot cancel a delivered order');
}
public function getStatus(): OrderStatus { return OrderStatus::Delivered; }
}
final class CancelledState implements OrderStateInterface
{
public function pay(OrderContext $order): void
{
throw new \LogicException('Cannot pay for a cancelled order');
}
public function ship(OrderContext $order): void
{
throw new \LogicException('Cannot ship a cancelled order');
}
public function deliver(OrderContext $order): void
{
throw new \LogicException('Cannot deliver a cancelled order');
}
public function cancel(OrderContext $order): void
{
throw new \LogicException('Order is already cancelled');
}
public function getStatus(): OrderStatus { return OrderStatus::Cancelled; }
}State Pattern Architecture
hourglass_empty
Rendering diagram...
lightbulb
State Pattern in the Real World
“Think of a traffic light. The light itself (context) doesn't change its wiring, but its active state—red, yellow, or green—completely determines what drivers should do. Each color has its own rules, and the light transitions through states on a timer. Adding a flashing-yellow state only requires defining that state's rules, not rewiring the entire light.”