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

State Machine Laravel Package

winzou/state-machine

Lightweight PHP state machine library. Define graphs with states, transitions, and guard/before/after callbacks, then apply and validate transitions on your domain objects. Supports multiple graphs per object and configurable state property paths.

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Setup

  1. Install via Composer:
    composer require winzou/state-machine
    
  2. Define a State Machine Graph (e.g., for an Order model):
    use Winzou\StateMachine\StateMachineFactory;
    
    $config = [
        'graph'         => 'orderWorkflow',
        'property_path' => 'status', // Eloquent attribute or property
        'states'        => ['draft', 'submitted', 'paid', 'shipped', 'cancelled'],
        'transitions'   => [
            'submit' => ['from' => 'draft', 'to' => 'submitted'],
            'pay'    => ['from' => 'submitted', 'to' => 'paid'],
            'ship'   => ['from' => 'paid', 'to' => 'shipped'],
            'cancel' => ['from' => ['submitted', 'paid'], 'to' => 'cancelled'],
        ],
        'callbacks'     => [
            'guard' => [
                'guard-pay' => [
                    'from' => 'submitted',
                    'do'   => fn(Order $order) => $order->payment->isSuccessful(),
                ],
            ],
            'after' => [
                'on-pay' => [
                    'on' => 'pay',
                    'do' => fn(Order $order) => $order->sendPaymentConfirmation(),
                ],
            ],
        ],
    ];
    
  3. Create a Factory Instance:
    $factory = new StateMachineFactory();
    $stateMachine = $factory->get($order, $config);
    
  4. Apply a Transition:
    $stateMachine->apply('pay'); // Throws \RuntimeException if invalid
    

First Use Case: Order Status Workflow

// In a Laravel controller:
public function pay(Order $order)
{
    $stateMachine = app('state_machine.factory')->get($order, 'orderWorkflow');
    $stateMachine->apply('pay'); // Transitions from 'submitted' to 'paid'
    return redirect()->route('order.show', $order);
}

Implementation Patterns

1. Laravel Service Container Integration

Bind the factory as a singleton in AppServiceProvider:

public function register()
{
    $this->app->singleton('state_machine.factory', function () {
        return new \Winzou\StateMachine\StateMachineFactory();
    });
}

Access via dependency injection:

public function __construct(private StateMachineFactory $factory) {}

2. Eloquent Model Integration

Store the state in a database column (e.g., status):

// Order.php
protected $casts = [
    'status' => 'string', // Stores 'draft', 'submitted', etc.
];

Define the graph in a trait or model method:

// Order.php
public function getStateMachineConfig()
{
    return [
        'graph' => 'orderWorkflow',
        'property_path' => 'status',
        // ... rest of config
    ];
}

3. Dynamic Graphs

Load configurations from a database or YAML:

// config/state_machines/order_workflow.php
return [
    'states' => ['draft', 'submitted', 'paid'],
    'transitions' => [
        'submit' => ['from' => 'draft', 'to' => 'submitted'],
    ],
];

Merge with runtime data:

$config = array_merge(
    require config_path('state_machines/order_workflow.php'),
    ['callbacks' => ['guard' => [...]]]
);

4. Guard Patterns

  • Authorization Guards:
    'guard' => [
        'guard-pay' => [
            'do' => fn(Order $order) => auth()->user()->can('pay-orders'),
        ],
    ],
    
  • Business Rule Guards:
    'guard' => [
        'guard-ship' => [
            'from' => 'paid',
            'do' => fn(Order $order) => $order->inventory->isAvailable(),
        ],
    ],
    

5. Callback Workflows

  • Pre-Transition Actions:
    'before' => [
        'validate-payment' => [
            'on' => 'pay',
            'do' => fn(Order $order) => $order->validatePayment(),
        ],
    ],
    
  • Post-Transition Actions:
    'after' => [
        'notify-shipper' => [
            'on' => 'ship',
            'do' => fn(Order $order) => $order->notifyShipper(),
        ],
    ],
    

6. Multi-Graph Objects

Attach multiple state machines to a single model (e.g., Order with payment and shipping workflows):

