name: platform-workflow
description: Workflow-Engine API used by Process-Designer-generated Use-Case orchestrators — seven routing statuses (ON_SUCCESS / ON_FAIL / ON_TIMEOUT / ON_SKIP / ON_CANCEL / ON_EVENT / ON_EXIT), WorkflowConfig/addNode graph construction, the Event-Kasten ◇ node variant, handlerFactory Closure conventions, three opaque WorkflowContext slots (reference, response, exception), R5 routing-safety rules.
zone: post-active
persona: C
prerequisites: [platform-implementation]
next: []
The Process-Designer-Orchestrator (<Name>Handler.php, generated under {BC}/Process/{Name}/) wires a WorkflowConfig and calls $workflow($config, $dto); the Engine walks the graph, invokes each Node-Action-Stub __invoke(WorkflowContextInterface): WorkflowResultInterface, and stamps every result with its producing handler FQCN. Knowing this API is mandatory both for Node bodies (which return a WorkflowResult) and for hand-written Orchestrator-Mantel-Code.
1. The seven routing statuses
A Node returns exactly one of these via new WorkflowResult(WorkflowResult::ON_*, $payload):
| Konstante | Bedeutung |
|---|---|
ON_SUCCESS |
Erfolgreicher Abschluss des Handlers — Default-Happy-Path. |
ON_FAIL |
Fachlicher Misserfolg (Validierung, Geschaeftsregel verletzt). |
ON_TIMEOUT |
Geplanter Recovery-Pfad: Service-Side-Timeout in fachliches Routing uebersetzt. |
ON_SKIP |
Handler nicht anwendbar — Flow ueberspringt zum Re-Konvergenz-Punkt. |
ON_CANCEL |
Fachlicher Abbruch (Stornierung, Zustimmung zurueckgezogen) — Cleanup-Pfad. |
ON_EVENT |
Aktiver async-Hand-off via DomainEvent — Folge-Runs entstehen extern (kein synchroner Folge-Node). |
ON_EXIT |
Schleifen-/Block-Terminierung — beendet eine Schleife bzw. einen Block aktiv (kein synchroner Folge-Node). |
User-Code laesst handlerFqcn immer null — die Engine stamped es via WorkflowResult::withHandler() selbst, sobald sie das Ergebnis in den Context anhaengt.
2. WorkflowConfig im config()-Body
Der Generator emittiert eine private config(): WorkflowConfigInterface, die jeden Knoten des gemalten Graphen via addNode() registriert (Start zuerst, in topologischer Reihenfolge; ein End-Knoten als addNode(X::class, [])). Routing ist eine [WorkflowResult::ON_* => NextNode::class]-Map pro Knoten — die Engine laeuft die gemalten Kanten, exklusive Zweige laufen exklusiv. Hand-edits am Orchestrator (z.B. zusaetzliche Transition) folgen demselben Muster:
private function config(): WorkflowConfigInterface
{
return (new WorkflowConfig())
->addNode(ValidateInput::class, [
WorkflowResult::ON_SUCCESS => LoadAggregate::class,
WorkflowResult::ON_FAIL => RejectInput::class,
])
->addNode(LoadAggregate::class, [
WorkflowResult::ON_SUCCESS => MutateState::class,
WorkflowResult::ON_SKIP => RejectInput::class,
])
->addNode(MutateState::class, [
WorkflowResult::ON_SUCCESS => PersistAndEmit::class,
WorkflowResult::ON_CANCEL => CompensateState::class,
])
->addNode(PersistAndEmit::class, [])
->addNode(RejectInput::class, [])
->addNode(CompensateState::class, []);
}
End-Knoten (leere Routing-Map []) lassen die Engine ordentlich beenden.
Event-Kasten ◇ (Event-Knoten)
Ein Designer-Knoten kann statt Action als Event ◇ markiert sein (type: event). Der Generator erzeugt dann zweierlei:
- eine readonly Event-Daten-Klasse unter
{BC}/Process/{Name}/Event/<EventClass>.php(hermetisch, ForceOverwrite — der Knotenname ist der Event-Klassenname); - einen Knoten mit derselben
__invoke(WorkflowContextInterface): WorkflowResultInterface-Signatur wie jeder Action-Knoten, dessen Body aber bereits vorgefuellt ist:__invoke()delegiert anlogic(), undlogic()legt das neue Event in dendata-Frachtraum unterEventScope::Domain->value:
protected function logic(FlagReading $cmd, WorkflowContextInterface $context): array
{
return ['status' => WorkflowResult::ON_SUCCESS, 'data' => [
EventScope::Domain->value => [
new ReadingFlagged(kennung: $this->kennung($cmd), occurredAt: new \DateTimeImmutable()),
],
]];
}
protected function kennung(FlagReading $cmd): int|string
{
throw new \RuntimeException('Kennung fuellen: ' . self::class); // <- der Dev fuellt das
}
Der Orchestrator erntet alle so abgelegten Domain-Events nach dem Lauf aus der Kette (getChain()) und haengt sie via addEvent(…, EventScope::Domain) an die Response. Die einzige Dev-Aufgabe am Event-Knoten ist, kennung() (und ggf. weitere Event-Felder) zu fuellen — der throw ist der „noch nicht gefuellt"-Waechter, kein echter Fehlerpfad. Publikation nach Commit ist Sache des Aufrufers (Event-Transport-Rezepte: platform-cookbook §1).
3. handlerFactory-Closure
new Workflow($factory) akzeptiert optional Closure(string $fqcn, mixed $data): object. Die Konvention im Aggregat-Kontext ist fn($cls, $data) => $this->context($cls, $data), sodass jeder Node eine frische BC mit $data als Payload bekommt — Nodes lesen es via $this->payload(). Bei $data === null ist $this->handle($cls) der Default, und der aeussere Payload bleibt erhalten. Ohne Factory ruft die Engine new $fqcn() ($data ignoriert) — fuer puren PHP-Code ausserhalb des Aggregat-Kontexts brauchbar.
4. Drei opake Context-Slots
WorkflowContext traegt neben dem Result-Chain drei freie Slots, die nicht vom Routing inspiziert werden:
| Slot | Getter / Setter | Verwendung |
|---|---|---|
reference |
reference() / setReference(mixed) |
Out-of-Band-Kanal Orchestrator → Node (z.B. vor-aufgeloeste Aggregat-Identitaet weiterreichen). |
response |
response() / setResponse(mixed) |
Out-of-Band-Kanal Node → Orchestrator (z.B. DomainResponse aus dem End-Node ablegen, statt ueber WorkflowResult.data). |
exception |
getException() / setException(\Throwable) |
Ein gefangenes Throwable einsteuern, ohne den Engine-Loop zu unterbrechen — Cleanup-Nodes koennen es auslesen und in die Response-Mapping-Schicht ueberfuehren. |
Slots sind explizit nicht fuer Daten gedacht, die zwischen sequentiellen Nodes fliessen — dafuer dienen WorkflowResult.data und WorkflowContext::getPrevious() / getLatest($fqcn) / getAll($fqcn) / getChain().
5. R5 — Routing-Safety
Die Engine bricht ohne Throw ab, wenn
- der aktuelle Handler keinerlei Transitions konfiguriert hat, oder
- fuer den zurueckgegebenen Status keine Transition existiert, oder
- das konfigurierte Transition-Target nicht selbst per
addNode()registriert ist.
In allen drei Faellen erhaelt der Aufrufer den vollstaendigen WorkflowContext zurueck; die Verantwortung fuer "war das jetzt ein gewolltes Ende oder ein Konfigurationsfehler?" liegt beim Orchestrator-Mantel (typisch: try/catch + Pruefung von $context->getException() und $context->getPrevious()).
Anchors
platform-implementation(Generated baseline, override targets, decision tree).support-workflow(the engine implementation itself).