A package to make use of the finite state pattern in eloquent Models.
The package stores all states in a database table, so all states changes and the corresponding times can be traced. Since states are mapped via a relation, no additional migrations need to be created when a new state is needed for a model.
Use states wherever possible! A state can be used instead of booleans like active
or timestamps like declined_at
or deleted_at
:
$product->state->is('active');
This way you also know when the change to active has taken place. Also your app becomes more scalable, you can simply add an additional state if needed.
- Install the package via composer:
composer require aw-studio/laravel-states
- Publish the required assets:
php artisan vendor:publish --tag="states:migrations"
- Run The Migrations
php artisan migrate
- Create A State:
class BookingState extends State
{
const PENDING = 'pending';
const FAILED = 'failed';
const SUCCESSFULL = 'successfull';
const INITIAL_STATE = self::PENDING;
const FINAL_STATES = [self::FAILED, self::SUCCESSFULL];
}
- Create the transitions class:
class BookingStateTransitions extends State
{
const PAYMENT_PAID = 'payment_paid';
const PAYMENT_FAILED = 'payment_failed';
}
- Define the allowed transitions:
class BookingState extends State
{
// ...
public static function config()
{
self::set(BookingStateTransition::PAYMENT_PAID)
->from(self::PENDING)
->to(self::SUCCESSFULL);
self::set(BookingStateTransition::PAYMENT_FAILED)
->from(self::PENDING)
->to(self::FAILED);
}
}
- Setup your Model:
use AwStudio\States\Contracts\Stateful;
use AwStudio\States\HasStates;
class Booking extends Model implements Stateful
{
use HasStates;
protected $states = [
'state' => BookingState::class,
'payment_state' => ...,
];
}
$booking->state->current(); // "pending"
(string) $booking->state; // "pending"
Determine if the current state is a given state:
if($booking->state->is(BookingState::PENDING)) {
//
}
Determine if the current state is any of a the given states:
$states = [
BookingState::PENDING,
BookingState::SUCCESSFULL
];
if($booking->state->isAnyOf($states)) {
//
}
Determine if the state has been the given state at any time:
if($booking->state->was(BookingState::PENDING)) {
//
}
Execute a state transition:
$booking->state->transition(BookingStateTransition::PAYMENT_PAID);
Prevent throwing an exception when the given transition is not allowed for the current state by setting fail to false
:
$booking->state->transition(BookingStateTransition::PAYMENT_PAID, fail: false);
Store additional information about the reason of a transition.
$booking->state->transition(BookingStateTransition::PAYMENT_PAID, reason: "Mollie API call failed.");
Determine wether the transition is allowed for the current state:
$booking->state->can(BookingStateTransition::PAYMENT_PAID);
Lock the current state for update at the start of a transaction so the state can not be modified by simultansiously requests until the transaction is finished:
DB::transaction(function() {
// Lock the current state for update:
$booking->state->lockForUpdate();
// ...
});
Reload the current state:
$booking->state->reload();
Eager load the current state:
Booking::withCurrentState();
Booking::withCurrentState('payment_state');
$booking->loadCurrentState();
$booking->loadCurrentState('payment_state');
Filter models that have or dont have a current state:
Booking::whereStateIs('payment_state', PaymentState::PAID);
Booking::orWhereStateIs('payment_state', PaymentState::PAID);
Booking::whereStateIsNot('payment_state', PaymentState::PAID);
Booking::orWhereStateIsNot('payment_state', PaymentState::PAID);
Booking::whereStateWas('payment_state', PaymentState::PAID);
Booking::whereStateWasNot('payment_state', PaymentState::PAID);
Receive state changes:
$booking->states()->get() // Get all states.
$booking->states('payment_state')->get() // Get all payment states.
Listen to state changes or transitions in your model observer:
class BookingObserver
{
public function stateSuccessfull(Booking $booking)
{
// Gets fired when booking state changed to successfull.
}
public function paymentStatePaid(Booking $booking)
{
// Gets fired when booking payment_state changed to paid.
}
public function stateTransitionPaymentPaid(Booking $booking)
{
// Gets fired when state transition payment_paid gets fired.
}
}
BookingState::whereCan(BookingStateTransition::PAYMENT_PAID); // Gets states where from where the given transition can be executed.
BookingState::canTransitionFrom('pending', 'cancel'); // Determines if the transition can be executed for the given state.