hpwebdeveloper/laravel-stateflow

Laravel Model States with Context

Installs: 4

Dependents: 0

Suggesters: 0

Security: 0

Stars: 1

Watchers: 0

Forks: 0

Open Issues: 0

pkg:composer/hpwebdeveloper/laravel-stateflow

v1.0.0 2026-01-14 18:23 UTC

This package is auto-updated.

Last update: 2026-01-14 18:46:51 UTC


README

Image

A modern, enterprise-ready state machine implementation for Laravel Eloquent models.

Author: Hamed Panjeh

Laravel StateFlow is inspired by similar concepts found in Spatie Laravel Model States. It combines the state pattern with state machines to deliver enterprise-ready features: automatic state class discovery, automatic transition discovery, permissions, UI metadata, history tracking, and API resources. Laravel StateFlow maintains a single, unified topology of all possible states and transitions in your application's backing enum. This centralized architecture ensures that state definitions remain synchronized across your entire application, eliminating inconsistencies between backend logic and frontend representations. For large, complex systems, managing state changes and transitions is no longer cumbersome or bug-prone as your system growsβ€”a single enum serves as the definitive source of truth.

πŸ“¦ Demo Application: See Laravel StateFlow in action with a complete order management demo at laravel-stateflow-demo.

πŸ“š Table of Contents

Introduction

This package adds state support to your Eloquent models. It lets you represent each state as a separate class, handles serialization to the database behind the scenes, and provides a clean API for state transitions with full authorization and audit capabilities.

Example: Imagine an Order model with states: Pending, Processing, Shipped, Delivered, and Cancelled. Each state can have its own color for UI, permitted roles for authorization, and the transitions between them are explicit and validated.

// Check the current state
$order->state->name();        // 'pending'
$order->state->color();       // 'yellow'

// Get available transitions for the current user
$order->getNextStates();      // [Processing::class, Cancelled::class]

// Perform a transition with full audit trail
$order->transitionTo(Processing::class, reason: 'Order confirmed by warehouse');

Why Laravel StateFlow?

The Problem with Manual Transitions

Traditional state machine packages require you to manually decide the next state in every controller:

// Manual approach - YOU must decide the target state
$order->state->transitionTo(Processing::class);

❌ This creates several pain points:

  • In every controller, you must manually select the target state
  • If there are multiple choices, you need conditional statements
  • Transition logic becomes hidden in complex conditional statements across layers of code in actions/controllers
  • Allowed transitions are not centralized β€” scattered throughout the codebase
  • Difficult to audit which transitions are possible from any given state
  • Not maintainable in large projects with complex state flows
  • Views need logic to determine which transition buttons to show
  • Hard to test all possible state transition paths

Laravel-StateFlow's Solution: Centralized State Topology

Laravel StateFlow solves this with centralized workflow definition β€” see your entire state machine at a glance:

// πŸ“‹ app/Enums/BookingStateStatus.php β€” Complete topology in ONE place!
enum BookingStateStatus: string
{
    case Draft = 'draft';
    case Confirmed = 'confirmed';
    case Paid = 'paid';
    case Fulfilled = 'fulfilled';
    case Cancelled = 'cancelled';

    public function canTransitionTo(): array
    {
        return match ($this) {
            self::Draft     => [Confirmed::class],
            self::Confirmed => [Paid::class, Cancelled::class],
            self::Paid      => [Fulfilled::class, Cancelled::class],
            self::Fulfilled, self::Cancelled => [], // Final states
        };
    }
}
// 🎯 app/Models/Booking.php β€” Clean, 3-line configuration!
class Booking extends Model implements HasStatesContract
{
    use HasStates;

    public static function registerStates(): void
    {
        static::addState('state', StateConfig::make(BookingState::class)
            ->default(Draft::class)
            ->registerStates(BookingStateStatus::stateClasses())
            ->allowTransitionsFromArray(BookingStateStatus::transitions())
        );
    }
}

πŸ’‘ See it live: BookingStateStatus.php ・ Booking.php ・ Docs

Concrete Examples

1. Automatic Next State Discovery

// Get available transitions from current state
$order->getNextStates();           // [Processing::class, Cancelled::class]
$order->hasNextStates();           // true

2. Rich State Metadata

