PHP State Design Pattern Behavioral Pattern

PHP State Pattern

A short screencast: the State pattern — an object's behavior changes depending on its current state. An example with a gumball vending machine.

State Pattern — open the lesson page

Overview

The State pattern lets you move behavior that depends on the object's current state into separate classes. Instead of large if/switch blocks, the gumball machine delegates all actions (insertCoin, ejectCoin, turnCrank, dispense) to the current state object that implements the State interface. The context GumballMachine holds the current state and can switch it. Concrete states (NoQuarterState, HasQuarterState, SoldState, SoldOutState) know which state to transition to when an action occurs.

UML diagram

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

Code

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();

Example output

After php demo.php the machine sells two gumballs and goes to the sold-out state:

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

Advantages

  • All code related to a specific state lives in a single class.
  • Adding new states is easy — just create a new State class.
  • The context is simpler as it doesn't contain a huge switch over states.
  • It makes state transitions clearer and easier to test.

When to use

  • When an object behaves differently depending on its internal state.
  • When there are many conditional statements checking the current state.
  • When states change often and transitions are well-defined.
  • When you want to localize logic for a particular state in one place.

Usage in Laravel

In Laravel the State pattern is used in status models and workflows. For example, in order management or billing packages, order states transition according to rules via dedicated handlers. Eloquent also has status concepts, and in business logic you can model transitions with state objects. Another example is job queues: the state of a job (pending, processing, failed, completed) determines which actions are allowed.