Weave Code
Code Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel State Machine Laravel Package

webrek/laravel-state-machine

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Setup

  1. Installation:

    composer require webrek/laravel-state-machine
    

    Publish the migration for transition history (if needed):

    php artisan vendor:publish --provider="Webrek\StateMachine\StateMachineServiceProvider" --tag="migrations"
    php artisan migrate
    
  2. Define a State Machine: Create a class extending Webrek\StateMachine\StateMachine (e.g., app/Models/OrderStatus.php):

    use Webrek\StateMachine\StateMachine;
    use Webrek\StateMachine\Transition;
    
    class OrderStatus extends StateMachine
    {
        public function states(): array { return ['pending', 'paid', 'shipped', 'cancelled']; }
    
        public function transitions(): array {
            return [
                'pay' => new Transition('pending', 'paid'),
                'ship' => new Transition('paid', 'shipped'),
                'cancel' => new Transition(['pending', 'paid'], 'cancelled'),
            ];
        }
    }
    
  3. Attach to Eloquent Model: Use the HasStateMachine trait in your model (e.g., app/Models/Order.php):

    use Webrek\StateMachine\HasStateMachine;
    
    class Order extends Model
    {
        use HasStateMachine;
    
        protected $stateMachine = OrderStatus::class;
        protected $stateColumn = 'status';
    }
    
  4. First Transition:

    $order = Order::find(1);
    $order->transition('pay'); // Enforces rules and updates state
    

Key Starting Points

  • Documentation: Check the README for advanced features like guards, events, and history.
  • Testing: Run php artisan test to verify the package works in your environment.
  • Debugging: Use dd($order->getStateMachine()->getErrors()) to inspect transition failures.

Implementation Patterns

Core Workflow

  1. Define States and Transitions:

    class OrderStatus extends StateMachine {
        public function states() { return ['draft', 'submitted', 'approved', 'rejected']; }
    
        public function transitions() {
            return [
                'submit' => new Transition('draft', 'submitted'),
                'approve' => new Transition('submitted', 'approved', [
                    'guard' => fn(Order $order) => $order->isValid(),
                ]),
            ];
        }
    }
    
    • Guards: Use closures or callables to validate transitions (e.g., guard in Transition).
    • Events: Bind events to transitions via onTransition:
      public function transitions() {
          return [
              'ship' => (new Transition('paid', 'shipped'))
                  ->onTransition(fn(Order $order) => event(new OrderShipped($order))),
          ];
      }
      
  2. Transition in Controllers:

    public function pay(Order $order) {
        $order->transition('pay');
        return redirect()->route('orders.show', $order);
    }
    
    • Error Handling: Wrap transitions in try-catch:
      try {
          $order->transition('pay');
      } catch (InvalidTransition $e) {
          return back()->withErrors($e->getMessage());
      }
      
  3. Querying by State:

    $pendingOrders = Order::whereState('pending')->get();
    

Integration Tips

  • APIs: Use transition() in API controllers with Problem responses for errors:
    try {
        $order->transition('cancel');
    } catch (InvalidTransition $e) {
        return response()->problem($e->getMessage(), status: 400);
    }
    
  • Commands: Schedule state changes with Laravel Jobs:
    $order->dispatchSync(fn() => $order->transition('ship'));
    
  • Testing: Use assertState() in PHPUnit:
    $order->transition('pay');
    $this->assertState($order, 'paid');
    

Advanced Patterns

  • Dynamic States: Override states() to fetch from a config or database.
  • State-Specific Logic: Use whenInState() in guards:
    'cancel' => new Transition(['paid', 'shipped'], 'cancelled')
        ->guard(fn(Order $order) => $order->whenInState('paid')->canCancel()),
    
  • History: Enable transition logging via the transition_history table:
    $order->transition('pay'); // Logs to DB automatically
    

Gotchas and Tips

Common Pitfalls

  1. Missing Migration:

    • Issue: Transition history won’t work if migrations aren’t published/ran.
    • Fix: Run php artisan vendor:publish --tag="migrations" and php artisan migrate.
  2. State Column Mismatch:

    • Issue: If $stateColumn isn’t set, the package defaults to state, which may conflict with existing columns.
    • Fix: Explicitly define protected $stateColumn = 'custom_status'; in your model.
  3. Guard Logic Errors:

    • Issue: Guards silently fail if not properly typed (e.g., returning false vs. throwing an exception).
    • Fix: Use throw new InvalidTransition() in guards for consistency:
      'guard' => fn(Order $order) => $order->isValid() ?: throw new InvalidTransition('Invalid order'),
      
  4. Circular Dependencies:

    • Issue: State machines with circular transitions (e.g., A → B → A) may cause infinite loops in guards.
    • Fix: Add explicit checks in guards or use allowSelfTransitions(false) in the StateMachine constructor.
  5. Event Binding:

    • Issue: Events fired during transitions may not have access to the full context.
    • Fix: Pass additional data via closures:
      ->onTransition(fn(Order $order) => event(new OrderEvent($order, ['reason' => 'manual'])))
      

Debugging Tips

  • Inspect State Machine:
    dd($order->getStateMachine()->getCurrentState());
    dd($order->getStateMachine()->getAllowedTransitions());
    
  • Log Transitions: Enable Laravel’s debug mode (APP_DEBUG=true) to see transition logs in the console.
  • Test Edge Cases:
    • Test transitions from all possible states (including invalid ones).
    • Verify guards with mock data:
      $order->shouldReceive('isValid')->andReturn(false);
      $this->expectException(InvalidTransition::class);
      $order->transition('approve');
      

Extension Points

  1. Custom Guard Classes: Create reusable guard logic:

    class ValidOrderGuard {
        public function __invoke(Order $order) {
            return $order->isValid();
        }
    }
    

    Use in transitions:

    'approve' => new Transition('submitted', 'approved', [
        'guard' => new ValidOrderGuard(),
    ]),
    
  2. State Machine Events: Extend the StateMachine class to dispatch custom events:

    class OrderStatus extends StateMachine {
        protected function onTransition(Order $order, string $transition) {
            event(new CustomOrderEvent($order, $transition));
        }
    }
    
  3. Database-Driven States: Dynamically load states from a states table:

    public function states(): array {
        return State::query()->pluck('name')->toArray();
    }
    
  4. Soft Transitions: For async workflows, use a queue job to handle transitions:

    $order->transitionSync('pay'); // Sync version for immediate feedback
    TransitionJob::dispatch($order, 'pay'); // Async version
    
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
codraw/graphviz
nexmo/api-specification
capell-app/block-library
axium/identity
cetria/laravel-dummy-models
cetria/reflection-helper
agropredict/sso-auth-bundle
evolvestudio/spam-protection
datacore/hub-sdk
develia/commons
cuci/prototurk-sdk
cuci/prototurk-sdk-symfony
develia/geo-bundle
dreamzy/livewire-charts
touchestate-sdk/php-sdk
22h/doctrine-garbage-collection-bundle
agtp/agtp-php
agtp/mod-php
splash/sonata-admin
splash/metadata