class Pending extends OrderState
{
    public const NAME = 'pending';

    public static function color(): string { return 'yellow'; }
    public static function title(): string { return 'Pending'; }
    public static function icon(): string { return 'clock'; }
}

3. Declarative Transitions with Attributes

#[AllowTransition(to: Processing::class)]
#[AllowTransition(to: Cancelled::class)]
class Pending extends OrderState {}

4. Multiple Permission Approaches

// Using attributes
#[StatePermission(roles: ['admin', 'warehouse'])]

// Using constants
public const PERMITTED_ROLES = ['admin', 'warehouse'];

// Using Laravel Policies
// Automatically checks OrderPolicy::transitionToProcessing()

5. Clean Eloquent Integration

// StateFlow provides automatic casting
$order->state;                    // State instance
$order->state->name();            // 'pending'
$order->state->color();           // 'yellow'
$order->getNextStates();          // Auto-discovered next states

Key Innovations

Laravel StateFlow provides enterprise features like automatic state discovery, rich UI metadata, built-in permissions, complete audit trails, and seamless Eloquent integration:

Feature βœ“ Description Example
Generate enum from states βœ… Create workflow enum from existing state classes php artisan stateflow:sync-enum ・ Docs
Automatic next states βœ… Discover available transitions from current state OrderController.php ・ Docs
UI metadata βœ… Colors, icons, titles for frontend integration Pending.php ・ Docs
Eloquent integration βœ… Cast-based approach with clean, Laravel-native syntax Order.php
Role-based permissions βœ… Control transitions by user roles Processing.php ・ Docs
Policy-based permissions βœ… Use Laravel policies for transition authorization OrderPolicy.php ・ Docs
State history & audit βœ… Complete transition history with performer tracking OrderController.php#show ・ Docs
API Resources βœ… Ready-to-use JSON responses for states Docs
Advanced query scopes βœ… orderByState, countByState, averageTimeInState Docs
Silent transitions βœ… Transition without firing events Docs
Force transitions βœ… Bypass validation for admin overrides Docs
Fluent transition API βœ… Clean, chainable API for transitions Docs
Centralized enum transitions βœ… Define state topology in a single enum for clarity BookingStateStatus.php ・ Docs

Installation

composer require hpwebdeveloper/laravel-stateflow

Publish the config (optional):

php artisan vendor:publish --tag="laravel-stateflow-config"

For history tracking, publish and run migrations:

php artisan vendor:publish --tag="laravel-stateflow-migrations"
php artisan migrate

Preparation in 4 simple Steps

StateFlow supports two approaches for defining your state machine:

Approach Best For Transitions Defined In
Traditional Self-contained states, IDE navigation State classes or model
Hybrid Enum Centralized workflow visualization Single enum file

Both approaches are demonstrated in the laravel-stateflow-demo: Orders (traditional) and Bookings (enum).

1. Add State Column to Your Model

Add a state column to the model that will have state transitions. For example, if you have an Order model:

// In a migration file
Schema::table('orders', function (Blueprint $table) {
    $table->string('state')->default('pending');
});

Note: Replace orders with your table name (e.g., posts, invoices, tickets).

2. Create State Classes

Generate all state classes at once using the --states option:

php artisan make:state OrderState --states=Pending,Processing,Shipped,Delivered,Cancelled

This single command creates the base class and all extending state classes:

app/States/
  β”œβ”€β”€ OrderState.php      # Abstract base class
  β”œβ”€β”€ Pending.php
  β”œβ”€β”€ Processing.php
  β”œβ”€β”€ Shipped.php
  β”œβ”€β”€ Delivered.php
  └── Cancelled.php

Alternative: You can also create states individually:

php artisan make:state OrderState --base
php artisan make:state Pending --extends=OrderState
php artisan make:state Processing --extends=OrderState
# ... and so on

πŸ’‘ See the demo: The laravel-stateflow-demo uses this structure β€” see States/Order/ for a complete example.

⚠️ Important: Keep all state classes in the same directory as their base state class. When adding new states later, use the full namespace:

php artisan make:state Processing --extends=App\\States\\Booking\\BookingState

The stateflow:sync-enum command only discovers states in the same directory as the base class.

3. Configure States

3.1 Traditional Approach β€” State Classes with Metadata

