Skip to content

Basic Usage Guide

Jean-Marc Strauven edited this page Aug 6, 2025 · 2 revisions

πŸš€ 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.

🎯 Core Concepts

State Machine Components

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

Model Integration

Laravel Statecraft integrates seamlessly with Eloquent models using the HasStateMachine trait.

πŸ“‹ Basic State Machine Structure

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

πŸ—οΈ Setting Up Your Model

1. Add the Trait

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',
    ];
}

2. Database Schema

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

πŸ”„ Working with States

Getting Current State

$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'

Checking State

// 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";
}

⚑ Performing Transitions

Basic Transitions

$order = Order::create([
    'amount' => 99.99,
    'customer_email' => '[email protected]'
]);

// Perform transition
$order->transitionTo('pay');
echo $order->getCurrentState(); // 'paid'

Checking Transition Availability

// 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;
}

Transition with Context

Pass additional data to guards and actions:

$order->transitionTo('pay', [
    'payment_method' => 'credit_card',
    'amount' => 99.99,
    'user_id' => auth()->id()
]);

πŸ›‘οΈ Basic Guards

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;
    }
}

βš™οΈ Basic Actions

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

πŸ“Š State History

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}";

🎯 Events

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.
    }
}

πŸ” Common Patterns

Conditional Transitions

// Multiple guards with logic operators
transitions:
  - name: approve
    from: pending
    to: approved
    guard:
      and:
        - IsManager
        - HasMinimumAmount
        - not: IsBlacklisted

Bulk State Operations

// Update multiple models
Order::whereIn('id', [1, 2, 3])
    ->get()
    ->each(function ($order) {
        if ($order->canTransitionTo('process')) {
            $order->transitionTo('process');
        }
    });

Error Handling

use Grazulex\LaravelStatecraft\Exceptions\TransitionNotAllowedException;

try {
    $order->transitionTo('ship');
} catch (TransitionNotAllowedException $e) {
    // Handle invalid transition
    logger("Cannot ship order {$order->id}: " . $e->getMessage());
}

🎨 Artisan Commands

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

πŸ§ͺ Testing Your State Machine

<?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'));
    }
}

πŸš€ What's Next?

Now that you understand the basics:

  1. Learn YAML Configuration - Master the complete YAML syntax
  2. Explore Guards & Actions - Build complex business logic
  3. Study Real Examples - See complete implementations
  4. Set up Events - Handle state change notifications

Ready for more advanced topics? Dive into our comprehensive examples or learn about Guards & Actions.

Clone this wiki locally