PHP / Magento Dev Blog

  • Publikacje
  • O autorze
  • Kontakt

Wzorzec State – maszyna stanów dla zamówienia, serializacja, porównanie ze Strategy

by Henryk Tews / wtorek, 13 czerwca 2023 / Opublikowano w Wzorce projektowe

Wzorzec State pozwala obiektowi zmieniać swoje zachowanie gdy zmienia się jego wewnętrzny stan. Z zewnątrz wygląda jakby obiekt zmienił klasę. Jeśli masz klasę z rosnącym if-else który sprawdza „w jakim stanie jestem przed każdą operacją” – to klasyczny kandydat na refaktoring do wzorca State. Implementuję maszynę stanów dla zamówienia e-commerce.

Problem bez wzorca State

<?php

// Klasa Order bez wzorca State - if-else rośnie z każdym nowym stanem
class Order
{
    private string $status = 'pending';

    public function process(): void
    {
        if ($this->status === 'pending') {
            $this->status = 'processing';
            echo "Zamówienie w realizacji\n";
        } elseif ($this->status === 'processing') {
            throw new \LogicException('Nie można przetworzyć zamówienia w realizacji');
        } elseif ($this->status === 'shipped') {
            throw new \LogicException('Nie można przetworzyć wysłanego zamówienia');
        }
        // ... dla każdego nowego stanu musisz modyfikować tę metodę
    }

    public function ship(): void
    {
        if ($this->status === 'pending') {
            throw new \LogicException('Nie można wysłać niepotwierdzonego zamówienia');
        } elseif ($this->status === 'processing') {
            $this->status = 'shipped';
            echo "Zamówienie wysłane\n";
        } elseif ($this->status === 'shipped') {
            throw new \LogicException('Zamówienie już wysłane');
        }
        // ... kolejne powielone if-else
    }

    public function cancel(): void
    {
        if ($this->status === 'shipped') {
            throw new \LogicException('Nie można anulować wysłanego zamówienia');
        }
        $this->status = 'cancelled';
    }
}
// Problemy: naruszenie OCP, duplikacja logiki, trudne testowanie

Implementacja wzorca State

<?php

declare(strict_types=1);

// Interfejs stanu - definiuje wszystkie możliwe akcje
interface OrderStateInterface
{
    public function process(Order $order): void;
    public function ship(Order $order): void;
    public function deliver(Order $order): void;
    public function cancel(Order $order): void;
    public function refund(Order $order): void;
    public function getName(): string;
}

// Abstrakcyjny stan bazowy - domyślnie rzuca wyjątek
// Konkretne stany nadpisują tylko akcje które obsługują
abstract class AbstractOrderState implements OrderStateInterface
{
    public function process(Order $order): void
    {
        throw new \LogicException(
            "Nie można przetworzyć zamówienia w stanie: {$this->getName()}"
        );
    }

    public function ship(Order $order): void
    {
        throw new \LogicException(
            "Nie można wysłać zamówienia w stanie: {$this->getName()}"
        );
    }

    public function deliver(Order $order): void
    {
        throw new \LogicException(
            "Nie można potwierdzić dostarczenia w stanie: {$this->getName()}"
        );
    }

    public function cancel(Order $order): void
    {
        throw new \LogicException(
            "Nie można anulować zamówienia w stanie: {$this->getName()}"
        );
    }

    public function refund(Order $order): void
    {
        throw new \LogicException(
            "Nie można zwrócić zamówienia w stanie: {$this->getName()}"
        );
    }
}
<?php

declare(strict_types=1);

// Konkretne stany - każdy obsługuje tylko swoje możliwe przejścia
class PendingState extends AbstractOrderState
{
    public function getName(): string { return 'pending'; }

    public function process(Order $order): void
    {
        echo "Zamówienie #{$order->getId()} przekazane do realizacji\n";
        $order->setState(new ProcessingState());
    }

    public function cancel(Order $order): void
    {
        echo "Zamówienie #{$order->getId()} anulowane przed realizacją\n";
        $order->setState(new CancelledState());
    }
}

class ProcessingState extends AbstractOrderState
{
    public function getName(): string { return 'processing'; }

    public function ship(Order $order): void
    {
        echo "Zamówienie #{$order->getId()} wysłane do klienta\n";
        $order->setState(new ShippedState());
    }

    public function cancel(Order $order): void
    {
        echo "Zamówienie #{$order->getId()} anulowane w trakcie realizacji\n";
        $order->setState(new CancelledState());
    }
}

class ShippedState extends AbstractOrderState
{
    public function getName(): string { return 'shipped'; }

    public function deliver(Order $order): void
    {
        echo "Zamówienie #{$order->getId()} dostarczone\n";
        $order->setState(new DeliveredState());
    }
}

class DeliveredState extends AbstractOrderState
{
    public function getName(): string { return 'delivered'; }

    public function refund(Order $order): void
    {
        echo "Zwrot dla zamówienia #{$order->getId()} zainicjowany\n";
        $order->setState(new RefundedState());
    }
}