StateFlow supports multiple approaches for defining state metadata: Methods, Attributes, and Constants. The demo uses a combined approach β€” attributes for static metadata (title, description) and methods for dynamic values (color(), icon()).

// app/States/Order/Pending.php
use Hpwebdeveloper\LaravelStateflow\Attributes\DefaultState;
use Hpwebdeveloper\LaravelStateflow\Attributes\StateMetadata;
use Hpwebdeveloper\LaravelStateflow\Attributes\AllowTransition;

#[DefaultState]
#[StateMetadata(title: 'Pending', description: 'Order is pending confirmation')]
#[AllowTransition(to: Processing::class)]
#[AllowTransition(to: Cancelled::class)]
class Pending extends OrderState
{
    public const NAME = 'pending';

    public static function color(): string { return 'yellow'; }
    public static function icon(): string { return 'clock'; }
}

πŸ’‘ See the demo: Pending.php and States/Order/

3.2 Hybrid Enum Approach β€” Centralized Workflow Topology

For teams who prefer seeing the entire workflow at a glance, use an enum to define the transition topology. State classes still handle behavior (colors, icons, metadata).

# Create states with enum scaffold
php artisan make:state BookingState --states=Draft,Confirmed,Paid,Fulfilled,Cancelled,Expired --transitions=enum

The Enum β€” Shows all transitions in one place:

// app/Enums/BookingStateStatus.php
enum BookingStateStatus: string
{
    case Draft = 'draft';
    case Confirmed = 'confirmed';
    case Paid = 'paid';
    case Fulfilled = 'fulfilled';
    case Cancelled = 'cancelled';
    case Expired = 'expired';

    /**
     * πŸ“‹ Complete workflow topology at a glance!
     */
    public function canTransitionTo(): array
    {
        return match ($this) {
            self::Draft     => [Confirmed::class, Expired::class],
            self::Confirmed => [Paid::class, Cancelled::class, Expired::class],
            self::Paid      => [Fulfilled::class, Cancelled::class],
            // Final states β€” no transitions
            self::Fulfilled, self::Cancelled, self::Expired => [],
        };
    }

    public function stateClass(): string { /* maps to state class */ }
    public static function stateClasses(): array { /* all state classes */ }
    public static function transitions(): array { /* for StateConfig */ }
}

State classes remain simple (behavior only, no transitions):

// app/States/Booking/Draft.php
#[DefaultState]
#[StateMetadata(title: 'Draft', description: 'Booking in draft')]
class Draft extends BookingState
{
    public const NAME = 'draft';
    public static function color(): string { return 'gray'; }
}

πŸ’‘ See the demo: BookingStateStatus.php and States/Booking/

πŸ“š Learn more: See Defining States and Transitions for detailed comparison of all approaches.

4. Add to Model

4.1 Traditional Approach β€” Explicit Transitions

use Hpwebdeveloper\LaravelStateflow\HasStates;
use Hpwebdeveloper\LaravelStateflow\HasStatesContract;
use Hpwebdeveloper\LaravelStateflow\StateConfig;

class Order extends Model implements HasStatesContract
{
    use HasStates;

    public static function registerStates(): void
    {
        static::addState('state', StateConfig::make(OrderState::class)
            ->default(Pending::class)
            ->registerStates([
                Pending::class,
                Processing::class,
                Shipped::class,
                Delivered::class,
                Cancelled::class,
            ])
            ->allowTransition(Pending::class, Processing::class)
            ->allowTransition(Pending::class, Cancelled::class)
            ->allowTransition(Processing::class, Shipped::class)
            ->allowTransition(Processing::class, Cancelled::class)
            ->allowTransition(Shipped::class, Delivered::class)
        );
    }
}

πŸ’‘ See the demo: Order.php

4.2 Hybrid Enum Approach β€” Clean & Elegant ✨

With the enum approach, your model becomes remarkably clean:

use App\Enums\BookingStateStatus;
use App\States\Booking\{BookingState, Draft};

class Booking extends Model implements HasStatesContract
{
    use HasStates;

    public static function registerStates(): void
    {
        static::addState('state', StateConfig::make(BookingState::class)
            ->default(Draft::class)
            ->registerStates(BookingStateStatus::stateClasses())
            ->allowTransitionsFromArray(BookingStateStatus::transitions())
        );
    }
}

