Defining States and Transitions
January 16, 2026 ยท View on GitHub
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
Quick Overview
| 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 |
Approach 1: Explicit Transitions in Model
Define all states and transitions directly in your model's registerStates() method.
Example: Order Model (from Demo)
// 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)
);
}
}
When to Use This Approach
โ 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
Approach 2: Attribute-Based Transitions
Define transitions directly in state classes using the #[AllowTransition] attribute. Each state declares where it can transition to.
Example: Order State Classes (from Demo)
// 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';
}
}
Model Becomes Simpler
public static function registerStates(): void
{
static::addState('state', StateConfig::make(OrderState::class)
->default(Pending::class)
// Transitions are auto-discovered from #[AllowTransition] attributes!
);
}
When to Use This Approach
โ 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/
Approach 3: Hybrid โ Enum for Topology, Classes for Behavior
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).
Why Hybrid?
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"
Example: Booking Workflow (from Demo)
The Enum โ Defines Topology
// 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;
}
}
State Classes โ Handle Behavior
// 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!
Model Uses 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())
);
}
}
When to Use This Approach
โ 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)
Scaffolding Commands
StateFlow provides Artisan commands to quickly scaffold your state machine.
Create States (Default Approach)
# Create base state + individual states
php artisan make:state OrderState --states=Pending,Processing,Shipped,Delivered,Cancelled
This creates state classes with #[AllowTransition] placeholders that you can fill in.
Create States with Enum (Hybrid Approach)
# Create states + enum scaffold
php artisan make:state BookingState --states=Draft,Confirmed,Paid,Fulfilled,Cancelled,Expired --transitions=enum
This creates:
- All state classes
- An enum file with
stateClasses(),canTransitionTo(), andtransitions()methods
Sync Existing States to Enum
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\\BookingWorkflow
This 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
Mixing Approaches
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 { }
Comparison Table
| 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 |
Our Recommendations
For Most Projects: Attributes (Approach 2)
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
For Complex Workflows: Hybrid Enum (Approach 3)
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
For Simple/Small Workflows: Explicit (Approach 1)
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
Key Takeaways
- 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.