StateFlow is flexible and supports multiple approaches for defining your state machine. This guide covers the different patterns so you can choose what works best for your team.
📦 Live Examples: All approaches shown here are demonstrated in the laravel-stateflow-demo repository:
- Orders — Uses explicit/attribute approach
- Bookings — Uses hybrid enum approach
| Approach | Transitions Defined In | Best For | Demo Example |
|---|---|---|---|
| Explicit | Model's registerStates() |
Full control, simple workflows | Order.php |
| Attributes | State classes via #[AllowTransition] |
Self-contained states, IDE navigation | Pending.php |
| Hybrid Enum | Enum for topology, classes for behavior | Centralized workflow visualization | Booking.php |
Define all states and transitions directly in your model's registerStates() method.
// app/Models/Order.php
use App\States\Order\{OrderState, Pending, Processing, Shipped, Delivered, Cancelled};
class Order extends Model implements HasStatesContract
{
use HasStates, HasStateHistory;
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)
);
}
}✅ Pros:
- Everything in one place — model controls its own workflow
- Easy to understand at a glance
- No additional files needed beyond state classes
❌ Cons:
- State classes are listed twice (registration + transitions)
- Adding new states requires editing multiple places
📁 See it in action: Order.php
Define transitions directly in state classes using the #[AllowTransition] attribute. Each state declares where it can transition to.
// app/States/Order/Pending.php
use Hpwebdeveloper\LaravelStateflow\Attributes\AllowTransition;
use Hpwebdeveloper\LaravelStateflow\Attributes\DefaultState;
use Hpwebdeveloper\LaravelStateflow\Attributes\StateMetadata;
#[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';
}
}// app/States/Order/Processing.php
#[StateMetadata(title: 'Processing', description: 'Order is being processed')]
#[AllowTransition(to: Shipped::class)]
#[AllowTransition(to: Cancelled::class)]
class Processing extends OrderState
{
public const NAME = 'processing';
public static function color(): string
{
return 'blue';
}
}public static function registerStates(): void
{
static::addState('state', StateConfig::make(OrderState::class)
->default(Pending::class)
// Transitions are auto-discovered from #[AllowTransition] attributes!
);
}✅ Pros:
- Each state is self-contained (behavior + transitions in one file)
- Great IDE support — click through to see transitions
- No duplication — each state defined once
- StateFlow auto-discovers attributes
❌ Cons:
- To see full workflow, must open multiple files
- Transitions are "scattered" across state classes
📁 See it in action: States/Order/
Use a PHP enum to define the workflow topology (which states exist and how they connect), while state classes handle behavior (colors, icons, metadata, permissions).
This approach gives you:
- Single file showing the entire workflow graph
- State classes still handle all metadata and behavior
- Clear separation: Enum = "what can happen", Class = "what it looks/acts like"
// app/Enums/BookingWorkflow.php
namespace App\Enums;
use App\States\Booking\{Draft, Confirmed, Paid, Fulfilled, Cancelled, Expired};
/**
* Booking workflow topology.
*
* Transition graph:
* draft → confirmed, expired
* confirmed → paid, cancelled, expired
* paid → fulfilled, cancelled
* fulfilled, cancelled, expired → (final states)
*/
enum BookingWorkflow: string
{
case Draft = 'draft';
case Confirmed = 'confirmed';
case Paid = 'paid';
case Fulfilled = 'fulfilled';
case Cancelled = 'cancelled';
case Expired = 'expired';
/**
* Map enum case to state class.
*/
public function stateClass(): string
{
return match ($this) {
self::Draft => Draft::class,
self::Confirmed => Confirmed::class,
self::Paid => Paid::class,
self::Fulfilled => Fulfilled::class,
self::Cancelled => Cancelled::class,
self::Expired => Expired::class,
};
}
/**
* Define transitions FROM this state.
*/
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
self::Fulfilled, self::Cancelled, self::Expired => [],
};
}
/**
* Get all state classes for registration.
*/
public static function stateClasses(): array
{
return array_map(fn (self $case) => $case->stateClass(), self::cases());
}
/**
* Get transitions in format for StateConfig::allowTransitionsFromArray().
*/
public static function transitions(): array
{
$transitions = [];
foreach (self::cases() as $case) {
foreach ($case->canTransitionTo() as $targetClass) {
$transitions[] = [
'from' => $case->stateClass(),
'to' => $targetClass,
];
}
}
return $transitions;
}
}// app/States/Booking/Draft.php
#[DefaultState]
#[StateMetadata(title: 'Draft', description: 'Booking is in draft')]
class Draft extends BookingState
{
public const NAME = 'draft';
public static function color(): string { return 'gray'; }
public static function icon(): string { return 'file-edit'; }
}
// app/States/Booking/Confirmed.php
#[StateMetadata(title: 'Confirmed', description: 'Booking confirmed by customer')]
class Confirmed extends BookingState
{
public const NAME = 'confirmed';
public static function color(): string { return 'blue'; }
public static function icon(): string { return 'check-circle'; }
}
// app/States/Booking/Paid.php
#[StateMetadata(title: 'Paid', description: 'Payment received')]
class Paid extends BookingState
{
public const NAME = 'paid';
public static function color(): string { return 'green'; }
public static function icon(): string { return 'credit-card'; }
}Notice: No #[AllowTransition] attributes — transitions are defined in the enum!
// app/Models/Booking.php
use App\Enums\BookingWorkflow;
use App\States\Booking\{BookingState, Draft};
class Booking extends Model implements HasStatesContract
{
use HasStates, HasStateHistory;
public static function registerStates(): void
{
static::addState('state', StateConfig::make(BookingState::class)
->default(Draft::class)
->registerStates(BookingWorkflow::stateClasses())
->allowTransitionsFromArray(BookingWorkflow::transitions())
);
}
}✅ Pros:
- Entire workflow visible in one file (the enum)
- Easy to visualize and document
- Share workflow across multiple models
- Great for generating diagrams
❌ Cons:
- Two files to maintain (enum + state classes)
- Must keep enum values synced with state
NAMEconstants
📁 See it in action:
- BookingWorkflow.php (enum)
- Booking.php (model)
- States/Booking/ (state classes)
StateFlow provides Artisan commands to quickly scaffold your state machine.
# Create base state + individual states
php artisan make:state OrderState --states=Pending,Processing,Shipped,Delivered,CancelledThis creates state classes with #[AllowTransition] placeholders that you can fill in.
# Create states + enum scaffold
php artisan make:state BookingState --states=Draft,Confirmed,Paid,Fulfilled,Cancelled,Expired --transitions=enumThis creates:
- All state classes
- An enum file with
stateClasses(),canTransitionTo(), andtransitions()methods
If you've already created state classes and want to generate an enum:
php artisan stateflow:sync-enum App\\States\\Booking\\BookingState --enum=App\\Enums\\BookingWorkflowThis scans your state classes and generates a matching enum.
⚠️ Naming Convention: By default,stateflow:sync-enumcreates an enum named{BaseStateClass}Status(e.g.,BookingState→BookingStateStatus). Use the--enumoption to specify a custom name likeBookingWorkflow.
⚠️ Directory Requirement: The sync command only discovers state classes in the same directory as the base state class. When adding new states, use the full namespace:php artisan make:state Processing --extends=App\\States\\Booking\\BookingState
StateFlow merges all transition definitions together. You can combine approaches:
public static function registerStates(): void
{
static::addState('state', StateConfig::make(OrderState::class)
->default(Pending::class)
// From enum
->registerStates(OrderStatus::stateClasses())
->allowTransitionsFromArray(OrderStatus::transitions())
// Add extra transition not in enum
->allowTransition(Processing::class, OnHold::class)
);
}State classes can also declare additional transitions via attributes:
// This transition is ALSO respected, even if using enum approach
#[AllowTransition(to: Refunded::class)]
class Cancelled extends OrderState { }| Feature | Explicit | Attributes | Hybrid Enum |
|---|---|---|---|
| Workflow visibility | Model file | Scattered across state files | Single enum file |
| Self-contained states | ❌ | ✅ | Partially (behavior only) |
| Easy to add states | Edit model | Create file + add attributes | Create file + edit enum |
| IDE navigation | Model → State | State → State | Enum → All transitions |
| Diagram generation | Manual | Parse attributes | Simple enum iteration |
| Reuse across models | Copy/paste | Copy state classes | Share enum |
| Demo example | Order.php | Pending.php | Booking.php |
The attribute-based approach is recommended because:
- Each state is completely self-contained
- IDE support is excellent
- Already built into StateFlow
- No synchronization between files
Consider the hybrid enum approach when:
- You need to see the entire workflow at a glance
- You're generating documentation or diagrams
- Multiple models share the same workflow
- Your team prefers centralized definitions
The explicit approach works well when:
- You have a simple workflow (3-5 states)
- You prefer everything in one file
- You don't need state class behavior beyond storage
- StateFlow doesn't force a single approach — choose what fits your team
- Approaches can be mixed — transitions from all sources are merged
- State classes always handle behavior — colors, icons, metadata, permissions
- Enums are optional — only use if you need centralized topology
- Both demo examples work — Orders (explicit/attributes) and Bookings (hybrid enum)
💡 Start simple, evolve as needed. Begin with explicit transitions, add attributes as your workflow grows, and consider an enum if you need centralized visualization.