Паттерн Состояние (State) в PHP
Короткий скринкаст: паттерн «Состояние» (State) — поведение объекта меняется в зависимости от его текущего состояния. На примере автомата с шариками гамбола.
Описание
Состояние позволяет вынести поведение, зависящее от текущего состояния объекта, в отдельные классы. Вместо длинных if/switch автомат с шариками делегирует все действия (insertCoin, ejectCoin, turnCrank, dispense) текущему объекту-состоянию, реализующему интерфейс State. Контекст GumballMachine хранит текущее состояние и умеет его переключать. Конкретные состояния (NoQuarterState, HasQuarterState, SoldState, SoldOutState) знают, в какое состояние нужно перейти при выполнении действия.
UML-диаграмма
Код
State.php
interface State
{
public function insertCoin(): void;
public function ejectCoin(): void;
public function turnCrank(): void;
public function dispense(): void;
}
GumballMachine.php
class GumballMachine
{
private State $state;
private int $count;
public function __construct(int $numberGumballs)
{
$this->count = $numberGumballs;
if ($numberGumballs > 0) {
$this->state = new NoQuarterState($this);
} else {
$this->state = new SoldOutState($this);
}
}
public function insertQuarter(): void
{
$this->state->insertCoin();
}
public function ejectQuarter(): void
{
$this->state->ejectCoin();
}
public function turnCrank(): void
{
$this->state->turnCrank();
$this->state->dispense();
}
public function setState(State $state): void
{
$this->state = $state;
}
public function getCount(): int
{
return $this->count;
}
public function releaseBall(): void
{
if ($this->count > 0) {
echo "A gumball comes rolling out...\n";
$this->count--;
}
}
}
NoQuarterState.php
class NoQuarterState implements State
{
public function __construct(private readonly GumballMachine $machine) {}
public function insertCoin(): void
{
echo "You inserted a quarter\n";
$this->machine->setState(new HasQuarterState($this->machine));
}
public function ejectCoin(): void
{
echo "You haven't inserted a quarter\n";
}
public function turnCrank(): void
{
echo "You turned, but there's no quarter\n";
}
public function dispense(): void
{
echo "You need to pay first\n";
}
}
HasQuarterState.php
class HasQuarterState implements State
{
public function __construct(private readonly GumballMachine $machine) {}
public function insertCoin(): void
{
echo "You can't insert another quarter\n";
}
public function ejectCoin(): void
{
echo "Quarter returned\n";
$this->machine->setState(new NoQuarterState($this->machine));
}
public function turnCrank(): void
{
echo "You turned...\n";
$this->machine->setState(new SoldState($this->machine));
}
public function dispense(): void
{
echo "No gumball dispensed\n";
}
}
SoldState.php
class SoldState implements State
{
public function __construct(private readonly GumballMachine $machine) {}
public function insertCoin(): void
{
echo "Please wait, we're already giving you a gumball\n";
}
public function ejectCoin(): void
{
echo "You can't eject, you haven't inserted a quarter yet\n";
}
public function turnCrank(): void
{
echo "Turning twice doesn't get you another gumball\n";
}
public function dispense(): void
{
$this->machine->releaseBall();
if ($this->machine->getCount() > 0) {
$this->machine->setState(new NoQuarterState($this->machine));
} else {
echo "Oops, out of gumballs!\n";
$this->machine->setState(new SoldOutState($this->machine));
}
}
}
SoldOutState.php
class SoldOutState implements State
{
public function __construct(private readonly GumballMachine $machine) {}
public function insertCoin(): void
{
echo "You can't insert a quarter, the machine is sold out\n";
}
public function ejectCoin(): void
{
echo "You can't eject, you haven't inserted a quarter yet\n";
}
public function turnCrank(): void
{
echo "You turned, but there are no gumballs\n";
}
public function dispense(): void
{
echo "No gumball dispensed\n";
}
}
demo.php
require_once 'State.php';
require_once 'GumballMachine.php';
require_once 'NoQuarterState.php';
require_once 'HasQuarterState.php';
require_once 'SoldState.php';
require_once 'SoldOutState.php';
$machine = new GumballMachine(2);
$machine->insertQuarter();
$machine->turnCrank();
$machine->insertQuarter();
$machine->turnCrank();
$machine->insertQuarter();
$machine->turnCrank();
Пример результата
После php demo.php автомат продаёт два шарика и переходит в состояние «закончились шарики»:
You inserted a quarter
You turned...
A gumball comes rolling out...
You inserted a quarter
You turned...
A gumball comes rolling out...
Oops, out of gumballs!
You can't insert a quarter, the machine is sold out
You turned, but there are no gumballs
No gumball dispensed
Преимущества
- Весь код, связанный с конкретным состоянием, живёт в одном классе.
- Автомат поведения при добавлении новых состояний — просто создать новый класс
State. - Упрощается контекст: ему не нужно содержать гигантский
switchпо состояниям. - Облегчает понимание и тестирование переходов между состояниями.
Когда использовать
- Когда объект ведёт себя по-разному в зависимости от своего внутреннего состояния.
- Когда в коде много условных конструкций, проверяющих текущее состояние.
- Когда состояние часто меняется и переходы между ними чётко определены.
- Когда нужно локализовать логику конкретного состояния в одном месте.
Использование в Laravel
В Laravel паттерн Состояние используется в статусных моделях и рабочих процессах. Например, в пакетах для управления заказами или в биллинге состояния заказа переходят по правилам через отдельные обработчики. Также Eloquent имеет понятие статусов, а в бизнес-логике можно реализовать переходы через объекты состояний. Ещё один пример — очереди задач: состояние задачи (pending, processing, failed, completed) определяет разрешённые действия над ней.