Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 0634766

Browse filesBrowse files
committed
feature #40171 [Workflow] Add Mermaid.js dumper (eFrane)
This PR was squashed before being merged into the 5.3-dev branch. Discussion ---------- [Workflow] Add Mermaid.js dumper | Q | A | ------------- | --- | Branch? | 5.x | Bug fix? | no | New feature? | yes | Deprecations? | no | Tickets | #40165 | License | MIT | Doc PR | symfony/symfony-docs#15102 Mermaid is - next to PlantUML - one of the most popular simple graphing solutions. This workflow dumper mirrors the feature set of the PlantUML dumper except that Mermaid does not currently support colored transitions. **Things I need help with:** - ~I basically tried to copy the code style of the surrounding files and hope everything is conforming. Please let me know if I missed something.~ I see, that's the magic of fabbot. Nice. ❤️ - There are currently no tests for the different graph direction constants, I can add those, just did not see value in doing so yet. - I am unsure how to integrate this with the current documentation. This however is likely better discussed in the corresponding issue (see above). Commits ------- ada6f7d315 [Workflow] Add Mermaid.js dumper
2 parents ab19cbc + 1a6d598 commit 0634766
Copy full SHA for 0634766

File tree

Expand file treeCollapse file tree

3 files changed

+515
-0
lines changed
Filter options
Expand file treeCollapse file tree

3 files changed

+515
-0
lines changed

‎CHANGELOG.md

Copy file name to clipboardExpand all lines: CHANGELOG.md
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ CHANGELOG
55
---
66

77
* Deprecate `InvalidTokenConfigurationException`
8+
* Added `MermaidDumper` to dump Workflow graphs in the Mermaid.js flowchart format
89

910
5.2.0
1011
-----

‎Dumper/MermaidDumper.php

