Skip to content

Commit f21bcef

Browse files
authored
Merge pull request #2 from wol-soft/CustomDebugFormatter
Add debug output formatter
2 parents 9b26f04 + dae0e7c commit f21bcef

26 files changed

Lines changed: 701 additions & 237 deletions

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

phpunit.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
<include>
1616
<directory>src</directory>
1717
</include>
18+
<exclude>
19+
<directory>src/State/ExecutionLog/OutputFormat</directory>
20+
</exclude>
1821
</coverage>
1922
<testsuite name="PHPWorkflow">
2023
<directory>tests</directory>

src/Stage/Next/AllowNextExecuteWorkflow.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use PHPWorkflow\Exception\WorkflowControl\SkipWorkflowException;
99
use PHPWorkflow\Exception\WorkflowException;
1010
use PHPWorkflow\State\ExecutionLog\ExecutionLog;
11+
use PHPWorkflow\State\ExecutionLog\Summary;
1112
use PHPWorkflow\State\WorkflowContainer;
1213
use PHPWorkflow\State\WorkflowResult;
1314
use PHPWorkflow\State\WorkflowState;
@@ -38,12 +39,12 @@ public function executeWorkflow(
3839

3940
$workflowState->getExecutionLog()->stopExecution();
4041
$workflowState->setStage(WorkflowState::STAGE_SUMMARY);
41-
$workflowState->addExecutionLog('Workflow execution');
42+
$workflowState->addExecutionLog(new Summary('Workflow execution'));
4243
} catch (Exception $exception) {
4344
$workflowState->getExecutionLog()->stopExecution();
4445
$workflowState->setStage(WorkflowState::STAGE_SUMMARY);
4546
$workflowState->addExecutionLog(
46-
'Workflow execution',
47+
new Summary('Workflow execution'),
4748
$exception instanceof SkipWorkflowException ? ExecutionLog::STATE_SKIPPED : ExecutionLog::STATE_FAILED,
4849
$exception->getMessage(),
4950
);
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PHPWorkflow\State\ExecutionLog;
6+
7+
/**
8+
* Interface Describable
9+
*
10+
* @package PHPWorkflow\State\ExecutionLog
11+
*/
12+
interface Describable
13+
{
14+
/**
15+
* Describe in a few words what this step does
16+
*/
17+
public function getDescription(): string;
18+
}

src/State/ExecutionLog/ExecutionLog.php

Lines changed: 14 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace PHPWorkflow\State\ExecutionLog;
66

7+
use PHPWorkflow\State\ExecutionLog\OutputFormat\OutputFormat;
78
use PHPWorkflow\State\WorkflowState;
89
use PHPWorkflow\Step\WorkflowStep;
910

@@ -15,7 +16,7 @@ class ExecutionLog
1516

1617
/** @var Step[][] */
1718
private array $stages = [];
18-
/** @var string[] Collect additional debug info concerning the current step */
19+
/** @var StepInfo[] Collect additional debug info concerning the current step */
1920
private array $stepInfo = [];
2021
/** @var string[][] Collect all warnings which occurred during the workflow execution */
2122
private array $warnings = [];
@@ -30,37 +31,25 @@ public function __construct(WorkflowState $workflowState)
3031
$this->workflowState = $workflowState;
3132
}
3233

33-
public function addStep(int $stage, string $step, string $state, ?string $reason): void {
34-
$stage = $this->mapStage($stage);
35-
34+
public function addStep(int $stage, Describable $step, string $state, ?string $reason): void {
3635
$this->stages[$stage][] = new Step($step, $state, $reason, $this->stepInfo, $this->warningsDuringStep);
3736
$this->stepInfo = [];
3837
$this->warningsDuringStep = 0;
3938
}
4039

41-
public function __toString(): string
40+
public function debug(OutputFormat $formatter)
4241
{
43-
$debug = "Process log for workflow '{$this->workflowState->getWorkflowName()}':\n";
44-
45-
foreach ($this->stages as $stage => $steps) {
46-
$debug .= "$stage:\n";
47-
48-
foreach ($steps as $step) {
49-
$debug .= ' - ' . $step . "\n";
50-
}
51-
}
52-
53-
return trim($debug);
42+
return $formatter->format($this->workflowState->getWorkflowName(), $this->stages);
5443
}
5544

56-
public function attachStepInfo(string $info): void
45+
public function attachStepInfo(string $info, array $context = []): void
5746
{
58-
$this->stepInfo[] = $info;
47+
$this->stepInfo[] = new StepInfo($info, $context);
5948
}
6049

6150
public function addWarning(string $message, bool $workflowReportWarning = false): void
6251
{
63-
$this->warnings[$this->mapStage($this->workflowState->getStage())][] = $message;
52+
$this->warnings[$this->workflowState->getStage()][] = $message;
6453

6554
if (!$workflowReportWarning) {
6655
$this->warningsDuringStep++;
@@ -74,11 +63,11 @@ public function startExecution(): void
7463

7564
public function stopExecution(): void
7665
{
77-
$this->attachStepInfo("Execution time: " . number_format(1000 * (microtime(true) - $this->startAt), 5) . 'ms');
66+
$this->attachStepInfo('Execution time: ' . number_format(1000 * (microtime(true) - $this->startAt), 5) . 'ms');
7867

7968
if ($this->warnings) {
8069
$warnings = sprintf(
81-
"Got %s warning%s during the execution:",
70+
'Got %s warning%s during the execution:',
8271
$amount = count($this->warnings, COUNT_RECURSIVE) - count($this->warnings),
8372
$amount > 1 ? 's' : '',
8473
);
@@ -87,7 +76,8 @@ public function stopExecution(): void
8776
$warnings .= implode(
8877
'',
8978
array_map(
90-
fn (string $warning): string => sprintf("\n %s: %s", $stage, $warning),
79+
fn (string $warning): string =>
80+
sprintf(PHP_EOL . ' %s: %s', self::mapStage($stage), $warning),
9181
$stageWarnings,
9282
),
9383
);
@@ -97,7 +87,7 @@ public function stopExecution(): void
9787
}
9888
}
9989

100-
private function mapStage(int $stage): string
90+
public static function mapStage(int $stage): string
10191
{
10292
switch ($stage) {
10393
case WorkflowState::STAGE_PREPARE: return 'Prepare';
@@ -107,7 +97,7 @@ private function mapStage(int $stage): string
10797
case WorkflowState::STAGE_ON_ERROR: return 'On Error';
10898
case WorkflowState::STAGE_ON_SUCCESS: return 'On Success';
10999
case WorkflowState::STAGE_AFTER: return 'After';
110-
case WorkflowState::STAGE_SUMMARY: return "\nSummary";
100+
case WorkflowState::STAGE_SUMMARY: return 'Summary';
111101
}
112102
}
113103

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+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PHPWorkflow\State\ExecutionLog\OutputFormat;
6+
7+
use PHPWorkflow\State\ExecutionLog\Step;
8+
9+
interface OutputFormat
10+
{
11+
/**
12+
* @param string $workflowName
13+
* @param Step[][] $steps Contains a list of the executed steps, grouped by the executed stages
14+
*
15+
* @return mixed
16+
*/
17+
public function format(string $workflowName, array $steps);
18+
}

0 commit comments

Comments
 (0)