// Order.php
public function getPaymentGraphConfig() { /* ... */ }
public function getShippingGraphConfig() { /* ... */ }

Apply transitions separately:

$paymentMachine->apply('charge');
$shippingMachine->apply('schedule');

7. Testing Strategies

  • Mock the Factory:
    $factory = Mockery::mock(StateMachineFactory::class);
    $factory->shouldReceive('get')->andReturn($mockedMachine);
    
  • Test Guards:
    $this->expectException(RuntimeException::class);
    $stateMachine->apply('pay'); // Fails if guard returns false
    
  • Assert States:
    $this->assertEquals('paid', $order->status);
    

Gotchas and Tips

Common Pitfalls

  1. State Property Path Mismatch:

    • Ensure property_path in the config matches the exact attribute name (e.g., status vs. order_status).
    • Fix: Use getAttribute() in callbacks if the property is dynamic:
      'do' => fn(Order $order) => $order->getAttribute('status'),
      
  2. Guard Logic Errors:

    • Guards must return a boolean. Non-boolean returns (e.g., null, string) will throw exceptions.
    • Fix: Explicitly return true/false:
      'do' => fn() => $this->checkPermission() ?: false,
      
  3. Callback Context:

    • Callbacks receive the object as the first argument, not $this. Avoid use($this) in closures:
      // ❌ Wrong (captures outer scope)
      'do' => function() { $this->log(); } // Fails: $this is the object, not the class
      // ✅ Correct
      'do' => fn(Order $order) => $order->logTransition(),
      
  4. Transition Validation:

    • apply() throws \RuntimeException on failure. Use can() to check first:
      if ($stateMachine->can('pay')) {
          $stateMachine->apply('pay');
      }
      
  5. Multiple Graphs on Same Object:

    • Each graph must have a unique name (graph key in config). Reusing names will overwrite the state machine.
    • Fix: Use descriptive names (e.g., order_payment_workflow, order_shipping_workflow).
  6. Database Sync:

    • The package does not auto-save the state. Manually persist after transitions:
      $stateMachine->apply('pay');
      $order->save(); // Required!
      
  7. Legacy State Handling:

    • If an object’s state is not in the defined states, apply() will throw an exception. Handle this in guards or pre-transition callbacks:
      'guard' => [
          'validate-state' => [
              'do' => fn(Order $order) => in_array($order->status, $config['states']),
          ],
      ],
      

Debugging Tips

  1. Enable Callback Logging: Add debug output to callbacks to trace execution:

    'after' => [
        'debug-transition' => [
            'do' => fn($object, $transition) => logger()->debug("Transition: {$transition}"),
        ],
    ],
    
  2. Check Transition Paths: Use getAllowedTransitions() to list valid transitions from the current state:

    $allowed = $stateMachine->getAllowedTransitions();
    // Outputs: ['pay' => ['from' => 'submitted', 'to' => 'paid']]
    
  3. Validate Config: Use the validate() method to check the graph config:

    $factory->validate($config); // Throws \InvalidArgumentException on errors
    

Extension Points

  1. Custom Callback Handlers: Override the default CallbackHandler to add logging or metrics:
    use Winzou\StateMachine\CallbackHandler;
    
    class LoggingCallbackHandler extends CallbackHandler
    {
        public function execute($callback, $object, $transition)
        {
            logger()->info("Executing callback: {$
    
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.
emuniq/filament-browser-notifications
syriable/filament-translator
hungnm28/livewire-form
wenprise/eloquent
crudly/encrypted
fadion/bouncy
cuci/prototurk-sdk
gos/pubsub-router-bundle
cuci/prototurk-sdk-symfony
clementtalleu/easyadmin-markdown-bundle
codeflextech/permission-manager
karnoweb/livewire-datepicker
sayedenam/sayed-dashboard
milito/query-filter
apiboxsym/user-bundle
apiboxsym/health-check-bundle
jayeshmepani/jpl-moshier-ephemeris-php
elnasnato/laraliveui
labrodev/rest-sdk
sampaui/sampaui