Skip to content

benrowe/stateflow

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 Version Build Status Total Downloads Latest Stable Version License


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.

About

An observable state management tool

Resources

License

Contributing

Stars

Watchers

Forks

Packages

No packages published