Skip to content

State Pattern

othercodes edited this page Mar 8, 2021 · 1 revision

IMPORTANT: This feature is heavily inspired in spatie/laravel-model-states

Simple implementation of State pattern.

Defining a state value in our aggregate is really easy we just need to create our Value object that defines our business rules of our "state", por example the PaymentStatus of an order:

class PaymentStatus extends State
{
    public const PENDING = 'pending';
    public const PAID = 'paid';
    public const CANCELLED = 'cancelled';

    protected function configure(): void
    {
        $this->defaultState(self::PENDING)
            ->allowTransition(self::PENDING, self::PAID)
            ->allowTransition(self::PENDING, self::CANCELLED);
    }
}

As we can se above the definition is really simple we only need to implement the configure() method, defining the default state, and the allowed state transitions using the defaultState() and allowTransition() methods.

Now we just need to use the transitionTo() method to change the state of our object:

$state = new PaymentState();
$state->value(); // pending
$state->transitionTo(PaymentState::PAID);
$state->value(); // paid

The PaymentState object is initialized in pending state as we define it in the configure() method. But, what happens if we try to transition to a non-allowed state?

$state = new PaymentState(PaymentState::PAID);
$state->transitionTo(PaymentState::PENDING); 

// TransitionNotAllowed exception will be thrown.

Additionally, we can add some behavior on our object by defining some methods:

class PaymentStatus extends State
{
    public const PENDING = 'pending';
    public const PAID = 'paid';
    public const CANCELLED = 'cancelled';

    private string $color;

    private int $stateChanges = 0;

    protected function configure(): void
    {
        $this->defaultState(self::PENDING)
            ->allowTransition(self::PENDING, self::PAID)
            ->allowTransition(self::PENDING, self::CANCELLED)
            ->allowTransition(self::CANCELLED, self::PENDING)
            ->allowTransition(self::PAID, self::CANCELLED);
    }

    protected function onPending(): void
    {
        $this->color = 'gray';
    }
    
    protected function onPaid(): void
    {
        $this->color = 'green';
    }
    
    protected function onCancelled(): void
    {
        $this->color = 'red';
    }
    
    protected function fromPaidToCancelled(): void
    {
        $this->stateChanges++;
    }
    
    protected function toPaid(): void
    {
        $this->stateChanges++;
    }
}
  • onPendig() this method configures the object for a specific state, so for example when the PaymentStatus is in paid state, the onPending() method will be executed.
  • toPaid() is executed when the transition is on going from any allowed state to paid, which means, the value of the object still the old one, in this case is pending. Once this method ends the value is updated to the new value paid.
  • fromPaidToCancelled() same as above but this method will only be executed when the state changes from paid to cancelled. The transition from pending to cancelled will not execute this method.

The way to define these methods is pretty simple:

  • on{status in camel case}.
  • to{status in camel case}.
  • from{stats in camel case}To{status in camel case}.

Finally, we can pass arbitrary arguments to the transitionTo() method as we can see at below:

class PaymentStatus extends State
{
    public const PENDING = 'pending';
    public const PAID = 'paid';
    public const CANCELLED = 'cancelled';

    protected function configure(): void
    {
        $this->defaultState(self::PENDING)
            ->allowTransition(self::PENDING, self::PAID)
            ->allowTransition(self::PENDING, self::CANCELLED);
    }
    
    //...

    protected function fromPendingToPaid($my_color): void
    {
        // do something with $my_color
    }
}

$state = new PaymentState(PaymentState::PENDING);
$state->transitionTo(PaymentState::PAID, 'my_color_value'); 
Clone this wiki locally