benrowe / stateflow
A flexible state machine library for PHP
Installs: 0
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 1
pkg:composer/benrowe/stateflow
Requires
- php: ^8.2
Requires (Dev)
- brianium/paratest: ^7.4
- friendsofphp/php-cs-fixer: ^3.0
- phpstan/phpstan: ^1.10
- phpunit/phpunit: ^10.0
This package is auto-updated.
Last update: 2025-12-01 19:34:34 UTC
README
A powerful state workflow engine for PHP that handles complex state transitions with built-in observability and race condition prevention.
Why StateFlow?
Most state machines force you into rigid patterns. StateFlow is different:
- 🎯 Delta-Based Transitions - Specify only what changes, not the entire state
- ⚙️ Granular Execution Control - Manage workflow execution at the per-action level
- 🔒 Race-Safe by Design - Built-in mutex locking prevents concurrent modification
- 👀 Fully Observable - Events fired at every step for monitoring and debugging
- 🎨 Flexible Validation - Two-tier gates (transition-level + action-level)
- 📦 Serializable Context - Pause, store, and resume workflows hours or days later
- 🔧 User-Controlled - You define state structure, merge strategy, and lock behavior
Perfect For
- E-commerce order processing with payment/inventory/shipping workflows
- Content publishing pipelines with approval stages and notifications
- Long-running batch jobs that need checkpointing
- Multi-step user onboarding flows
- Any scenario where state transitions need audit trails and concurrency control
Quick Example
use BenRowe\StateFlow\StateMachine; use BenRowe\StateFlow\Configuration; // Define your state class Order implements State { public function __construct( private string $status, private ?string $paymentId = null, ) {} public function with(array $changes): State { return new self( status: $changes['status'] ?? $this->status, paymentId: $changes['paymentId'] ?? $this->paymentId, ); } public function toArray(): array { return ['status' => $this->status, 'paymentId' => $this->paymentId]; } } // Configure the workflow $machine = new StateMachine( configProvider: fn($state, $delta) => new Configuration( transitionGates: [new CanProcessGate()], // Must pass to proceed actions: [ new ChargePaymentAction(), // Execute in order new ReserveInventoryAction(), // Skip if guard fails new SendConfirmationAction(), ], ), eventDispatcher: new Logger(), // See everything that happens lockProvider: new RedisLock($redis), // Prevent race conditions ); // Execute transition with automatic locking $order = new Order('pending'); $worker = $machine->transition($order, ['status' => 'processing']); $context = $worker->execute(); if ($context->isCompleted()) { echo "Order processed!"; } elseif ($context->isPaused()) { // Action paused (e.g., waiting for external API) // Lock is HELD across pause saveToDatabase($context->serialize()); // Resume hours later... $resumedWorker = $machine->fromContext($context); $resumedWorker->execute(); }
Key Features
🎯 Delta-Based Transitions
Specify only what changes:
// Just this $worker = $machine->transition($state, ['status' => 'published']); $context = $worker->execute(); // Not this $worker = $machine->transition($state, ['status' => 'published', 'author' => 'same', 'created' => 'same', ...]); $context = $worker->execute();
⚙️ Granular Execution Control
The StateWorker gives you full control over the workflow execution:
$worker = $machine->transition($state, ['status' => 'published']); // 1. Run gates first $gateResult = $worker->runGates(); // 2. Then run actions if gates pass if (!$gateResult->shouldStopTransition()) { $context = $worker->runActions(); } // Or let actions pause themselves for async operations class ProcessVideoAction implements Action { public function execute(ActionContext $context): ActionResult { $job = dispatch(new VideoProcessingJob()); // Pause execution, lock is held return ActionResult::pause(metadata: ['jobId' => $job->id]); } } // Resume later when ready $resumedWorker = $machine->fromContext($pausedContext); $resumedWorker->execute();
🔒 Race Condition Prevention
Built-in mutex locking, configured on the StateMachine:
$lockProvider = new RedisLockProvider($redis, $config); $machine = new StateMachine( configProvider: $configProvider, lockProvider: $lockProvider, ); // This transition will be automatically locked $worker = $machine->transition($state, ['status' => 'published']); $context = $worker->execute();
If another process tries to transition the same entity, it will wait, fail, or skip based on your lock provider's behavior.
👀 Fully Observable
Every step emits events:
class MyEventDispatcher implements EventDispatcher { public function dispatch(Event $event): void { match (true) { $event instanceof TransitionStarting => $this->log('Starting...'), $event instanceof GateEvaluated => $this->log('Gate: ' . $event->result), $event instanceof ActionExecuted => $this->log('Action done'), $event instanceof TransitionCompleted => $this->log('Complete!'), }; } }
🎨 Two-Tier Validation
Transition Gates - Must pass for transition to begin:
class CanPublishGate implements Gate { public function evaluate(GateContext $context): GateResult { return $context->currentState->hasContent() ? GateResult::ALLOW : GateResult::DENY; } }
Action Gates - Skip individual actions if guard fails:
class NotifyAction implements Action, Guardable { public function gate(): Gate { return new HasSubscribersGate(); } public function execute(ActionContext $context): ActionResult { // Only runs if HasSubscribersGate passes } }
Installation
composer require benrowe/stateflow
Requirements: PHP 8.2+
Documentation
📚 Comprehensive documentation available in the docs/ directory:
| Document | Description |
|---|---|
| Architecture Overview | Design goals and principles |
| Flow Diagrams | Visual flowcharts (Mermaid) |
| Core Concepts | State, Gates, Actions, Configuration |
| Observability | Event system and monitoring |
| Locking System | Race condition handling |
| Interface Reference | Complete API documentation |
| Usage Examples | Real-world patterns |
Real-World Example
E-Commerce Order Processing
// 1. Define state with your domain model class OrderState implements State { public function __construct( private string $id, private string $status, private float $total, private ?string $paymentId = null, ) {} public function with(array $changes): State { return new self( id: $this->id, status: $changes['status'] ?? $this->status, total: $changes['total'] ?? $this->total, paymentId: $changes['paymentId'] ?? $this->paymentId, ); } public function toArray(): array { /* ... */ } } // 2. Configure workflow based on transition type $configProvider = function(State $state, array $delta): Configuration { return match ($delta['status'] ?? null) { 'processing' => new Configuration( transitionGates: [new HasInventoryGate($inventory)], actions: [ new ChargePaymentAction($paymentGateway), new ReserveInventoryAction($inventory), new SendEmailAction($mailer), ], ), 'shipped' => new Configuration( transitionGates: [new HasPaymentGate()], actions: [new CreateShipmentAction($shipping)], ), default => new Configuration(), }; }; // 3. Create machine with observability and locking $machine = new StateMachine( configProvider: $configProvider, eventDispatcher: new MetricsDispatcher(), lockProvider: new RedisLockProvider($redis), lockKeyProvider: new class implements LockKeyProvider { public function getLockKey(State $state, array $delta): string { return "order:" . $state->toArray()['id']; } }, ); // 4. Execute with race protection try { $order = new OrderState('ORD-123', 'pending', 99.99); $worker = $machine->transition($order, ['status' => 'processing']); $context = $worker->execute(); if ($context->isCompleted()) { return response()->json(['status' => 'success']); } } catch (LockAcquisitionException $e) { // Another request is processing this order return response()->json(['error' => 'Order is being processed'], 409); }
What Makes StateFlow Different?
| Feature | StateFlow | Traditional State Machines |
|---|---|---|
| Granular Control | ✅ Per-action execution & pause/resume | ❌ Must complete in one execution |
| Race-Safe | ✅ Built-in mutex locking | ❌ Manual coordination required |
| Observable | ✅ Events at every step | ❌ Limited visibility |
| Flexible State | ✅ User-defined merge strategy | ❌ Rigid state structure |
| Lazy Config | ✅ Load gates/actions on-demand | ❌ All configured upfront |
| Lock Persistence | ✅ Lock held across pauses | ❌ N/A |
| Execution Trace | ✅ Complete audit trail | ❌ Limited history |
Status
🚧 Alpha Stage
The architecture is designed and documented. The project is under active development.
Contributing
Contributions welcome! See Contributing Guide for development setup and guidelines.
License
The MIT License (MIT). See LICENSE for details.
Credits
Built with ❤️ for developers who need powerful, observable, race-safe workflows.