Skip to content

Commit f1ca38c

Browse files
author
Enno Woortmann
committed
nested workflows and loops in graph output formatter
Add GraphViz output formatter indent loops in debug output
1 parent 1167ec1 commit f1ca38c

12 files changed

Lines changed: 411 additions & 192 deletions

File tree

README.md

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ Bonus: you will get an execution log for each executed workflow - if you want to
2323
* [Nested workflows](#Nested-workflows)
2424
* [Loops](#Loops)
2525
* [Error handling, logging and debugging](#Error-handling-logging-and-debugging)
26+
* [Custom output formatter](#Custom-output-formatter)
2627
* [Tests](#Tests)
2728

2829
## Installation
@@ -438,7 +439,7 @@ public function getWarnings(): array;
438439
// get the exception which caused the workflow to fail
439440
public function getException(): ?Exception;
440441
// get the debug execution log of the workflow
441-
public function debug(): string;
442+
public function debug(?OutputFormat $formatter = null);
442443
// access the container which was used for the workflow
443444
public function getContainer(): WorkflowContainer;
444445
// get the last executed step
@@ -455,7 +456,7 @@ The **debug** method provides an execution log including all processed steps wit
455456

456457
Some example outputs for our example workflow may look like the following.
457458

458-
### Successful execution
459+
#### Successful execution
459460

460461
```
461462
Process log for workflow 'AddSongToPlaylist':
@@ -481,7 +482,7 @@ Summary:
481482

482483
Note the additional data added to the debug log for the **Process** stage and the **NotifySubscribers** step via the **attachStepInfo** method of the **WorkflowControl**.
483484

484-
### Failed workflow
485+
#### Failed workflow
485486

486487
```
487488
Process log for workflow 'AddSongToPlaylist':
@@ -495,7 +496,7 @@ Summary:
495496

496497
In this example the **CurrentUserIsAllowedToEditPlaylistValidator** step threw an exception with the message `playlist locked`.
497498

498-
### Workflow skipped
499+
#### Workflow skipped
499500

500501
```
501502
Process log for workflow 'AddSongToPlaylist':
@@ -513,6 +514,18 @@ Summary:
513514
In this example the **AcceptOpenSuggestionForSong** step found a matching open suggestion and successfully accepted the suggestion.
514515
Consequently, the further workflow execution is skipped.
515516

517+
### Custom-output-formatter
518+
519+
The output of the `debug` method can be controlled via an implementation of the `OutputFormat` interface.
520+
By default a string representation of the execution will be returned (just like the example outputs).
521+
522+
Currently the following additional formatters are implemented:
523+
524+
| Formatter | Description |
525+
| --------------- | ------------- |
526+
| `StringLog` | The default formatter. Creates a string representation. <br />Example:<br />`$result->debug();` |
527+
| `WorkflowGraph` | Creates a SVG file containing a graph which represents the workflow execution. The generated image will be stored in the provided target directory. Requires `dot` executable.<br />Example:<br />`$result->debug(new WorkflowGraph('/var/log/workflow/graph'));` |
528+
| `GraphViz` | Returns a string containing [GraphViz](https://graphviz.org/) code for a graph representing the workflow execution. <br />Example:<br />`$result->debug(new GraphViz());`|
516529

517530
## Tests ##
518531

src/State/ExecutionLog/ExecutionLog.php

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,6 @@ public function __construct(WorkflowState $workflowState)
3232
}
3333

3434
public function addStep(int $stage, Describable $step, string $state, ?string $reason): void {
35-
$stage = $this->mapStage($stage);
36-
3735
$this->stages[$stage][] = new Step($step, $state, $reason, $this->stepInfo, $this->warningsDuringStep);
3836
$this->stepInfo = [];
3937
$this->warningsDuringStep = 0;
@@ -51,7 +49,7 @@ public function attachStepInfo(string $info, array $context = []): void
5149

5250
public function addWarning(string $message, bool $workflowReportWarning = false): void
5351
{
54-
$this->warnings[$this->mapStage($this->workflowState->getStage())][] = $message;
52+
$this->warnings[$this->workflowState->getStage()][] = $message;
5553

5654
if (!$workflowReportWarning) {
5755
$this->warningsDuringStep++;
@@ -78,7 +76,8 @@ public function stopExecution(): void
7876
$warnings .= implode(
7977
'',
8078
array_map(
81-
fn (string $warning): string => sprintf(PHP_EOL . ' %s: %s', $stage, $warning),
79+
fn (string $warning): string =>
80+
sprintf(PHP_EOL . ' %s: %s', self::mapStage($stage), $warning),
8281
$stageWarnings,
8382
),
8483
);
@@ -88,7 +87,7 @@ public function stopExecution(): void
8887
}
8988
}
9089

91-
private function mapStage(int $stage): string
90+
public static function mapStage(int $stage): string
9291
{
9392
switch ($stage) {
9493
case WorkflowState::STAGE_PREPARE: return 'Prepare';
@@ -98,7 +97,7 @@ private function mapStage(int $stage): string
9897
case WorkflowState::STAGE_ON_ERROR: return 'On Error';
9998
case WorkflowState::STAGE_ON_SUCCESS: return 'On Success';
10099
case WorkflowState::STAGE_AFTER: return 'After';
101-
case WorkflowState::STAGE_SUMMARY: return PHP_EOL . 'Summary';
100+
case WorkflowState::STAGE_SUMMARY: return 'Summary';
102101
}
103102
}
104103

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PHPWorkflow\State\ExecutionLog\OutputFormat;
6+
7+
use PHPWorkflow\State\ExecutionLog\ExecutionLog;
8+
use PHPWorkflow\State\ExecutionLog\Step;
9+
use PHPWorkflow\State\ExecutionLog\StepInfo;
10+
use PHPWorkflow\State\WorkflowResult;
11+
12+
class GraphViz implements OutputFormat
13+
{
14+
private static int $stepIndex = 0;
15+
private static int $clusterIndex = 0;
16+
17+
private static int $loopIndex = 0;
18+
private static array $loopInitialElement = [];
19+
private static array $loopLinks = [];
20+
21+
public function format(string $workflowName, array $steps): string
22+
{
23+
$dotScript = "digraph \"$workflowName\" {\n";
24+
25+
$dotScript .= $this->renderWorkflowGraph($workflowName, $steps);
26+
27+
for ($i = 0; $i < self::$stepIndex - 1; $i++) {
28+
if (isset(self::$loopLinks[$i + 1])) {
29+
continue;
30+
}
31+
32+
$dotScript .= sprintf(" %s -> %s\n", $i, $i + 1);
33+
}
34+
35+
foreach (self::$loopLinks as $loopElement => $loopRoot) {
36+
$dotScript .= sprintf(" %s -> %s\n", $loopRoot, $loopElement);
37+
}
38+
39+
$dotScript .= '}';
40+
41+
return $dotScript;
42+
}
43+
44+
private function renderWorkflowGraph(string $workflowName, array $steps): string
45+
{
46+
$dotScript = sprintf(" %s [label=\"$workflowName\"]\n", self::$stepIndex++);
47+
foreach ($steps as $stage => $stageSteps) {
48+
$dotScript .= sprintf(
49+
" subgraph cluster_%s {\n label = \"%s\"\n",
50+
self::$clusterIndex++,
51+
ExecutionLog::mapStage($stage)
52+
);
53+
54+
/** @var Step $step */
55+
foreach ($stageSteps as $step) {
56+
foreach ($step->getStepInfo() as $info) {
57+
switch ($info->getInfo()) {
58+
case StepInfo::LOOP_START:
59+
$dotScript .= sprintf(
60+
" subgraph cluster_loop_%s {\n label = \"Loop\"\n",
61+
self::$clusterIndex++
62+
);
63+
64+
self::$loopInitialElement[++self::$loopIndex] = self::$stepIndex;
65+
66+
continue 2;
67+
case StepInfo::LOOP_ITERATION:
68+
self::$loopLinks[self::$stepIndex + 1] = self::$loopInitialElement[self::$loopIndex];
69+
70+
continue 2;
71+
case StepInfo::LOOP_END:
72+
$dotScript .= "\n}\n";
73+
array_pop(self::$loopLinks);
74+
self::$loopIndex--;
75+
76+
continue 2;
77+
case StepInfo::NESTED_WORKFLOW:
78+
/** @var WorkflowResult $nestedWorkflowResult */
79+
$nestedWorkflowResult = $info->getContext()['result'];
80+
$nestedWorkflowGraph = $nestedWorkflowResult->debug($this);
81+
82+
$lines = explode("\n", $nestedWorkflowGraph);
83+
array_shift($lines);
84+
array_pop($lines);
85+
86+
$dotScript .=
87+
sprintf(
88+
" subgraph cluster_%s {\n label = \"Nested workflow\"\n",
89+
self::$clusterIndex++,
90+
)
91+
. preg_replace('/\d+ -> \d+\s*/m', '', join("\n", $lines))
92+
. "\n}\n";
93+
94+
// TODO: additional infos. Currently skipped
95+
continue 3;
96+
}
97+
}
98+
99+
$dotScript .= sprintf(
100+
' %s [label=%s shape="box" color="%s"]' . "\n",
101+
self::$stepIndex++,
102+
"<{$step->getDescription()} ({$step->getState()})"
103+
. ($step->getReason() ? "<BR/><FONT POINT-SIZE=\"10\">{$step->getReason()}</FONT>" : '')
104+
. join('', array_map(
105+
fn (StepInfo $info): string => "<BR/><FONT POINT-SIZE=\"10\">{$info->getInfo()}</FONT>",
106+
array_filter(
107+
$step->getStepInfo(),
108+
fn (StepInfo $info): bool => !in_array(
109+
$info->getInfo(),
110+
[
111+
StepInfo::LOOP_START,
112+
StepInfo::LOOP_ITERATION,
113+
StepInfo::LOOP_END,
114+
StepInfo::NESTED_WORKFLOW,
115+
],
116+
)
117+
),
118+
))
119+
. ">",
120+
$this->mapColor($step),
121+
);
122+
}
123+
$dotScript .= " }\n";
124+
}
125+
126+
return $dotScript;
127+
}
128+
129+
private function mapColor(Step $step): string
130+
{
131+
if ($step->getState() === ExecutionLog::STATE_SUCCESS && $step->getWarnings()) {
132+
return 'yellow';
133+
}
134+
135+
return [
136+
ExecutionLog::STATE_SUCCESS => 'green',
137+
ExecutionLog::STATE_SKIPPED => 'grey',
138+
ExecutionLog::STATE_FAILED => 'red',
139+
][$step->getState()];
140+
}
141+
}

src/State/ExecutionLog/OutputFormat/StringLog.php

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,30 @@
44

55
namespace PHPWorkflow\State\ExecutionLog\OutputFormat;
66

7+
use PHPWorkflow\State\ExecutionLog\ExecutionLog;
78
use PHPWorkflow\State\ExecutionLog\Step;
89
use PHPWorkflow\State\ExecutionLog\StepInfo;
910
use PHPWorkflow\State\WorkflowResult;
11+
use PHPWorkflow\State\WorkflowState;
1012

1113
class StringLog implements OutputFormat
1214
{
15+
private string $indentation = '';
16+
1317
public function format(string $workflowName, array $steps): string
1418
{
1519
$debug = "Process log for workflow '$workflowName':" . PHP_EOL;
1620

1721
foreach ($steps as $stage => $stageSteps) {
18-
$debug .= "$stage:" . PHP_EOL;
22+
$debug .= sprintf(
23+
'%s%s%s:' . PHP_EOL,
24+
$stage === WorkflowState::STAGE_SUMMARY ? PHP_EOL : '',
25+
$this->indentation,
26+
ExecutionLog::mapStage($stage)
27+
);
1928

2029
foreach ($stageSteps as $step) {
21-
$debug .= ' - ' . $this->formatStep($step) . PHP_EOL;
30+
$debug .= "{$this->indentation} - " . $this->formatStep($step) . PHP_EOL;
2231
}
2332
}
2433

@@ -35,25 +44,40 @@ private function formatStep(Step $step): string
3544
);
3645

3746
foreach ($step->getStepInfo() as $info) {
38-
$stepLog .= PHP_EOL . " - " . $this->formatInfo($info);
47+
$formattedInfo = $this->formatInfo($info);
48+
49+
if ($formattedInfo) {
50+
$stepLog .= PHP_EOL . $formattedInfo;
51+
}
3952
}
4053

4154
return $stepLog;
4255
}
4356

44-
private function formatInfo(StepInfo $info): string
57+
private function formatInfo(StepInfo $info): ?string
4558
{
4659
switch ($info->getInfo()) {
4760
case StepInfo::NESTED_WORKFLOW:
4861
/** @var WorkflowResult $nestedWorkflowResult */
4962
$nestedWorkflowResult = $info->getContext()['result'];
5063

51-
return str_replace(
64+
return "$this->indentation - " . str_replace(
5265
PHP_EOL . ' ' . PHP_EOL,
5366
PHP_EOL . PHP_EOL,
54-
str_replace(PHP_EOL, PHP_EOL . ' ', $nestedWorkflowResult->debug($this)),
67+
str_replace(PHP_EOL, PHP_EOL . ' ', $nestedWorkflowResult->debug($this))
5568
);
56-
default: return $info->getInfo();
69+
case StepInfo::LOOP_START:
70+
$this->indentation .= ' ';
71+
72+
return null;
73+
case StepInfo::LOOP_ITERATION:
74+
return null;
75+
case StepInfo::LOOP_END:
76+
$this->indentation = substr($this->indentation, 0, -2);
77+
$iterations = $info->getContext()['iterations'];
78+
79+
return " - Loop finished after $iterations iteration" . ($iterations === 1 ? '' : 's');
80+
default: return "{$this->indentation} - " . $info->getInfo();
5781
}
5882
}
5983
}

src/State/ExecutionLog/OutputFormat/WorkflowGraph.php

Lines changed: 1 addition & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,6 @@
55
namespace PHPWorkflow\State\ExecutionLog\OutputFormat;
66

77
use Exception;
8-
use PHPWorkflow\State\ExecutionLog\ExecutionLog;
9-
use PHPWorkflow\State\ExecutionLog\Step;
10-
use PHPWorkflow\State\ExecutionLog\StepInfo;
118

129
class WorkflowGraph implements OutputFormat
1310
{
@@ -20,38 +17,8 @@ public function __construct(string $path)
2017

2118
public function format(string $workflowName, array $steps): string
2219
{
23-
$dotScript = "digraph \"$workflowName\" {\n";
24-
$stepIndex = 0;
25-
$stageIndex = 0;
26-
27-
$dotScript .= sprintf(" %s [label=\"$workflowName\"]\n", $stepIndex++);
28-
foreach ($steps as $stage => $stageSteps) {
29-
$dotScript .= sprintf(" subgraph cluster_%s {\n label = $stage", $stageIndex++);
30-
/** @var Step $step */
31-
foreach ($stageSteps as $step) {
32-
$dotScript .= sprintf(
33-
' %s [label=%s shape="box" color="%s"]' . "\n",
34-
$stepIndex++,
35-
"<{$step->getDescription()} ({$step->getState()})"
36-
. ($step->getReason() ? "<BR/><FONT POINT-SIZE=\"10\">{$step->getReason()}</FONT>" : '')
37-
. join('', array_map(
38-
fn (StepInfo $info): string => "<BR/><FONT POINT-SIZE=\"10\">{$info->getInfo()}</FONT>",
39-
$step->getStepInfo(),
40-
))
41-
. ">",
42-
$this->mapColor($step),
43-
);
44-
}
45-
$dotScript .= " }\n";
46-
}
47-
48-
for ($i = 0; $i < $stepIndex - 1; $i++) {
49-
$dotScript .= sprintf(" %s -> %s\n", $i, $i + 1);
50-
}
51-
$dotScript .= '}';
52-
5320
$this->generateImageFromScript(
54-
$dotScript,
21+
(new GraphViz())->format($workflowName, $steps),
5522
$filePath = $this->path . DIRECTORY_SEPARATOR . $workflowName . '_' . uniqid() . '.svg',
5623
);
5724

@@ -79,17 +46,4 @@ private function generateImageFromScript(string $script, string $file)
7946

8047
unlink($tmp);
8148
}
82-
83-
private function mapColor(Step $step): string
84-
{
85-
if ($step->getState() === ExecutionLog::STATE_SUCCESS && $step->getWarnings()) {
86-
return 'yellow';
87-
}
88-
89-
return [
90-
ExecutionLog::STATE_SUCCESS => 'green',
91-
ExecutionLog::STATE_SKIPPED => 'grey',
92-
ExecutionLog::STATE_FAILED => 'red',
93-
][$step->getState()];
94-
}
9549
}

0 commit comments

Comments
 (0)