Skip to content

Repository files navigation

StateFlow

A powerful state workflow engine for PHP that handles complex state transitions with built-in observability and race condition prevention.

PHP VersionBuild StatusTotal DownloadsLatest Stable VersionLicense


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
  • Async Action Yielding - Suspend a single action for external async work (webhooks, third-party APIs, external events) and resume with the response; remaining actions in the same transition continue automatically
  • 🔧 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
  • Complex workflow systems
  • Any scenario where state transitions need audit trails and concurrency control

Quick Example

useCoverGenius\StateFlow\StateFlow;
useCoverGenius\StateFlow\Configuration;
// Define your stateclass Order implements State {
publicfunction__construct(
privatestring$status,
private ?string$paymentId = null,
) {}
publicfunctionwith(array$changes): State {
returnnewself(
status: $changes['status'] ?? $this->status,
paymentId: $changes['paymentId'] ?? $this->paymentId,
);
}
publicfunctiontoArray(): array {
return ['status' => $this->status, 'paymentId' => $this->paymentId];
}
}
// Configure the workflow$stateFlow = newStateFlow(
configProvider: fn(State$state, Delta$delta) => newConfiguration( // your configuration can be dynamic based on the current state & incoming delta
transitionGates: [newCanProcessGate()], // Must pass to proceed
actions: [
newChargePaymentAction(), // Execute in ordernewReserveInventoryAction(), // Skip if guard failsnewSendConfirmationAction(),
],
),
eventDispatcher: newLogger(), // See everything that happens
lockProvider: newRedisLock($redis), // Prevent race conditions, leave null for no locking.
);
// Execute transition with automatic locking$order = newOrder('pending');
$worker = $stateFlow->transition($order, newArrayDelta(['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 pausesaveToDatabase($context->serialize());
// Resume hours later...$resumedWorker = $stateFlow->fromContext($context);
$resumedWorker->execute();
}

Key Features

🎯 Delta-Based Transitions

Specify only what changes:

// Just this$worker = $stateFlow->transition($state, newArrayDelta(['status' => 'published']));
$context = $worker->execute();
// Not this$worker = $stateFlow->transition($state, newArrayDelta(['status' => 'published', 'author' => 'same', 'created' => 'same', ...]));
$context = $worker->execute();

⚙️ Granular Execution Control

The StateWorker gives you full control over the workflow execution:

$worker = $stateFlow->transition($state, newArrayDelta(['status' => 'published']));
// 1. Run gates first$gateResult = $worker->runGates();
// 2. Then run actions if gates passif (!$gateResult->shouldStopTransition()) {
$context = $worker->runActions();
}
// Or let actions pause themselves for async operationsclass ProcessVideoAction implements Action {
publicfunctionexecute(ActionContext$context): ActionResult {
$job = dispatch(newVideoProcessingJob());
// Pause execution, lock is heldreturn ActionResult::pause(metadata: ['jobId' => $job->id]);
}
}
// Resume later when ready$resumedWorker = $stateFlow->fromContext($pausedContext);
$resumedWorker->execute();

⏳ Async Action Yielding

Suspend a single action mid-transition to wait for an external response — remaining actions continue in the same transition once resumed:

class RunFraudCheckAction implements Action, Yieldable {
publicfunctionexecute(ActionContext$context): ActionResult {
if ($context->hasYieldResponse()) {
// Second call: webhook delivered the async result$response = $context->yieldResponse();
return$response['outcome'] === 'approved'
? ActionResult::continue()
: ActionResult::stop(['reason' => 'fraud_check_rejected']);
}
// First call: dispatch external work and suspend$this->client->startCheck($context->currentState);
return ActionResult::yield(['dispatchedAt' => time()]);
}
}
// Webhook handler resumes the transition with the async response$worker = $stateFlow->fromContext($persistedContext);
$worker->resumeWithResponse(['outcome' => 'approved', 'checkId' => $id]);
// Remaining actions in the transition run immediately after

🔒 Race Condition Prevention

Built-in mutex locking, configured on the StateFlow:

