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.8
- symfony/console: ^7.4.8 | ^8.0.8
- symfony/dependency-injection: ^7.4.8 | ^8.0.8
- symfony/filesystem: ^7.4.8 | ^8.0.8
- symfony/http-foundation: ^7.4.8 | ^8.0.8
- symfony/process: ^7.4.8 | ^8.0.8
- symfony/routing: ^7.4.8 | ^8.0.8
- wundii/data-mapper: ^1.5.0
Requires (Dev)
- ext-pdo_mysql: *
- ext-redis: *
- phpstan/phpstan: ^2.1.46
- phpunit/phpunit: ^11.5.55
- rector/rector: ^2.4.1
- 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, includeStubs, Observer |
| Monitoring | Prometheus / OpenMetrics, CheckMK |
| REST-API | Endpunkte, Pagination, Auth |
| Testing | Flows & Stubs 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
Minimalbeispiel
Messages
readonly Value-Objects. Drei Typen: Init startet den Flow, Data fließt zwischen Stubs, 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 Stub 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 StartStub implements StubInterface { 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); } }
Stubs
reine PHP-Klassen. Der Constructor-Typ entscheidet das Routing. Ein Stub 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\StubInterface; // Zwischenschritt: Init → Data class ValidateStub implements StubInterface { 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 CompleteOrderStub implements StubInterface { 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-Stub: Data → bool (FlowResult, kein Weiterleiten) class AuditStub implements StubInterface { 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 Stubs 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->addStub(ValidateStub::class); $builder->addStub(CompleteOrderStub::class); $builder->addStub(AuditStub::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 [*]-->ValidateStub: OrderInit ValidateStub-->CompleteOrderStub: OrderValidated ValidateStub-->AuditStub: OrderValidated CompleteOrderStub-->[*]: OrderCompletedLoading
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/flows/run (synchron) bzw. POST /api/queue (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.
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->assertStubExecuted(ValidateStub::class); $this->assertStubExecuted(CompleteOrderStub::class); $this->assertStubExecuted(AuditStub::class); $this->assertFlowHasMessage(OrderValidated::class); $this->assertFlowBoolResult(true); // AuditStub 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.