Copy file name to clipboard
+288Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Workflow\Dumper;
13+
14+
use Symfony\Component\Workflow\Definition;
15+
use Symfony\Component\Workflow\Exception\InvalidArgumentException;
16+
use Symfony\Component\Workflow\Marking;
17+
18+
class MermaidDumper implements DumperInterface
19+
{
20+
public const DIRECTION_TOP_TO_BOTTOM = 'TB';
21+
public const DIRECTION_TOP_DOWN = 'TD';
22+
public const DIRECTION_BOTTOM_TO_TOP = 'BT';
23+
public const DIRECTION_RIGHT_TO_LEFT = 'RL';
24+
public const DIRECTION_LEFT_TO_RIGHT = 'LR';
25+
26+
private const VALID_DIRECTIONS = [
27+
self::DIRECTION_TOP_TO_BOTTOM,
28+
self::DIRECTION_TOP_DOWN,
29+
self::DIRECTION_BOTTOM_TO_TOP,
30+
self::DIRECTION_RIGHT_TO_LEFT,
31+
self::DIRECTION_LEFT_TO_RIGHT,
32+
];
33+
34+
public const TRANSITION_TYPE_STATEMACHINE = 'statemachine';
35+
public const TRANSITION_TYPE_WORKFLOW = 'workflow';
36+
37+
private const VALID_TRANSITION_TYPES = [
38+
self::TRANSITION_TYPE_STATEMACHINE,
39+
self::TRANSITION_TYPE_WORKFLOW,
40+
];
41+
42+
/**
43+
* @var string
44+
*/
45+
private $direction;
46+
47+
/**
48+
* @var string
49+
*/
50+
private $transitionType;
51+
52+
/**
53+
* Just tracking the transition id is in some cases inaccurate to
54+
* get the link's number for styling purposes.
55+
*
56+
* @var int
57+
*/
58+
private $linkCount;
59+
60+
public function __construct(string $transitionType, string $direction = self::DIRECTION_LEFT_TO_RIGHT)
61+
{
62+
$this->validateDirection($direction);
63+
$this->validateTransitionType($transitionType);
64+
65+
$this->direction = $direction;
66+
$this->transitionType = $transitionType;
67+
}
68+
69+
public function dump(Definition $definition, Marking $marking = null, array $options = []): string
70+
{
71+
$this->linkCount = 0;
72+
$placeNameMap = [];
73+
$placeId = 0;
74+
75+
$output = ['graph '.$this->direction];
76+
77+
$meta = $definition->getMetadataStore();
78+
79+
foreach ($definition->getPlaces() as $place) {
80+
[$placeNode, $placeStyle] = $this->preparePlace(
81+
$placeId,
82+
$place,
83+
$meta->getPlaceMetadata($place),
84+
\in_array($place, $definition->getInitialPlaces()),
85+
null !== $marking && $marking->has($place)
86+
);
87+
88+
$output[] = $placeNode;
89+
90+
if ('' !== $placeStyle) {
91+
$output[] = $placeStyle;
92+
}
93+
94+
$placeNameMap[$place] = $place.$placeId;
95+
96+
++$placeId;
97+
}
98+
99+
foreach ($definition->getTransitions() as $transitionId => $transition) {
100+
$transitionMeta = $meta->getTransitionMetadata($transition);
101+
102+
$transitionLabel = $transition->getName();
103+
if (\array_key_exists('label', $transitionMeta)) {
104+
$transitionLabel = $transitionMeta['label'];
105+
}
106+
107+
foreach ($transition->getFroms() as $from) {
108+
$from = $placeNameMap[$from];
109+
110+
foreach ($transition->getTos() as $to) {
111+
$to = $placeNameMap[$to];
112+
113+
if (self::TRANSITION_TYPE_STATEMACHINE === $this->transitionType) {
114+
$transitionOutput = $this->styleStatemachineTransition(
115+
$from,
116+
$to,
117+
$transitionId,
118+
$transitionLabel,
119+
$transitionMeta
120+
);
121+
} else {
122+
$transitionOutput = $this->styleWorkflowTransition(
123+
$from,
124+
$to,
125+
$transitionId,
126+
$transitionLabel,
127+
$transitionMeta
128+
);
129+
}
130+
131+
foreach ($transitionOutput as $line) {
132+
if (\in_array($line, $output)) {
133+
// additional links must be decremented again to align the styling
134+
if (0 < strpos($line, '-->')) {
135+
--$this->linkCount;
136+
}
137+
138+
continue;
139+
}
140+
141+
$output[] = $line;
142+
}
143+
}
144+
}
145+
}
146+
147+
return implode("\n", $output);
148+
}
149+
150+
private function preparePlace(int $placeId, string $placeName, array $meta, bool $isInitial, bool $hasMarking): array
151+
{
152+
$placeLabel = $placeName;
153+
if (\array_key_exists('label', $meta)) {
154+
$placeLabel = $meta['label'];
155+
}
156+
157+
$placeLabel = $this->escape($placeLabel);
158+
159+
$labelShape = '((%s))';
160+
if ($isInitial) {
161+
$labelShape = '([%s])';
162+
}
163+
164+
$placeNodeName = $placeName.$placeId;
165+
$placeNodeFormat = '%s'.$labelShape;
166+
$placeNode = sprintf($placeNodeFormat, $placeNodeName, $placeLabel);
167+
168+
$placeStyle = $this->styleNode($meta, $placeNodeName, $hasMarking);
169+
170+
return [$placeNode, $placeStyle];
171+
}
172+
173+
private function styleNode(array $meta, string $nodeName, bool $hasMarking = false): string
174+
{
175+
$nodeStyles = [];
176+
177+
if (\array_key_exists('bg_color', $meta)) {
178+
$nodeStyles[] = sprintf(
179+
'fill:%s',
180+
$meta['bg_color']
181+
);
182+
}
183+
184+
if ($hasMarking) {
185+
$nodeStyles[] = 'stroke-width:4px';
186+
}
187+
188+
if (0 === \count($nodeStyles)) {
189+
return '';
190+
}
191+
192+
return sprintf('style %s %s', $nodeName, implode(',', $nodeStyles));
193+
}
194+
195+
/**
196+
* Replace double quotes with the mermaid escape syntax and
197+
* ensure all other characters are properly escaped.
198+
*/
199+
private function escape(string $label)
200+
{
201+
$label = str_replace('"', '#quot;', $label);
202+
203+
return sprintf('"%s"', $label);
204+
}
205+
206+
public function validateDirection(string $direction): void
207+
{
208+
if (!\in_array($direction, self::VALID_DIRECTIONS, true)) {
209+
throw new InvalidArgumentException(sprintf('Direction "%s" is not valid, valid directions are: "%s".', $direction, implode(', ', self::VALID_DIRECTIONS)));
210+
}
211+
}
212+
213+
private function validateTransitionType(string $transitionType): void
214+
{
215+
if (!\in_array($transitionType, self::VALID_TRANSITION_TYPES, true)) {
216+
throw new InvalidArgumentException(sprintf('Transition type "%s" is not valid, valid types are: "%s".', $transitionType, implode(', ', self::VALID_TRANSITION_TYPES)));
217+
}
218+
}
219+
220+
private function styleStatemachineTransition(
221+
string $from,
222+
string $to,
223+
int $transitionId,
224+
string $transitionLabel,
225+
array $transitionMeta
226+
): array {
227+
$transitionOutput = [sprintf('%s-->|%s|%s', $from, $this->escape($transitionLabel), $to)];
228+
229+
$linkStyle = $this->styleLink($transitionMeta);
230+
if ('' !== $linkStyle) {
231+
$transitionOutput[] = $linkStyle;
232+
}
233+
234+
++$this->linkCount;
235+
236+
return $transitionOutput;
237+
}
238+
239+
private function styleWorkflowTransition(
240+
string $from,
241+
string $to,
242+
int $transitionId,
243+
string $transitionLabel,
244+
array $transitionMeta
245+
) {
246+
$transitionOutput = [];
247+
248+
$transitionLabel = $this->escape($transitionLabel);
249+
$transitionNodeName = 'transition'.$transitionId;
250+
251+
$transitionOutput[] = sprintf('%s[%s]', $transitionNodeName, $transitionLabel);
252+
253+
$transitionNodeStyle = $this->styleNode($transitionMeta, $transitionNodeName);
254+
if ('' !== $transitionNodeStyle) {
255+
$transitionOutput[] = $transitionNodeStyle;
256+
}
257+
258+
$connectionStyle = '%s-->%s';
259+
$transitionOutput[] = sprintf($connectionStyle, $from, $transitionNodeName);
260+
261+
$linkStyle = $this->styleLink($transitionMeta);
262+
if ('' !== $linkStyle) {
263+
$transitionOutput[] = $linkStyle;
264+
}
265+
266+
++$this->linkCount;
267+
268+
$transitionOutput[] = sprintf($connectionStyle, $transitionNodeName, $to);
269+
270+
$linkStyle = $this->styleLink($transitionMeta);
271+
if ('' !== $linkStyle) {
272+
$transitionOutput[] = $linkStyle;
273+
}
274+
275+
++$this->linkCount;
276+
277+
return $transitionOutput;
278+
}
279+
280+
private function styleLink(array $transitionMeta): string
281+
{
282+
if (\array_key_exists('color', $transitionMeta)) {
283+
return sprintf('linkStyle %d stroke:%s', $this->linkCount, $transitionMeta['color']);
284+
}
285+
286+
return '';
287+
}
288+
}

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.