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

v0.0.0-beta.1 2025-11-26 03:57 UTC

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.

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.