
Состояние на PHP
Состояние — это поведенческий паттерн, позволяющий динамически изменять поведение объекта при смене его состояния.
Поведения, зависящие от состояния, переезжают в отдельные классы. Первоначальный класс хранит ссылку на один из таких объектов-состояний и делегирует ему работу.
Сложность:
Популярность:
Применимость: Паттерн Состояние иногда используют в PHP для превращения громоздких стейт-машин, построенных на операторах switch
, в объекты.
Признаки применения паттерна: Методы класса делегируют работу одному вложенному объекту.
Концептуальный пример
Этот пример показывает структуру паттерна Состояние, а именно — из каких классов он состоит, какие роли эти классы выполняют и как они взаимодействуют друг с другом.
После ознакомления со структурой, вам будет легче воспринимать второй пример, который рассматривает реальный случай использования паттерна в мире PHP.
index.php: Пример структуры паттерна
<?php
namespace RefactoringGuru\State\Conceptual;
/**
* Контекст определяет интерфейс, представляющий интерес для клиентов. Он также
* хранит ссылку на экземпляр подкласса Состояния, который отображает текущее
* состояние Контекста.
*/
class Context
{
/**
* @var State Ссылка на текущее состояние Контекста.
*/
private $state;
public function __construct(State $state)
{
$this->transitionTo($state);
}
/**
* Контекст позволяет изменять объект Состояния во время выполнения.
*/
public function transitionTo(State $state): void
{
echo "Context: Transition to " . get_class($state) . ".\n";
$this->state = $state;
$this->state->setContext($this);
}
/**
* Контекст делегирует часть своего поведения текущему объекту Состояния.
*/
public function request1(): void
{
$this->state->handle1();
}
public function request2(): void
{
$this->state->handle2();
}
}
/**
* Базовый класс Состояния объявляет методы, которые должны реализовать все
* Конкретные Состояния, а также предоставляет обратную ссылку на объект
* Контекст, связанный с Состоянием. Эта обратная ссылка может использоваться
* Состояниями для передачи Контекста другому Состоянию.
*/
abstract class State
{
/**
* @var Context
*/
protected $context;
public function setContext(Context $context)
{
$this->context = $context;
}
abstract public function handle1(): void;
abstract public function handle2(): void;
}
/**
* Конкретные Состояния реализуют различные модели поведения, связанные с
* состоянием Контекста.
*/
class ConcreteStateA extends State
{
public function handle1(): void
{
echo "ConcreteStateA handles request1.\n";
echo "ConcreteStateA wants to change the state of the context.\n";
$this->context->transitionTo(new ConcreteStateB());
}
public function handle2(): void
{
echo "ConcreteStateA handles request2.\n";
}
}
class ConcreteStateB extends State
{
public function handle1(): void
{
echo "ConcreteStateB handles request1.\n";
}
public function handle2(): void
{
echo "ConcreteStateB handles request2.\n";
echo "ConcreteStateB wants to change the state of the context.\n";
$this->context->transitionTo(new ConcreteStateA());
}
}
/**
* Клиентский код.
*/
$context = new Context(new ConcreteStateA());
$context->request1();
$context->request2();
Output.txt: Результат выполнения
Context: Transition to RefactoringGuru\State\Conceptual\ConcreteStateA.
ConcreteStateA handles request1.
ConcreteStateA wants to change the state of the context.
Context: Transition to RefactoringGuru\State\Conceptual\ConcreteStateB.
ConcreteStateB handles request2.
ConcreteStateB wants to change the state of the context.
Context: Transition to RefactoringGuru\State\Conceptual\ConcreteStateA.
Пример из реальной жизни
В этом примере паттерн Состояние используется для представления различных состояний Счёта. Этот подход позволяет реализовать различные проверки условий при переходе счёта из одного состояния в другое, а также инкапсулировать логику каждого состояния в отдельном классе.
index.php: Пример структуры паттерна
<?php
namespace RefactoringGuru\State\RealWorld;
/**
* State Design Pattern
*
* Intent: lets an object alter its behavior when its internal state changes. It
* appears as if the object changed its class.
*/
/**
* Invoice State Interface
*
* This interface defines the contract that all invoice states must implement.
* It represents the State interface in the State pattern, ensuring that all
* concrete states provide implementations for all possible events/transitions.
*
* The interface defines all possible events that can occur in the invoice
* lifecycle, regardless of whether a particular state can handle them. This
* approach ensures consistency across all states and makes the system more
* maintainable.
*/
interface InvoiceState
{
public function finalize(): void;
public function pay(): void;
public function cancel(): void;
public function void(): void;
public function getName(): string;
}
/**
* Abstract Base State class
*
* This abstract class implements the InvoiceState interface and provides
* default implementations for all state transition methods. The default
* behavior is to throw exceptions for invalid transitions, following the "fail-
* fast" principle.
*
* This approach allows concrete states to only override the methods for
* transitions they actually support, keeping the code clean and focused. Any
* attempt to perform an invalid transition will result in a clear exception
* rather than silent failure.
*
* The abstract class also maintains a reference to the context (Invoice)
* object, which is needed for performing state transitions.
*/
abstract class BaseInvoiceState implements InvoiceState
{
/**
* Reference to the context object (Invoice)
*
* Each state needs access to the context to perform state transitions.
* This creates a bidirectional relationship between the state and context.
*/
protected $invoice;
public function __construct(Invoice $invoice)
{
$this->invoice = $invoice;
}
/**
* Default implementation for finalize event
*
* By default, finalize is not allowed in most states. Only states that
* support this transition will override this method.
*
* @throws InvalidStateTransitionException
*/
public function finalize(): void
{
throw new InvalidStateTransitionException("Cannot finalize invoice in " . $this->getName() . " state");
}
/**
* Default implementation for pay event
*
* By default, payment is not allowed in most states. Only states that
* support this transition will override this method.
*
* @throws InvalidStateTransitionException
*/
public function pay(): void
{
throw new InvalidStateTransitionException("Cannot pay invoice in " . $this->getName() . " state");
}
/**
* Default implementation for cancel event
*
* By default, cancellation is not allowed in most states. Only states that
* support this transition will override this method.
*
* @throws InvalidStateTransitionException
*/
public function cancel(): void
{
throw new InvalidStateTransitionException("Cannot cancel invoice in " . $this->getName() . " state");
}
/**
* Default implementation for void event
*
* By default, voiding is not allowed in most states. Only states that
* support this transition will override this method.
*
* @throws InvalidStateTransitionException
*/
public function void(): void
{
throw new InvalidStateTransitionException("Cannot void invoice in " . $this->getName() . " state");
}
/**
* Abstract method to get the state name
*
* Each concrete state must implement this method to return its name. This
* is used for logging, debugging, and display purposes.
*
* @return string The name of the current state
*/
abstract public function getName(): string;
}
/**
* Each Concrete State corresponds to a specific state.
*
* This Concrete State Represents a draft invoice.
*
* This is the initial state of every invoice. In this state, the invoice is
* still being prepared and can only be finalized to move to the Open state. No
* other operations are allowed in this state.
*/
class DraftInvoiceState extends BaseInvoiceState
{
/**
* Handle finalize event
*
* This is the only valid transition from Draft state. When an invoice is
* finalized, it transitions to the Open state where it can be paid, voided,
* or cancelled.
*/
public function finalize(): void
{
echo "Invoice #{$this->invoice->getId()} finalized - changing from Draft to Open\n";
$this->invoice->setState(new OpenInvoiceState($this->invoice));
}
public function getName(): string
{
return 'draft';
}
}
/**
* This Concrete State Represents an open invoice.
*
* This state represents an invoice that has been finalized and is ready for
* processing. From this state, the invoice can be:
* - Paid (moves to Paid state)
* - Voided (moves to Void state)
* - Cancelled (moves to Uncollectable state)
*/
class OpenInvoiceState extends BaseInvoiceState
{
/**
* Handle pay event
*
* When payment is received, the invoice transitions to the Paid state. This
* is a terminal state - no further operations are allowed.
*/
public function pay(): void
{
echo "Invoice #{$this->invoice->getId()} paid - changing from Open to Paid\n";
$this->invoice->setState(new PaidInvoiceState($this->invoice));
}
/**
* Handle void event
*
* When an invoice is voided, it transitions to the Void state. This is a
* terminal state - no further operations are allowed.
*/
public function void(): void
{
echo "Invoice #{$this->invoice->getId()} voided - changing from Open to Void\n";
$this->invoice->setState(new VoidInvoiceState($this->invoice));
}
/**
* Handle cancel event
*
* When an invoice is cancelled, it transitions to the Uncollectable state.
* From Uncollectable, the invoice can still be paid or voided.
*/
public function cancel(): void
{
echo "Invoice #{$this->invoice->getId()} cancelled - changing from Open to Uncollectable\n";
$this->invoice->setState(new UncollectableInvoiceState($this->invoice));
}
public function getName(): string
{
return 'open';
}
}
/**
* This Concrete State Represents a paid invoice.
*
* This is a terminal state representing a paid invoice. Once an invoice is
* paid, no further state transitions are allowed. All event methods use the
* default implementation which throws exceptions.
*/
class PaidInvoiceState extends BaseInvoiceState
{
public function getName(): string
{
return 'paid';
}
}
/**
* This Concrete State Represents a void invoice.
*
* This is a terminal state representing a voided invoice. Once an invoice is
* voided, no further state transitions are allowed. All event methods use the
* default implementation which throws exceptions.
*/
class VoidInvoiceState extends BaseInvoiceState
{
public function getName(): string
{
return 'void';
}
}
/**
* This Concrete State Represents a collectable invoice.
*
* This state represents an invoice that has been cancelled but can still be
* recovered. From this state, the invoice can be:
* - Paid (moves to Paid state)
* - Voided (moves to Void state)
*
* This provides a way to handle invoices that were cancelled but later can be
* collected or definitively written off.
*/
class UncollectableInvoiceState extends BaseInvoiceState
{
/**
* Handle pay event
*
* Even though the invoice was cancelled, payment can still be received.
* This transitions the invoice to the Paid state.
*/
public function pay(): void
{
echo "Invoice #{$this->invoice->getId()} paid - changing from Uncollectable to Paid\n";
$this->invoice->setState(new PaidInvoiceState($this->invoice));
}
/**
* Handle void event
*
* If the invoice is definitively uncollectable, it can be voided. This
* transitions the invoice to the Void state.
*/
public function void(): void
{
echo "Invoice #{$this->invoice->getId()} voided - changing from Uncollectable to Void\n";
$this->invoice->setState(new VoidInvoiceState($this->invoice));
}
public function getName(): string
{
return 'uncollectable';
}
}
/**
* Context class - Invoice
*
* This is the context class in the State pattern. It maintains a reference to
* the current state object and delegates all state-specific behavior to the
* current state. The context is unaware of the specific state classes and
* interacts with them through the abstract InvoiceState interface.
*
* The context also maintains the invoice's data (id, amount, etc.) that remains
* constant regardless of the state.
*/
class Invoice
{
private $id;
private $amount;
/**
* Current state object
*
* This is the key component of the State pattern. The context maintains a
* reference to the current state object and delegates all state-specific
* operations to this object.
*
* @var InvoiceState
*/
private $state;
private $createdAt;
/**
* Constructor
*
* Creates a new invoice. The invoice always starts in the Draft state as
* per business requirements.
*/
public function __construct(int $id, float $amount)
{
$this->id = $id;
$this->amount = $amount;
$this->createdAt = new \DateTime();
// Initial state is draft This is where the State pattern begins - we
// set the initial state
$this->state = new DraftInvoiceState($this);
}
public function getId(): int
{
return $this->id;
}
/**
* Set the current state
*
* This method is called by state objects to transition to a new state. It's
* the mechanism that allows the State pattern to work - states can change
* the context's state by calling this method.
*
* @param InvoiceState $state The new state object
*/
public function setState(InvoiceState $state)
{
$this->state = $state;
}
/**
* Get the current state object
*
* @return InvoiceState
*/
public function getState(): InvoiceState
{
return $this->state;
}
/**
* Get the current state name
*
* This is a convenience method that delegates to the current state object.
*
* @return string
*/
public function getStateName(): string
{
return $this->state->getName();
}
/**
* Event method: finalize
*
* This method delegates the finalize operation to the current state. This
* is the core of the State pattern - the context doesn't know how to handle
* the operation, so it delegates to the current state.
*/
public function finalize()
{
$this->state->finalize();
}
/**
* Event method: pay
*
* This method delegates the pay operation to the current state. The
* behavior will vary depending on the current state.
*/
public function pay()
{
$this->state->pay();
}
/**
* Event method: cancel
*
* This method delegates the cancel operation to the current state. The
* behavior will vary depending on the current state.
*/
public function cancel()
{
$this->state->cancel();
}
/**
* Event method: void
*
* This method delegates the void operation to the current state. The
* behavior will vary depending on the current state.
*/
public function void()
{
$this->state->void();
}
/**
* Get invoice information
*
* Returns an array with all invoice information including current state.
* This is useful for debugging, logging, or API responses.
*
* @return array
*/
public function getInfo(): array
{
return [
'id' => $this->id,
'amount' => $this->amount,
'state' => $this->getStateName(),
'created_at' => $this->createdAt->format('Y-m-d H:i:s')
];
}
}
/**
* Custom exception for invalid state transitions
*
* This exception is thrown when an invalid state transition is attempted. It
* provides clear error messages about what transition was attempted and why it
* failed.
*/
class InvalidStateTransitionException extends \Exception
{
/**
* Constructor
*
* @param string $message Error message
* @param int $code Error code
* @param \Exception|null $previous Previous exception
*/
public function __construct($message = "", $code = 0, \Exception $previous = null)
{
parent::__construct($message, $code, $previous);
}
}
/**
* ============================================================================
* USAGE EXAMPLE AND DEMONSTRATION
* ============================================================================
*
* The following code demonstrates how to use the State pattern implementation
* with various scenarios that show all possible state transitions.
*/
try {
echo "=== Invoice State Pattern Demo ===\n\n";
// Create a new invoice (starts in draft state)
$invoice = new Invoice(1001, 1500.00);
echo "Created invoice: " . json_encode($invoice->getInfo()) . "\n\n";
// Scenario 1: Draft -> Open -> Paid
echo "--- Scenario 1: Draft -> Open -> Paid ---\n";
$invoice->finalize(); // Draft -> Open
echo "Current state: " . $invoice->getStateName() . "\n";
$invoice->pay(); // Open -> Paid
echo "Current state: " . $invoice->getStateName() . "\n";
// Try to pay again (should fail)
try {
$invoice->pay();
} catch (InvalidStateTransitionException $e) {
echo "Expected error: " . $e->getMessage() . "\n";
}
echo "\n--- Scenario 2: Draft -> Open -> Void ---\n";
$invoice2 = new Invoice(1002, 750.00);
$invoice2->finalize(); // Draft -> Open
$invoice2->void(); // Open -> Void
echo "Invoice 2 state: " . $invoice2->getStateName() . "\n";
echo "\n--- Scenario 3: Draft -> Open -> Uncollectable -> Paid ---\n";
$invoice3 = new Invoice(1003, 2000.00);
$invoice3->finalize(); // Draft -> Open
$invoice3->cancel(); // Open -> Uncollectable
echo "Invoice 3 state: " . $invoice3->getStateName() . "\n";
$invoice3->pay(); // Uncollectable -> Paid
echo "Invoice 3 final state: " . $invoice3->getStateName() . "\n";
echo "\n--- Scenario 4: Draft -> Open -> Uncollectable -> Void ---\n";
$invoice4 = new Invoice(1004, 500.00);
$invoice4->finalize(); // Draft -> Open
$invoice4->cancel(); // Open -> Uncollectable
$invoice4->void(); // Uncollectable -> Void
echo "Invoice 4 final state: " . $invoice4->getStateName() . "\n";
echo "\n--- Error Scenario: Invalid transition ---\n";
$invoice5 = new Invoice(1005, 300.00);
try {
$invoice5->pay(); // Try to pay draft invoice (should fail)
} catch (InvalidStateTransitionException $e) {
echo "Expected error: " . $e->getMessage() . "\n";
}
echo "\n--- State Information ---\n";
echo "Invoice 1: " . json_encode($invoice->getInfo()) . "\n";
echo "Invoice 2: " . json_encode($invoice2->getInfo()) . "\n";
echo "Invoice 3: " . json_encode($invoice3->getInfo()) . "\n";
echo "Invoice 4: " . json_encode($invoice4->getInfo()) . "\n";
echo "Invoice 5: " . json_encode($invoice5->getInfo()) . "\n";
} catch (InvalidStateTransitionException $e) {
echo "Error: " . $e->getMessage() . "\n";
}
Output.txt: Результат выполнения
=== Invoice State Pattern Demo ===
Created invoice: {"id":1001,"amount":1500,"state":"draft","created_at":"2025-07-12 13:14:15"}
--- Scenario 1: Draft -> Open -> Paid ---
Invoice #1001 finalized - changing from Draft to Open
Current state: open
Invoice #1001 paid - changing from Open to Paid
Current state: paid
Expected error: Cannot pay invoice in paid state
--- Scenario 2: Draft -> Open -> Void ---
Invoice #1002 finalized - changing from Draft to Open
Invoice #1002 voided - changing from Open to Void
Invoice 2 state: void
--- Scenario 3: Draft -> Open -> Uncollectable -> Paid ---
Invoice #1003 finalized - changing from Draft to Open
Invoice #1003 cancelled - changing from Open to Uncollectable
Invoice 3 state: uncollectable
Invoice #1003 paid - changing from Uncollectable to Paid
Invoice 3 final state: paid
--- Scenario 4: Draft -> Open -> Uncollectable -> Void ---
Invoice #1004 finalized - changing from Draft to Open
Invoice #1004 cancelled - changing from Open to Uncollectable
Invoice #1004 voided - changing from Uncollectable to Void
Invoice 4 final state: void
--- Error Scenario: Invalid transition ---
Expected error: Cannot pay invoice in draft state
--- State Information ---
Invoice 1: {"id":1001,"amount":1500,"state":"paid","created_at":"2025-07-12 13:14:15"}
Invoice 2: {"id":1002,"amount":750,"state":"void","created_at":"2025-07-12 13:14:15"}
Invoice 3: {"id":1003,"amount":2000,"state":"paid","created_at":"2025-07-12 13:14:15"}
Invoice 4: {"id":1004,"amount":500,"state":"void","created_at":"2025-07-12 13:14:15"}
Invoice 5: {"id":1005,"amount":300,"state":"draft","created_at":"2025-07-12 13:14:15"}