wundii/flowcrafter

PHP library for defining, executing, and monitoring message-driven workflows (state machines)

Maintainers

Package info

github.com/wundii/flowcrafter

pkg:composer/wundii/flowcrafter

Statistics

Installs: 65

Dependents: 0

Suggesters: 0

Stars: 2

Open Issues: 0

0.23.6 2026-05-22 17:36 UTC

README

PHP-Tests PHPStan VERSION PHP Rector ECS PHPUnit codecov Downloads

PHP-Engine für message-driven Workflows — Schema-as-Code, typsicheres Routing über Message-Klassen, synchrone und asynchrone Ausführung mit vollständigem Audit-Log.

Features

  • Typsichere Workflow-Definitionen als PHP-Klassen — kein YAML/XML
  • Storage-Backends: MySQL, Redis, EventSourcingDB mit SQLite-Service-Layer als Query-Cache — eigene Backends via StorageInterface frei erweiterbar
  • Synchrone Ausführung (FlowRunner) + asynchrone Queue-Verarbeitung (FlowObserver) + zeitgesteuerte Ausführung (FlowScheduler)
  • Automatischer Flow-Status, vollständiges Message-, Exception- & Schedule-Exception-Logging, Schema-Versionierung via Hash
  • #[FlowGroup]- und #[FlowSchedule(group:)]-Attribute für UI-Gruppierung von Flow-Typen und Schedules
  • UI-DevTool wird automatisch aktiviert wenn der Server via bin/flowcrafter dev gestartet wird
  • REST-API für Flows, Schemas, Queues, Exceptions & Schedule-Exceptions inkl. Prometheus/OpenMetrics-Endpunkt;
  • Symfony Console Commands für Config, Storage-Init/Rebuild, Dev-Server, Observer, Scheduler und Mermaid-Diagramme
  • Testing-Helper (FlowTestCase, FlowAssertTrait) für storageless Unit-Tests

Installation

composer require wundii/flowcrafter

Dokumentation

Die vollständige Dokumentation liegt im docs/-Ordner:

Kapitel Inhalt
Console Commands Command-Referenz
Deployment Produktion: FrankenPHP + Docker
Entwicklung QA-Scripts für Contributor
Getting Started Erste Schritte: Config, Storage, Dev-Server
Konfiguration flowcrafter.php, Storage-Backends, Server-Einstellungen
Konzepte Flow, Status, Schema, Messages, includeSteps, Observer
Monitoring Prometheus / OpenMetrics, CheckMK
REST-API Endpunkte, Pagination, Auth
Testing Flows & Steps testen mit PHPUnit 11+

Quickstart

# 1. Config-Datei erzeugen
vendor/bin/flowcrafter config:create

# 2. Storage initialisieren
vendor/bin/flowcrafter storage:init

# 3. Dev-Server (API + Observer + Scheduler) starten
vendor/bin/flowcrafter dev

Details siehe docs/getting-started.md.

Web-UI

Das optionale Web-Frontend FlowCrafter UI visualisiert Flows, Messages, Exceptions, Schedules und Queues in Echtzeit:

docker run -p 5173:5173 -v ./data:/flowcrafter/data wundii/flowcrafter-ui:latest

Overview

Flow Detail

Flow Input Modal

Flow Devtool

Claude Code Plugin

Das optionale Claude Code Plugin flowcrafter-claude erweitert Claude Code mit Flowcrafter-Wissen — Flows, Steps, Messages und Schedules lassen sich per Slash-Command generieren und analysieren, ohne das Framework-Modell im Kopf behalten zu müssen.

/plugin marketplace add wundii/flowcrafter-claude
/plugin install flowcrafter@flowcrafter-claude
Command Beschreibung
/create-flow Flow-Klasse mit FlowBuilder-DSL generieren
/create-step Step-Klasse mit Message-Injection generieren
/create-message Message-Klasse (init / data / return) generieren
/create-schedule Schedule-Klasse mit Cron-Ausdruck generieren
/analyze-flow Flow auf Fehler und Verbesserungen prüfen

Der flowcrafter-Skill wird zusätzlich automatisch aktiviert, sobald Flowcrafter-Begriffe im Gespräch auftauchen — ohne manuellen Befehl.

Minimalbeispiel

Messages

readonly Value-Objects. Drei Typen: Init startet den Flow, Data fließt zwischen Steps, Return beendet den Flow:

use Wundii\Flowcrafter\AbstractMessage;
use Wundii\Flowcrafter\Interface\MessageDataInterface;
use Wundii\Flowcrafter\Interface\MessageInitInterface;
use Wundii\Flowcrafter\Interface\MessageReturnInterface;

readonly class OrderInit extends AbstractMessage implements MessageInitInterface
{
    public function __construct(private string $sku) {}
    public function getSku(): string { return $this->sku; }
}

readonly class OrderValidated extends AbstractMessage implements MessageDataInterface
{
    public function __construct(private string $sku, private int $quantity) {}
    public function getSku(): string { return $this->sku; }
    public function getQuantity(): int { return $this->quantity; }
}

