wundii / flowcrafter
PHP library for defining, executing, and monitoring message-driven workflows (state machines)
Requires
- php: >=8.2
- ext-pdo: *
- ext-pdo_sqlite: *
- dragonmantank/cron-expression: ^3.6
- ramsey/uuid: ^4.9.2
- symfony/config: ^7.4.8 | ^8.0.10
- symfony/console: ^7.4.8 | ^8.0.11
- symfony/dependency-injection: ^7.4.8 | ^8.0.10
- symfony/filesystem: ^7.4.8 | ^8.0.11
- symfony/finder: ^7.4.8 | ^8.0.8
- symfony/http-foundation: ^7.4.8 | ^8.0.8
- symfony/process: ^7.4.8 | ^8.0.11
- symfony/routing: ^7.4.8 | ^8.0.12
- wundii/data-mapper: ^1.5.1
Requires (Dev)
- ext-pdo_mysql: *
- ext-redis: *
- phpstan/phpstan: ^2.1.55
- phpunit/phpunit: ^11.5.55
- rector/rector: ^2.4.4
- symfony/var-dumper: ^7.4.8 | ^8.0.8
- symplify/easy-coding-standard: ^12.6.2
- testcontainers/testcontainers: ^1.0.4
- thenativeweb/eventsourcingdb: ^1.4.1
- wundii/phplint: ^0.3.4
Suggests
- ext-pdo_mysql: Required for the MySQL storage backend
- ext-redis: Required for the Redis storage backend
- thenativeweb/eventsourcingdb: Required for the ESDB storage backend
README
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
StorageInterfacefrei 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 devgestartet 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
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-->[*]: OrderCompletedLoading
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: 3bedeutet: 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
FlowExceptionpersistiert
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.