Benefits of the enum approach:

  • βœ… 3 lines instead of 10+ for state configuration
  • βœ… All transitions visible in one file (the enum)
  • βœ… Easy to generate workflow diagrams
  • βœ… Share workflows across multiple models

πŸ’‘ See the demo: Booking.php

πŸ“š Learn more: See Defining States and Transitions for all approaches.

Optional: Generate Enum & History Tracking

Generate enum from existing state classes:

php artisan stateflow:sync-enum App\\States\\Order\\OrderState
# Creates App\Enums\OrderStateStatus with all discovered states!

Naming Convention: By default, the command creates {BaseStateClass}Status (e.g., OrderState β†’ OrderStateStatus). Use --enum=App\Enums\YourCustomName to specify a different name.

⚠️ Directory Requirement: The sync command only discovers state classes in the same directory as the base state class. If you add new states later, ensure they are in the correct directory (e.g., app/States/Order/ for OrderState).

Enable history tracking: Add ->recordHistory() to your StateConfig and use the HasStateHistory trait. See History Tracking.

How to use it

Basic Usage

$order = Order::create(['customer_name' => 'John Doe']);

// Check current state
$order->state;                              // Pending instance
$order->state->name();                      // 'pending'
$order->isInState('pending');               // true

// Check allowed transitions
$order->canTransitionTo('processing');      // true
$order->canTransitionTo('shipped');         // false (must process first)
$order->getNextStates();                    // [Processing::class, Cancelled::class]

// Transition
$result = $order->transitionTo('processing');
$result->succeeded();                       // true
$order->state->name();                      // 'processing'

πŸ’‘ Full Example: See the laravel-stateflow-demo for a complete implementation β€” Order.php (model), OrderController.php (controller), and States/Order/ (state classes).

Serializing States

States are stored in the database using their NAME constant value:

// Creating with a state class
$order = Order::create([
    'state' => Processing::class,  // Stored as 'processing' in DB
]);

// The package handles serialization automatically
$order->state;              // Returns Processing instance
$order->state->name();      // 'processing'

Tip: You can use class names (e.g., Processing::class) throughout your code - the package handles mapping to/from database values.

Listing Registered States

// Get all states for the model (grouped by field)
Order::getStates();
// Returns: ['state' => ['pending', 'processing', 'shipped', 'delivered', 'cancelled']]

// Get states for a specific field
Order::getStatesFor('state');
// Returns: ['pending', 'processing', 'shipped', 'delivered', 'cancelled']

// Get default states
Order::getDefaultStates();
// Returns: ['state' => 'pending']

// Get default for specific field
Order::getDefaultStateFor('state');
// Returns: 'pending'

Retrieving Transitionable States

// Get state classes you can transition to from current state
$order->getNextStates();
// Returns: [Processing::class, Cancelled::class] (when in pending state)

// Count available transitions
count($order->getNextStates());
// Returns: 2

// Check if any transitions available
$order->hasNextStates();
// Returns: true

// Check specific transition
$order->canTransitionTo(Processing::class);
// Returns: true

Using States in Blade Templates

{{-- Display current state with color badge --}}
<span class="badge" style="background-color: {{ $order->state->color() }}">
    {{ $order->state->title() }}
</span>

{{-- Show available transition buttons --}}
@foreach($order->getNextStates() as $nextStateClass)
    <form action="{{ route('orders.transition', $order) }}" method="POST">
        @csrf
        <input type="hidden" name="state" value="{{ $nextStateClass::name() }}">
        <button type="submit" class="btn btn-{{ $nextStateClass::color() }}">
            <i class="{{ $nextStateClass::icon() }}"></i>
            {{ $nextStateClass::title() }}
        </button>
    </form>
@endforeach

Transitions

Basic Transition

$result = $order->transitionTo('processing');

if ($result->succeeded()) {
    // Transition completed
}

if ($result->failed()) {
    echo $result->error;  // Error message
}

With Metadata

$result = $order->transitionTo(
    state: Shipped::class,
    reason: 'Shipped via FedEx',
    metadata: ['tracking_number' => 'FX123456789']
);

Fluent API

