A powerful state workflow engine for PHP that handles complex state transitions with built-in observability and race condition prevention.
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
- 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
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();
}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();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();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.
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!'),
};
}
}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
}
}composer require benrowe/stateflowRequirements: PHP 8.2+
π 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 |
// 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);
}| 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 |
π§ Alpha Stage
The architecture is designed and documented. The project is under active development.
Contributions welcome! See Contributing Guide for development setup and guidelines.
The MIT License (MIT). See LICENSE for details.
Built with β€οΈ for developers who need powerful, observable, race-safe workflows.