-
Notifications
You must be signed in to change notification settings - Fork 1
State Pattern
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 inpaid
state, theonPending()
method will be executed. -
toPaid()
is executed when the transition is on going from any allowed state topaid
, which means, the value of the object still the old one, in this case ispending
. Once this method ends the value is updated to the new valuepaid
. -
fromPaidToCancelled()
same as above but this method will only be executed when the state changes frompaid
tocancelled
. The transition frompending
tocancelled
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');