readonly class OrderCompleted extends AbstractMessage implements MessageReturnInterface
{
    public function __construct(private string $summary) {}
    public function getSummary(): string { return $this->summary; }
}

Braucht der erste Step keinen externen Input, kann statt einer eigenen Init-Klasse die mitgelieferte Wundii\Flowcrafter\EmptyInitMessage verwendet werden. Damit Rector den Konstruktor-Parameter nicht als ungenutzt entfernt, wird sie als public readonly promoted Property deklariert:

use Wundii\Flowcrafter\EmptyInitMessage;

class StartStep implements StepInterface
{
    public function __construct(
        public readonly EmptyInitMessage $init,
    ) {}

    /** @return class-string[] */
    public function returnTypes(): array { return [OrderValidated::class]; }

    public function process(): MessageDataInterface
    {
        return new OrderValidated('SKU-1', quantity: 1);
    }
}

Steps

reine PHP-Klassen. Der Constructor-Typ entscheidet das Routing. Ein Step kann MessageData (→ Flow läuft weiter), MessageReturn (→ Flow endet) oder bool (→ Leaf-Result) zurückgeben:

use Wundii\Flowcrafter\Interface\MessageDataInterface;
use Wundii\Flowcrafter\Interface\MessageReturnInterface;
use Wundii\Flowcrafter\Interface\StepInterface;

// Zwischenschritt: Init → Data
class ValidateStep implements StepInterface
{
    public function __construct(private readonly OrderInit $init) {}

    /** @return class-string[] */
    public function returnTypes(): array { return [OrderValidated::class]; }

    public function process(): MessageDataInterface
    {
        return new OrderValidated($this->init->getSku(), quantity: 1);
    }
}

// Haupt-Branch: Data → Return (beendet den Flow)
class CompleteOrderStep implements StepInterface
{
    public function __construct(private readonly OrderValidated $validated) {}

    /** @return class-string[] */
    public function returnTypes(): array { return [OrderCompleted::class]; }

    public function process(): MessageReturnInterface
    {
        return new OrderCompleted(sprintf(
            'Order %s x%d completed',
            $this->validated->getSku(),
            $this->validated->getQuantity(),
        ));
    }
}

// Leaf-Step: Data → bool (FlowResult, kein Weiterleiten)
class AuditStep implements StepInterface
{
    public function __construct(private readonly OrderValidated $validated) {}

    /** @return class-string[] */
    public function returnTypes(): array { return []; }

    public function process(): bool
    {
        return $this->validated->getQuantity() > 0;
    }
}

Flow

Schema via FlowBuilder, kein YAML. Zwei Steps konsumieren OrderValidated parallel.

Optional kann ein Flow mit #[FlowGroup] einer UI-Gruppe zugeordnet werden — beeinflusst den Schema-Hash nicht:

use Wundii\Flowcrafter\Attribute\FlowGroup;

#[FlowGroup('Order Management')]
class OrderFlow implements FlowInterface { ... }
use Wundii\Flowcrafter\FlowBuilder;
use Wundii\Flowcrafter\FlowSchema;
use Wundii\Flowcrafter\Interface\FlowInterface;

class OrderFlow implements FlowInterface
{
    public static function schema(): FlowSchema
    {
        $builder = new FlowBuilder('flow.order.v1', OrderInit::class, OrderCompleted::class);
        $builder->addStep(ValidateStep::class);
        $builder->addStep(CompleteOrderStep::class, retries: 3, delay: 500);
        $builder->addStep(AuditStep::class);
        return $builder->build();
    }
}

Flow-Diagramm

automatisch aus dem Schema generierbar via vendor/bin/flowcrafter diagram:mermaid App\\OrderFlow:

---
title: flow.order.v1
theme: neo
---
stateDiagram-v2
[*]-->ValidateStep: OrderInit
ValidateStep-->CompleteOrderStep: OrderValidated
ValidateStep-->AuditStep: OrderValidated
CompleteOrderStep-->[*]: OrderCompleted
Loading

Step Retry

Steps können bei transienten Fehlern (z. B. externe API nicht erreichbar) automatisch wiederholt werden. Die Konfiguration erfolgt pro Step über addStep():

$builder->addStep(ValidateStep::class);                            // kein Retry (default)
$builder->addStep(ExternalApiStep::class, retries: 3);             // 3 zusätzliche Versuche, 200ms Delay
$builder->addStep(SlowServiceStep::class, retries: 5, delay: 500); // 5 zusätzliche Versuche, 500ms Delay
Parameter Typ Default Beschreibung
retries int 0 Anzahl zusätzlicher Versuche nach dem Erstversuch
delay int 200 Fixer Delay in Millisekunden zwischen den Versuchen
  • retries: 3 bedeutet: 1 Erstversuch + 3 Wiederholungen = max. 4 Ausführungen
  • Retry-Konfiguration beeinflusst den Schema-Hash — eine Änderung erzeugt eine neue Schema-Version
  • Bei Erschöpfung aller Versuche wird die letzte Exception wie gewohnt als FlowException persistiert

Flow auslösen

