PHP State Design Pattern Поведенческий паттерн

Паттерн Состояние (State) в PHP

Короткий скринкаст: паттерн «Состояние» (State) — поведение объекта меняется в зависимости от его текущего состояния. На примере автомата с шариками гамбола.

State Pattern — открыть страницу урока

Описание

Состояние позволяет вынести поведение, зависящее от текущего состояния объекта, в отдельные классы. Вместо длинных if/switch автомат с шариками делегирует все действия (insertCoin, ejectCoin, turnCrank, dispense) текущему объекту-состоянию, реализующему интерфейс State. Контекст GumballMachine хранит текущее состояние и умеет его переключать. Конкретные состояния (NoQuarterState, HasQuarterState, SoldState, SoldOutState) знают, в какое состояние нужно перейти при выполнении действия.

UML-диаграмма

classDiagram class State { <<interface>> +insertCoin(): void +ejectCoin(): void +turnCrank(): void +dispense(): void } class GumballMachine { -state: State -count: int +insertQuarter(): void +ejectQuarter(): void +turnCrank(): void +setState(state: State): void +getCount(): int +releaseBall(): void } class NoQuarterState { +insertCoin(): void +ejectCoin(): void +turnCrank(): void +dispense(): void } class HasQuarterState { +insertCoin(): void +ejectCoin(): void +turnCrank(): void +dispense(): void } class SoldState { +insertCoin(): void +ejectCoin(): void +turnCrank(): void +dispense(): void } class SoldOutState { +insertCoin(): void +ejectCoin(): void +turnCrank(): void +dispense(): void } State <|.. NoQuarterState State <|.. HasQuarterState State <|.. SoldState State <|.. SoldOutState GumballMachine *-- State NoQuarterState ..> GumballMachine HasQuarterState ..> GumballMachine SoldState ..> GumballMachine SoldOutState ..> GumballMachine

Код

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) определяет разрешённые действия над ней.