$result = $order->transition()
    ->to(Shipped::class)
    ->reason('Order shipped')
    ->metadata(['carrier' => 'FedEx'])
    ->execute();

Silent Transition (No Events)

$order->transitionToWithoutEvents('processing');

Force Transition (Skip Validation)

$order->forceTransitionTo('delivered');

Permissions

StateFlow provides flexible permission control through role-based and policy-based authorization. Control who can perform state transitions based on user roles, ownership, or complex business logic.

πŸ“– Complete Permissions Documentation

Quick example:

// Role-based: Define permitted roles in state class
#[StatePermission(roles: ['admin', 'warehouse'])]
class Shipped extends OrderState {}

// Policy-based: Complex authorization logic
class OrderPolicy {
    public function transitionToShipped(User $user, Order $order): bool {
        return $user->hasRole('warehouse') && $order->isPaid();
    }
}

// Check permissions
$order->userCanTransitionTo($user, 'shipped');

History Tracking

Enable History

use Hpwebdeveloper\LaravelStateflow\Concerns\HasStateHistory;

class Order extends Model implements HasStatesContract
{
    use HasStates, HasStateHistory;
}

Query History

// Get all history
$order->stateHistory;

// Get history for specific field
$order->stateHistoryFor('state');

// Get previous state
$order->previousState();

// Get initial state
$order->initialState();

History Record

$history = $order->stateHistory->first();

$history->from_state;    // 'pending'
$history->to_state;      // 'processing'
$history->reason;        // 'Order confirmed by warehouse'
$history->performer;     // User model
$history->metadata;      // ['key' => 'value']
$history->transitioned_at;

πŸ’‘ See it in action: The demo shows complete history tracking with a timeline UI β€” see OrderController.php for how history is queried and formatted.

API Resources

State Resource

use Hpwebdeveloper\LaravelStateflow\Http\Resources\StateResource;

// Single state
return StateResource::make($order->state);

// All available states
return StateResource::collection($order->getAvailableStates());

// Response format
{
    "name": "pending",
    "title": "Pending",
    "color": "yellow",
    "icon": "clock",
    "description": "Order is pending confirmation",
    "is_current": true,
    "can_transition_to": true,
    "allowed_transitions": ["processing", "cancelled"]
}

In Controller

public function show(Order $order)
{
    return [
        'order' => $order,
        'current_state' => StateResource::make($order->state),
        'available_states' => StateResource::collection(
            $order->getAvailableStates()
        ),
    ];
}

Query Scopes

// By state
Order::whereState('shipped')->get();
Order::whereNotState('pending')->get();
Order::whereStateIn(['pending', 'processing'])->get();

// By transition capability
Order::whereCanTransitionTo('shipped')->get();

// History-based
Order::whereWasEverInState('processing')->get();
Order::whereNeverInState('cancelled')->get();

Validation Rules

use Hpwebdeveloper\LaravelStateflow\Validation\StateRule;
use Hpwebdeveloper\LaravelStateflow\Validation\TransitionRule;

// Validate state value
$request->validate([
    'state' => ['required', StateRule::make(OrderState::class)],
]);

// Validate transition is allowed
$request->validate([
    'new_state' => ['required', TransitionRule::make($order)],
]);

Events

use Hpwebdeveloper\LaravelStateflow\Events\StateTransitioning;
use Hpwebdeveloper\LaravelStateflow\Events\StateTransitioned;
use Hpwebdeveloper\LaravelStateflow\Events\TransitionFailed;

// In EventServiceProvider
protected $listen = [
    StateTransitioning::class => [
        ValidateInventory::class,
    ],
    StateTransitioned::class => [
        SendOrderNotification::class,
        UpdateInventory::class,
    ],
    TransitionFailed::class => [
        LogFailure::class,
    ],
];

Event Properties

// StateTransitioned event
$event->model;       // The model
$event->field;       // 'state'
$event->fromState;   // 'pending'
$event->toState;     // 'processing'
$event->performer;   // User who performed transition
$event->reason;      // Reason string
$event->metadata;    // Additional data

Artisan Commands

# Generate state class
php artisan make:state Pending --extends=OrderState

# Generate base state class
php artisan make:state OrderState --base

# Generate all states at once
php artisan make:state OrderState --states=Pending,Processing,Shipped,Delivered