Zwei Wege: synchron im eigenen Code via FlowRunner oder asynchron über die Queue (vom FlowObserver abgearbeitet).

Synchron — direkter Aufruf, Ergebnis sofort verfügbar:

use Wundii\Flowcrafter\FlowRunner;

$flowRunner = new FlowRunner(
    type: 'flow.order.v1',
    flowSource: OrderFlow::class,
    flowSubject: 'sku-42',          // optional, Geschäfts-Key zur späteren Suche
    storage: $storage,              // aus $flowcrafterConfig->getStorage()
);

$result = $flowRunner->run(new OrderInit('sku-42'));
// $result ist MessageReturnInterface|bool — hier: OrderCompleted

Asynchron — Message in die Queue legen, der FlowObserver-Worker führt sie aus:

$storage->appendObserveItem(
    type: 'flow.order.v1',
    flowSource: OrderFlow::class,
    flowHash: null,                 // null = neuer Flow, sonst Re-Run einer bestehenden Instanz
    messageSource: OrderInit::class,
    message: (new OrderInit('sku-42'))->jsonSerialize(),
    flowSubject: 'sku-42',
);

Alternativ über die REST-API: POST /api/flow/flow-run (synchron) bzw. POST /api/queue/enqueue (async) — siehe docs/api.md.

Zeitgesteuert — Schedule-Klasse mit Cron-Ausdruck, wird automatisch vom FlowScheduler entdeckt und ausgeführt:

use Wundii\Flowcrafter\Attribute\FlowSchedule;
use Wundii\Flowcrafter\Schedule\AbstractSchedule;

#[FlowSchedule('0 */6 * * *', name: 'order-cleanup', group: 'Maintenance')]
class OrderCleanupSchedule extends AbstractSchedule
{
    public function process(): void
    {
        $this->enqueue(OrderFlow::class, new OrderInit('scheduled-cleanup'));
        // oder synchron: $this->run(OrderFlow::class, new OrderInit('cleanup'));
    }
}

Schedule-Klassen werden über das #[FlowSchedule]-Attribut automatisch aus dem Composer-Classmap entdeckt — keine manuelle Registrierung nötig. Der Scheduler läuft als eigenständiger Prozess (vendor/bin/flowcrafter scheduler) oder im Dev-Modus inline mit.

Dependency Injection

Steps können neben Messages auch externe Services per Constructor-Injection erhalten. Die Abhängigkeiten werden über dependenciesInjection in FlowRunner, FlowScheduler und FlowAssertTrait registriert — drei Modi stehen zur Verfügung:

Schlüssel Wert Verhalten
ohne Schlüssel class-string Klasse wird automatisch per Autowiring registriert
ohne Schlüssel object Konkrete Instanz, gebunden an die eigene Klasse
Interface-Klassenname object Instanz wird an Interface und Konkreten Klasse gebunden (Alias)
// Step mit Interface-Abhängigkeit
class FetchStep implements StepInterface
{
    public function __construct(
        private readonly OrderInit $init,
        private readonly HttpClientInterface $http,  // Interface, kein Concrete!
    ) {}

    public function returnTypes(): array { return [OrderValidated::class]; }

    public function process(): MessageDataInterface
    {
        $data = $this->http->get('/api/order/' . $this->init->getSku());
        return new OrderValidated($this->init->getSku(), $data['quantity']);
    }
}
// FlowRunner mit Interface-Binding
$flowRunner = new FlowRunner(
    type: 'flow.order.v1',
    flowSource: OrderFlow::class,
    storage: $storage,
    dependenciesInjection: [
        // Interface → konkrete Instanz
        HttpClientInterface::class => new CurlHttpClient(),

        // oder: direkte Instanz ohne Interface
        new MyLogger(),

        // oder: Klasse per Autowiring
        SomeService::class,
    ],
);

$result = $flowRunner->run(new OrderInit('sku-42'));
// Test mit Interface-Binding in FlowAssertTrait
$this->setDependenciesInjection([
    HttpClientInterface::class => new CurlHttpClientMock(),
]);
$this->runFlow('flow.order.v1', OrderFlow::class, new OrderInit('sku-42'));

Test

storageless mit FlowTestCase, kein Docker nötig:

use Wundii\Flowcrafter\Testing\FlowTestCase;

final class OrderFlowTest extends FlowTestCase
{
    public function testHappyPath(): void
    {
        $this->runFlow(
            flowType: 'flow.order.v1',
            flowSource: OrderFlow::class,
            initMessage: new OrderInit('sku-42'),
        );

        $this->assertFlowOk();
        $this->assertStepExecuted(ValidateStep::class);
        $this->assertStepExecuted(CompleteOrderStep::class);
        $this->assertStepExecuted(AuditStep::class);
        $this->assertFlowHasMessage(OrderValidated::class);
        $this->assertFlowBoolResult(true);   // AuditStep lieferte true

        $return = $this->assertFlowReturned(OrderCompleted::class);
        $this->assertSame('Order sku-42 x1 completed', $return->getSummary());
    }
}

Vollständiger Testing-Leitfaden: docs/testing.md.

Lizenz

MIT — siehe LICENCE.