$lockProvider = newRedisLockProvider($redis, $config);
$stateFlow = newStateFlow(
configProvider: $configProvider,
lockProvider: $lockProvider,
);
// This transition will be automatically locked$worker = $stateFlow->transition($state, newArrayDelta(['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 {
publicfunctiondispatch(Event$event): void {
match (true) {
$eventinstanceof TransitionStarting => $this->log('Starting...'),
$eventinstanceof GateEvaluated => $this->log('Gate: ' . $event->result),
$eventinstanceof ActionExecuted => $this->log('Action done'),
$eventinstanceof TransitionCompleted => $this->log('Complete!'),
};
}
}

🎨 Two-Tier Validation

Transition Gates - Must pass for transition to begin:

class CanPublishGate implements Gate {
publicfunctionevaluate(GateContext$context): GateResult {
return$context->currentState->hasContent()
? GateResult::ALLOW
: GateResult::DENY;
}
}

Action Gates - Skip individual actions if guard fails:

class NotifyAction implements Action, Guardable {
publicfunctiongate(): Gate {
returnnewHasSubscribersGate();
}
publicfunctionexecute(ActionContext$context): ActionResult {
// Only runs if HasSubscribersGate passes
}
}

Installation

composer require covergenius/stateflow

Requirements: PHP 8.2+

Documentation

📚 Comprehensive documentation available in the docs/ directory:

DocumentDescription
Architecture OverviewDesign goals and principles
Flow DiagramsVisual flowcharts (Mermaid)
Core ConceptsState, Gates, Actions, Configuration
ObservabilityEvent system and monitoring
Locking SystemRace condition handling
Interface ReferenceComplete API documentation
Usage ExamplesReal-world patterns

Real-World Example

E-Commerce Order Processing

// 1. Define state with your domain modelclass OrderState implements State {
publicfunction__construct(
privatestring$id,
privatestring$status,
privatefloat$total,
private ?string$paymentId = null,
) {}
publicfunctionwith(array$changes): State {
returnnewself(
id: $this->id,
status: $changes['status'] ?? $this->status,
total: $changes['total'] ?? $this->total,
paymentId: $changes['paymentId'] ?? $this->paymentId,
);
}
publicfunctiontoArray(): array { /* ... */ }
}
// 2. Configure workflow based on transition type$configProvider = function(State$state, Delta$delta): Configuration {
returnmatch ($delta->get('status')) {
'processing' => newConfiguration(
transitionGates: [newHasInventoryGate($inventory)],
actions: [
newChargePaymentAction($paymentGateway),
newReserveInventoryAction($inventory),
newSendEmailAction($mailer),
],
),
'shipped' => newConfiguration(
transitionGates: [newHasPaymentGate()],
actions: [newCreateShipmentAction($shipping)],
),
default => newConfiguration(),
};
};
// 3. Create state flow with observability and locking$stateFlow = newStateFlow(
configProvider: $configProvider,
eventDispatcher: newMetricsDispatcher(),
lockProvider: newRedisLockProvider($redis),
lockKeyProvider: newclassimplements LockKeyProvider {
publicfunctiongetLockKey(State$state, Delta$delta): string {
return"order:" . $state->toArray()['id'];
}
},
);
// 4. Execute with race protectiontry {
$order = newOrderState('ORD-123', 'pending', 99.99);
$worker = $stateFlow->transition($order, newArrayDelta(['status' => 'processing']));
$context = $worker->execute();
if ($context->isCompleted()) {
returnresponse()->json(['status' => 'success']);
}
} catch (LockAcquisitionException$e) {
// Another request is processing this orderreturnresponse()->json(['error' => 'Order is being processed'], 409);
}

What Makes StateFlow Different?

FeatureStateFlowTraditional State Machines
Granular Control✅ Per-action execution & pause/resume❌ Must complete in one execution
Async Yielding✅ Single action suspends for external work, resumes with response❌ Must split into multiple transitions
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/yields❌ 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.

About

An observable state management tool

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages