PHP / Magento Dev Blog

  • Publikacje
  • O autorze
  • Kontakt

State pattern – order state machine, serialisation, comparison with Strategy

by Henryk Tews / Tuesday, 13 June 2023 / Published in Wzorce projektowe

State is a behavioural pattern that allows an object to change its behaviour when its internal state changes. It looks like the object changed its class. The classic example is an order state machine – an order in “pending” status behaves differently from one in “processing”. I show an implementation in PHP and compare it with Strategy, which it superficially resembles.

Without the pattern – the growing switch

<?php

// Without State pattern - one class handling all states
class Order
{
    private string $state = 'pending';

    public function pay(): void
    {
        match($this->state) {
            'pending'    => $this->state = 'processing',
            'processing' => throw new \RuntimeException('Already paid'),
            'complete'   => throw new \RuntimeException('Already complete'),
            'cancelled'  => throw new \RuntimeException('Order cancelled'),
        };
    }

    public function ship(): void
    {
        match($this->state) {
            'processing' => $this->state = 'shipped',
            'pending'    => throw new \RuntimeException('Pay first'),
            // ... more cases
        };
    }
    // Adding a new state requires modifying every method - OCP violation
}

State pattern – each state is a class

<?php

declare(strict_types=1);

// State interface - all states implement the same operations
interface OrderStateInterface
{
    public function pay(Order $order): void;
    public function ship(Order $order): void;
    public function cancel(Order $order): void;
    public function getName(): string;
}

// Concrete states
class PendingState implements OrderStateInterface
{
    public function pay(Order $order): void
    {
        echo "Payment received. Moving to processing.\n";
        $order->setState(new ProcessingState());
    }

    public function ship(Order $order): void
    {
        throw new \RuntimeException('Cannot ship: payment required first.');
    }

    public function cancel(Order $order): void
    {
        echo "Order cancelled.\n";
        $order->setState(new CancelledState());
    }

    public function getName(): string { return 'pending'; }
}

class ProcessingState implements OrderStateInterface
{
    public function pay(Order $order): void
    {
        throw new \RuntimeException('Already paid.');
    }

    public function ship(Order $order): void
    {
        echo "Order shipped.\n";
        $order->setState(new ShippedState());
    }

    public function cancel(Order $order): void
    {
        echo "Order cancelled (refund will be processed).\n";
        $order->setState(new CancelledState());
    }

    public function getName(): string { return 'processing'; }
}

class ShippedState implements OrderStateInterface
{
    public function pay(Order $order): void
    {
        throw new \RuntimeException('Already paid and shipped.');
    }

    public function ship(Order $order): void
    {
        throw new \RuntimeException('Already shipped.');
    }

    public function cancel(Order $order): void
    {
        throw new \RuntimeException('Cannot cancel a shipped order. Request return instead.');
    }

    public function getName(): string { return 'shipped'; }
}

class CancelledState implements OrderStateInterface
{
    public function pay(Order $order): void  { throw new \RuntimeException('Order cancelled.'); }
    public function ship(Order $order): void { throw new \RuntimeException('Order cancelled.'); }
    public function cancel(Order $order): void { throw new \RuntimeException('Already cancelled.'); }
    public function getName(): string { return 'cancelled'; }
}

// Context - delegates operations to its current state
class Order
{
    private OrderStateInterface $state;
    private array $history = [];

    public function __construct(private int $id)
    {
        $this->state = new PendingState(); // initial state
    }

    public function setState(OrderStateInterface $state): void
    {
        $this->history[] = $this->state->getName();
        $this->state      = $state;
    }

    // Delegate to state - Order does not know what state does
    public function pay(): void    { $this->state->pay($this); }
    public function ship(): void   { $this->state->ship($this); }
    public function cancel(): void { $this->state->cancel($this); }

    public function getStatus(): string      { return $this->state->getName(); }
    public function getHistory(): array      { return $this->history; }
}

// Usage
$order = new Order(42);
echo $order->getStatus(); // pending

$order->pay();
echo $order->getStatus(); // processing

$order->ship();
echo $order->getStatus(); // shipped

// $order->cancel(); // RuntimeException: Cannot cancel a shipped order

State serialisation – persist to database

<?php

// Persist and restore the state from a string identifier
class OrderStateFactory
{
    private array $states = [
        'pending'    => PendingState::class,
        'processing' => ProcessingState::class,
        'shipped'    => ShippedState::class,
        'cancelled'  => CancelledState::class,
    ];

    public function create(string $name): OrderStateInterface
    {
        $class = $this->states[$name]
            ?? throw new \InvalidArgumentException("Unknown state: {$name}");

        return new $class();
    }
}

// In repository - restore order with correct state from DB
class OrderRepository
{
    public function getById(int $id): Order
    {
        $row   = $this->db->fetchOne('SELECT * FROM orders WHERE id = ?', [$id]);
        $order = new Order($id);
        $order->setState($this->stateFactory->create($row['status']));
        return $order;
    }

    public function save(Order $order): void
    {
        $this->db->update('orders', ['status' => $order->getStatus()], ['id' => $order->getId()]);
    }
}

State vs Strategy – the key distinction

Aspect State Strategy
Who changes the behaviour object? The state itself (or the context) The client / external code
States aware of each other? Yes – state transitions to other states No – strategies are independent
Lifecycle Context moves through states over time Algorithm selected once or occasionally
Example Order lifecycle: pending → processing → shipped Shipping cost: flat vs weight-based

Summary

State eliminates growing switch statements in objects that change behaviour over their lifecycle. Each state encapsulates what is allowed and what is not in that state, and handles its own transitions. In Magento 2 the order state machine is the canonical real-world example. The pattern makes invalid state transitions impossible by design rather than by guard clauses scattered across methods.

About Henryk Tews

What you can read next

Event Sourcing – Domain Events, Aggregate Root, Event Store, connecting with CQRS
Command and Chain of Responsibility in PHP – behavioural patterns
Observer and Strategy in PHP – behavioural patterns

© 2026 Created by

TOP
Zarządzaj zgodą
Aby zapewnić jak najlepsze wrażenia, korzystamy z technologii, takich jak pliki cookie, do przechowywania i/lub uzyskiwania dostępu do informacji o urządzeniu. Zgoda na te technologie pozwoli nam przetwarzać dane, takie jak zachowanie podczas przeglądania lub unikalne identyfikatory na tej stronie. Brak wyrażenia zgody lub wycofanie zgody może niekorzystnie wpłynąć na niektóre cechy i funkcje.
Funkcjonalne Always active
Przechowywanie lub dostęp do danych technicznych jest ściśle konieczny do uzasadnionego celu umożliwienia korzystania z konkretnej usługi wyraźnie żądanej przez subskrybenta lub użytkownika, lub wyłącznie w celu przeprowadzenia transmisji komunikatu przez sieć łączności elektronicznej.
Preferencje
Przechowywanie lub dostęp techniczny jest niezbędny do uzasadnionego celu przechowywania preferencji, o które nie prosi subskrybent lub użytkownik.
Statystyka
Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do celów statystycznych. Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do anonimowych celów statystycznych. Bez wezwania do sądu, dobrowolnego podporządkowania się dostawcy usług internetowych lub dodatkowych zapisów od strony trzeciej, informacje przechowywane lub pobierane wyłącznie w tym celu zwykle nie mogą być wykorzystywane do identyfikacji użytkownika.
Marketing
Przechowywanie lub dostęp techniczny jest wymagany do tworzenia profili użytkowników w celu wysyłania reklam lub śledzenia użytkownika na stronie internetowej lub na kilku stronach internetowych w podobnych celach marketingowych.
  • Manage options
  • Manage services
  • Manage {vendor_count} vendors
  • Read more about these purposes
Zobacz preferencje
  • {title}
  • {title}
  • {title}