class CancelledState extends AbstractOrderState
{
    public function getName(): string { return 'cancelled'; }
    // Żadne przejścia nie są możliwe z tego stanu - wszystko rzuca wyjątek
}

class RefundedState extends AbstractOrderState
{
    public function getName(): string { return 'refunded'; }
    // Stan końcowy - brak dalszych przejść
}
<?php

declare(strict_types=1);

// Kontekst - klasa Order deleguje operacje do aktualnego stanu
class Order
{
    private OrderStateInterface $state;

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

    public function setState(OrderStateInterface $state): void
    {
        $previousState = $this->state->getName();
        $this->state   = $state;

        echo "Zmiana stanu: {$previousState} -> {$state->getName()}\n";
    }

    public function getState(): OrderStateInterface { return $this->state; }
    public function getStatus(): string             { return $this->state->getName(); }
    public function getId(): int                    { return $this->id; }

    // Delegacja do stanu - Order nie wie jakie przejścia są możliwe
    public function process(): void  { $this->state->process($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); }
    public function refund(): void   { $this->state->refund($this); }
}

// Użycie - szczęśliwa ścieżka
$order = new Order(1042);
echo "Stan: " . $order->getStatus() . "\n\n"; // pending

$order->process();  // pending -> processing
$order->ship();     // processing -> shipped
$order->deliver();  // shipped -> delivered
$order->refund();   // delivered -> refunded

echo "\nStan końcowy: " . $order->getStatus() . "\n";
<?php

// Niedozwolone przejście - stan rzuca wyjątek
$order2 = new Order(1043);
$order2->process();

try {
    $order2->ship();  // ok - processing -> shipped
    $order2->ship();  // błąd - shipped nie ma metody ship
} catch (\LogicException $e) {
    echo "Błąd: " . $e->getMessage() . "\n";
    // Błąd: Nie można wysłać zamówienia w stanie: shipped
}

Serializacja stanu – zapis do bazy danych

<?php

declare(strict_types=1);

// Fabryka stanów - odtwarzanie obiektu stanu z nazwy (np. przy ładowaniu z bazy)
class OrderStateFactory
{
    private array $states = [];

    public function __construct()
    {
        $this->states = [
            'pending'    => new PendingState(),
            'processing' => new ProcessingState(),
            'shipped'    => new ShippedState(),
            'delivered'  => new DeliveredState(),
            'cancelled'  => new CancelledState(),
            'refunded'   => new RefundedState(),
        ];
    }

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

// Repoztorium ładujące Order z bazy z właściwym stanem
class OrderRepository
{
    public function __construct(
        private \PDO $pdo,
        private OrderStateFactory $stateFactory
    ) {}

    public function getById(int $id): Order
    {
        $row = $this->pdo
            ->prepare('SELECT * FROM orders WHERE id = ?')
            ->execute([$id]);

        $order = new Order((int) $row['id']);
        // Przywróć stan z bazy - fabryka tworzy odpowiedni obiekt stanu
        $state = $this->stateFactory->create($row['status']);
        $order->setState($state);

        return $order;
    }
}

State vs Strategy – kiedy który?

Aspekt State Strategy
Zmiana zachowania Na podstawie wewnętrznego stanu obiektu Na podstawie zewnętrznej konfiguracji
Kto zmienia stan/strategię Sam obiekt (wewnętrznie) Klient (z zewnątrz)
Stany wiedzą o sobie Tak – PendingState wie że następny to ProcessingState Nie – strategie są niezależne
Typowe użycie Maszyna stanów, workflow zamówień, dokumentów Wymienne algorytmy sortowania, płatności, wysyłki

Podsumowanie

Wzorzec State eliminuje if-else spaghetti w klasach które mają logikę zależną od stanu wewnętrznego. Każdy stan to osobna klasa która wie jakie przejścia są dla niego możliwe i co zrobić gdy ktoś próbuje wykonać niedozwoloną operację. Dodanie nowego stanu to nowa klasa i zero zmian w istniejącym kodzie – czyste Open/Closed. W Magento 2 podobny mechanizm znajdziesz przy statusach zamówień – choć tam jest bardziej konfiguracyjny niż oparty na obiektach stanu.

About Henryk Tews

Co możesz przeczytać następne

Observer i Strategy w PHP – wzorce behawioralne
Wzorzec Memento – undo/redo, historia zmian cen, persystencja do bazy jako audit log
Wzorzec Decorator w PHP – kompozycja zamiast dziedziczenia, przykład z cache repository
  • Publikacje
  • O autorze
  • Kontakt

© 2026 Created by

GÓRA
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 Zawsze aktywne
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.
  • Zarządzaj opcjami
  • Zarządzaj serwisami
  • Zarządzaj {vendor_count} dostawcami
  • Przeczytaj więcej o tych celach
Zobacz preferencje
  • {title}
  • {title}
  • {title}