-
-
Notifications
You must be signed in to change notification settings - Fork 0
Basic Usage Guide
This guide covers the fundamental concepts and basic usage patterns of Laravel Statecraft. After reading this, you'll understand how to create and use state machines in your Laravel applications.
A state machine in Laravel Statecraft consists of four main components:
- States: The different conditions your model can be in
- Transitions: The allowed changes between states
- Guards: Conditions that must be met before a transition
- Actions: Side effects that happen during transitions
Laravel Statecraft integrates seamlessly with Eloquent models using the HasStateMachine
trait.
Here's a simple state machine for an order workflow:
# resources/state-machines/OrderStateMachine.yaml
name: OrderStateMachine
model: App\Models\Order
initial_state: pending
states:
- name: pending
description: Order is awaiting payment
- name: paid
description: Order has been paid
- name: processing
description: Order is being processed
- name: shipped
description: Order has been shipped
- name: delivered
description: Order has been delivered
- name: cancelled
description: Order was cancelled
transitions:
- name: pay
from: pending
to: paid
guard: PaymentGuard
action: ProcessPayment
- name: process
from: paid
to: processing
action: StartProcessing
- name: ship
from: processing
to: shipped
guard: InventoryGuard
action: CreateShipment
- name: deliver
from: shipped
to: delivered
action: NotifyDelivery
- name: cancel
from: [pending, paid, processing]
to: cancelled
action: RefundPayment
Add the HasStateMachine
trait to your Eloquent model:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Grazulex\LaravelStatecraft\Traits\HasStateMachine;
class Order extends Model
{
use HasStateMachine;
protected $stateMachine = 'OrderStateMachine';
protected $fillable = [
'amount',
'customer_email',
'status',
];
}
Ensure your model's table has a column for storing the current state:
Schema::create('orders', function (Blueprint $table) {
$table->id();
$table->decimal('amount', 10, 2);
$table->string('customer_email');
$table->string('status')->default('pending'); // State column
$table->timestamps();
});
$order = Order::find(1);
// Get current state name
$currentState = $order->getCurrentState();
echo $currentState; // 'pending'
// Get state object with metadata
$stateObject = $order->getStateObject();
echo $stateObject->getDescription(); // 'Order is awaiting payment'
// Check if model is in specific state
if ($order->isInState('pending')) {
echo "Order is waiting for payment";
}
// Check multiple states
if ($order->isInAnyState(['paid', 'processing'])) {
echo "Order is being handled";
}
$order = Order::create([
'amount' => 99.99,
'customer_email' => '[email protected]'
]);
// Perform transition
$order->transitionTo('pay');
echo $order->getCurrentState(); // 'paid'
// Check if transition is possible
if ($order->canTransitionTo('process')) {
$order->transitionTo('process');
} else {
echo "Cannot process this order yet";
}
// Get all available transitions
$availableTransitions = $order->getAvailableTransitions();
foreach ($availableTransitions as $transition) {
echo "Can transition to: " . $transition;
}
Pass additional data to guards and actions:
$order->transitionTo('pay', [
'payment_method' => 'credit_card',
'amount' => 99.99,
'user_id' => auth()->id()
]);
Guards are conditions that must be met before a transition can occur:
<?php
namespace App\StateMachine\Guards;
use Grazulex\LaravelStatecraft\Contracts\GuardContract;
class PaymentGuard implements GuardContract
{
public function handle($model, array $context = []): bool
{
// Check if payment amount is valid
$amount = $context['amount'] ?? $model->amount;
return $amount > 0 && $amount <= 10000;
}
}
Actions are side effects that occur during transitions:
<?php
namespace App\StateMachine\Actions;
use Grazulex\LaravelStatecraft\Contracts\ActionContract;
use Illuminate\Support\Facades\Mail;
class ProcessPayment implements ActionContract
{
public function handle($model, array $context = []): void
{
// Process the payment
$paymentMethod = $context['payment_method'] ?? 'default';
// Your payment processing logic here
// PaymentService::process($model, $paymentMethod);
// Send confirmation email
Mail::to($model->customer_email)->send(
new PaymentConfirmationMail($model)
);
// Update model attributes
$model->update([
'payment_method' => $paymentMethod,
'paid_at' => now()
]);
}
}
Track all state changes automatically:
// Get state history
$history = $order->getStateHistory();
foreach ($history as $record) {
echo "Changed from {$record->from_state} to {$record->to_state} at {$record->created_at}";
}
// Get latest state change
$lastChange = $order->getLastStateChange();
echo "Last changed to: {$lastChange->to_state}";
Laravel Statecraft fires events during transitions:
// Listen for state changes in EventServiceProvider
use Grazulex\LaravelStatecraft\Events\StateTransitioned;
protected $listen = [
StateTransitioned::class => [
\App\Listeners\OrderStateChangedListener::class,
],
];
Create a listener:
<?php
namespace App\Listeners;
use Grazulex\LaravelStatecraft\Events\StateTransitioned;
class OrderStateChangedListener
{
public function handle(StateTransitioned $event): void
{
$model = $event->model;
$fromState = $event->fromState;
$toState = $event->toState;
// Log state change
logger("Order {$model->id} changed from {$fromState} to {$toState}");
// Send notifications, update external systems, etc.
}
}
// Multiple guards with logic operators
transitions:
- name: approve
from: pending
to: approved
guard:
and:
- IsManager
- HasMinimumAmount
- not: IsBlacklisted
// Update multiple models
Order::whereIn('id', [1, 2, 3])
->get()
->each(function ($order) {
if ($order->canTransitionTo('process')) {
$order->transitionTo('process');
}
});
use Grazulex\LaravelStatecraft\Exceptions\TransitionNotAllowedException;
try {
$order->transitionTo('ship');
} catch (TransitionNotAllowedException $e) {
// Handle invalid transition
logger("Cannot ship order {$order->id}: " . $e->getMessage());
}
Generate components quickly:
# Create state machine
php artisan statecraft:make OrderStateMachine --model=Order
# Create guard
php artisan statecraft:make:guard PaymentGuard
# Create action
php artisan statecraft:make:action ProcessPayment
# Validate state machine
php artisan statecraft:validate OrderStateMachine
# List all state machines
php artisan statecraft:list
<?php
namespace Tests\Feature;
use App\Models\Order;
use Tests\TestCase;
class OrderStateMachineTest extends TestCase
{
public function test_order_starts_in_pending_state()
{
$order = Order::factory()->create();
$this->assertEquals('pending', $order->getCurrentState());
}
public function test_order_can_be_paid()
{
$order = Order::factory()->create();
$this->assertTrue($order->canTransitionTo('pay'));
$order->transitionTo('pay');
$this->assertEquals('paid', $order->getCurrentState());
}
public function test_pending_order_cannot_be_shipped()
{
$order = Order::factory()->create();
$this->assertFalse($order->canTransitionTo('ship'));
}
}
Now that you understand the basics:
- Learn YAML Configuration - Master the complete YAML syntax
- Explore Guards & Actions - Build complex business logic
- Study Real Examples - See complete implementations
- Set up Events - Handle state change notifications
Ready for more advanced topics? Dive into our comprehensive examples or learn about Guards & Actions.
π― Laravel Statecraft - Advanced State Machine Implementation for Laravel
Navigate: π Home | π¦ Installation | π Basic Guide | π YAML Config | π‘ Examples
Resources: π‘οΈ Guards & Actions | π― Events | π State History | π§ͺ Testing | π¨ Commands
π Community Resources:
Made with β€οΈ for the Laravel community β’ Contribute β’ License