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: 45

Dependents: 0

Suggesters: 0

Stars: 1

Open Issues: 0

0.20.1 2026-04-12 09:39 UTC

This package is auto-updated.

Last update: 2026-04-12 09:45:22 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, 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

wundii/flowcrafter

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-->[*]: OrderCompleted
Loading

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.