# Generate enum from existing state classes
php artisan stateflow:sync-enum App\\States\\Order\\OrderState
# Or with custom enum name:
php artisan stateflow:sync-enum App\\States\\Order\\OrderState --enum=App\\Enums\\OrderWorkflow

# Generate transition class
php artisan make:transition ShipOrder

# List all states for a model
php artisan stateflow:list "App\Models\Order"

# Audit state configurations
php artisan stateflow:audit

🌟 Key Feature: The stateflow:sync-enum command scans your state directory and generates a workflow enum with all discovered states. This creates an enum with stateClasses(), canTransitionTo(), and transitions() methods ready for use in your model's registerStates() method.

Configuration Reference

// config/laravel-stateflow.php
return [
    // Default database column for state
    'default_state_field' => 'state',

    // Directory for generated state classes
    'states_directory' => 'States',

    // History tracking
    'history' => [
        'enabled' => true,
        'table' => 'state_histories',
        'prune_after_days' => null,
    ],

    // Permission system
    'permissions' => [
        'enabled' => true,
        'role_based' => true,
        'policy_based' => false,
        'throw_on_unauthorized' => true,
    ],

    // Event dispatching
    'events' => [
        'enabled' => true,
    ],
];

Common Patterns

Order Workflow

class Order extends Model implements HasStatesContract
{
    use HasStates;

    public static function registerStates(): void
    {
        static::addState('state', StateConfig::make(OrderState::class)
            ->default(Pending::class)
            ->allowTransition(Pending::class, Processing::class)
            ->allowTransition(Processing::class, Shipped::class)
            ->allowTransition(Shipped::class, Delivered::class)
            ->allowTransition(Pending::class, Cancelled::class)
            ->allowTransition(Processing::class, Cancelled::class)
        );
    }
}

πŸ’‘ See this in action: The laravel-stateflow-demo demonstrates this workflow β€” see Order.php for the model and OrderController.php for the controller implementation.

Multiple State Fields

public static function registerStates(): void
{
    // Order status (main workflow)
    static::addState('state', StateConfig::make(OrderState::class)
        ->default(Pending::class)
        ->allowTransition(Pending::class, Processing::class)
        ->allowTransition(Processing::class, Shipped::class)
        ->allowTransition(Shipped::class, Delivered::class)
        ->allowTransition(Pending::class, Cancelled::class)
        ->allowTransition(Processing::class, Cancelled::class)
    );

    // Payment status (separate workflow)
    static::addState('payment_status', StateConfig::make(PaymentStatus::class)
        ->default(Unpaid::class)
        ->allowTransition(Unpaid::class, Paid::class)
        ->allowTransition(Paid::class, Refunded::class)
    );
}

// Usage
$order->transitionTo(Processing::class, field: 'state');
$order->transitionTo(Paid::class, field: 'payment_status');

Custom Transition Logic

php artisan make:transition ShipOrder
// app/Transitions/ShipOrder.php
use Hpwebdeveloper\LaravelStateflow\Transition;

class ShipOrder extends Transition
{
    public function handle(): void
    {
        $this->model->shipped_at = now();
        $this->model->save();

        // Send notification, update inventory, etc.
    }

    public function canTransition(): bool
    {
        return $this->model->shipping_address !== null
            && $this->model->total > 0;
    }
}

Register in config:

->allowTransition(Processing::class, Shipped::class, ShipOrder::class)

Dependency Injection in Transitions

The handle() method supports dependency injection via Laravel's container:

use App\Services\NotificationService;
use App\Services\InventoryService;

class ShipOrder extends Transition
{
    public function handle(
        NotificationService $notifications,
        InventoryService $inventory
    ): void {
        $this->model->shipped_at = now();
        $this->model->save();

        // Services are automatically resolved from the container
        $notifications->sendShippedNotification($this->model);
        $inventory->decrementStock($this->model->items);
    }
}

This allows for clean separation of concerns and easier testing through dependency mocking.

Version Compatibility

Package Version Laravel Versions PHP Versions Status
1.x 12.x 8.3+ Active support

Note: The package is currently built for Laravel 12 with PHP 8.3+. Support for earlier Laravel versions may be added in future releases.

Credits

License

The MIT License (MIT). See License File for more information.