Skip to content

Commit 11e8902

Browse files
feature #60201 [Workflow] Add support for weighted transitions (lyrixx)
This PR was merged into the 7.4 branch. Discussion ---------- [Workflow] Add support for weighted transitions | Q | A | ------------- | --- | Branch? | 7.4 | Bug fix? | no | New feature? | yes | Deprecations? | yes | Issues | Fix #60107 | License | MIT Allow to handle complex workflow like: ```yaml make_table: transitions: start: from: init to: - place: prepare_leg weight: 4 - place: prepare_top weight: 1 - place: stopwatch_running weight: 1 build_leg: from: prepare_leg to: leg_created build_top: from: prepare_top to: top_created join: from: - place: leg_created weight: 4 - top_created - stopwatch_running to: finished ``` ```php $definition = new Definition( [], [ new Transition('start', 'init', [new Arc('prepare_leg', 4), 'prepare_top', 'stopwatch_running']), new Transition('build_leg', 'prepare_leg', 'leg_created'), new Transition('build_top', 'prepare_top', 'top_created'), new Transition('join', [new Arc('leg_created', 4), 'top_created', 'stopwatch_running'], 'finished'), ] ); $subject = new Subject(); $workflow = new Workflow($definition); $workflow->apply($subject, 'start'); $workflow->apply($subject, 'build_leg'); $workflow->apply($subject, 'build_top'); $workflow->apply($subject, 'build_leg'); $workflow->apply($subject, 'build_leg'); $workflow->apply($subject, 'build_leg'); $workflow->apply($subject, 'join'); ``` --- Another example, based on https://demo-symfony-workflow.cleverapps.io/articles/show/1 [workflow.webm](https://github.com/user-attachments/assets/fe5a2fca-cfef-4621-94f9-98b7d9bb9a01) Commits ------- 207fc49a895 [Workflow] Add support for weighted transitions
2 parents 4a75dac + 8954871 commit 11e8902

19 files changed

+434
-77
lines changed

Arc.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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;
13+
14+
/**
15+
* @author Grégoire Pineau <lyrixx@lyrixx.info>
16+
*/
17+
final readonly class Arc
18+
{
19+
public function __construct(
20+
public string $place,
21+
public int $weight,
22+
) {
23+
if ($weight < 1) {
24+
throw new \InvalidArgumentException(\sprintf('The weight must be greater than 0, %d given.', $weight));
25+
}
26+
if (!$place) {
27+
throw new \InvalidArgumentException('The place name cannot be empty.');
28+
}
29+
}
30+
}

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ CHANGELOG
55
---
66

77
* Add support for `BackedEnum` in `MethodMarkingStore`
8+
* Add support for weighted transitions
89

910
7.3
1011
---

Definition.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -104,15 +104,15 @@ private function addPlace(string $place): void
104104

105105
private function addTransition(Transition $transition): void
106106
{
107-
foreach ($transition->getFroms() as $from) {
108-
if (!\array_key_exists($from, $this->places)) {
109-
$this->addPlace($from);
107+
foreach ($transition->getFroms(true) as $arc) {
108+
if (!\array_key_exists($arc->place, $this->places)) {
109+
$this->addPlace($arc->place);
110110
}
111111
}
112112

113-
foreach ($transition->getTos() as $to) {
114-
if (!\array_key_exists($to, $this->places)) {
115-
$this->addPlace($to);
113+
foreach ($transition->getTos(true) as $arc) {
114+
if (!\array_key_exists($arc->place, $this->places)) {
115+
$this->addPlace($arc->place);
116116
}
117117
}
118118

Dumper/GraphvizDumper.php

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -199,20 +199,22 @@ protected function findEdges(Definition $definition): array
199199
foreach ($definition->getTransitions() as $i => $transition) {
200200
$transitionName = $workflowMetadata->getMetadata('label', $transition) ?? $transition->getName();
201201

202-
foreach ($transition->getFroms() as $from) {
202+
foreach ($transition->getFroms(true) as $arc) {
203203
$dotEdges[] = [
204-
'from' => $from,
204+
'from' => $arc->place,
205205
'to' => $transitionName,
206206
'direction' => 'from',
207207
'transition_number' => $i,
208+
'weight' => $arc->weight,
208209
];
209210
}
210-
foreach ($transition->getTos() as $to) {
211+
foreach ($transition->getTos(true) as $arc) {
211212
$dotEdges[] = [
212213
'from' => $transitionName,
213-
'to' => $to,
214+
'to' => $arc->place,
214215
'direction' => 'to',
215216
'transition_number' => $i,
217+
'weight' => $arc->weight,
216218
];
217219
}
218220
}
@@ -229,14 +231,16 @@ protected function addEdges(array $edges): string
229231

230232
foreach ($edges as $edge) {
231233
if ('from' === $edge['direction']) {
232-
$code .= \sprintf(" place_%s -> transition_%s [style=\"solid\"];\n",
234+
$code .= \sprintf(" place_%s -> transition_%s [style=\"solid\"%s];\n",
233235
$this->dotize($edge['from']),
234-
$this->dotize($edge['transition_number'])
236+
$this->dotize($edge['transition_number']),
237+
$edge['weight'] > 1 ? \sprintf(',label="%s"', $this->escape($edge['weight'])) : '',
235238
);
236239
} else {
237-
$code .= \sprintf(" transition_%s -> place_%s [style=\"solid\"];\n",
240+
$code .= \sprintf(" transition_%s -> place_%s [style=\"solid\"%s];\n",
238241
$this->dotize($edge['transition_number']),
239-
$this->dotize($edge['to'])
242+
$this->dotize($edge['to']),
243+
$edge['weight'] > 1 ? \sprintf(',label="%s"', $this->escape($edge['weight'])) : '',
240244
);
241245
}
242246
}

Dumper/MermaidDumper.php

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Component\Workflow\Dumper;
1313

14+
use Symfony\Component\Workflow\Arc;
1415
use Symfony\Component\Workflow\Definition;
1516
use Symfony\Component\Workflow\Exception\InvalidArgumentException;
1617
use Symfony\Component\Workflow\Marking;
@@ -69,7 +70,8 @@ public function dump(Definition $definition, ?Marking $marking = null, array $op
6970
$place,
7071
$meta->getPlaceMetadata($place),
7172
\in_array($place, $definition->getInitialPlaces(), true),
72-
$marking?->has($place) ?? false
73+
$marking?->has($place) ?? false,
74+
$marking?->getTokenCount($place) ?? 0
7375
);
7476

7577
$output[] = $placeNode;
@@ -91,16 +93,15 @@ public function dump(Definition $definition, ?Marking $marking = null, array $op
9193
$transitionLabel = $transitionMeta['label'];
9294
}
9395

94-
foreach ($transition->getFroms() as $from) {
95-
$from = $placeNameMap[$from];
96-
97-
foreach ($transition->getTos() as $to) {
98-
$to = $placeNameMap[$to];
99-
96+
foreach ($transition->getFroms(true) as $fromArc) {
97+
foreach ($transition->getTos(true) as $toArc) {
10098
if (self::TRANSITION_TYPE_STATEMACHINE === $this->transitionType) {
99+
$from = $placeNameMap[$fromArc->place];
100+
$to = $placeNameMap[$toArc->place];
101+
101102
$transitionOutput = $this->styleStateMachineTransition($from, $to, $transitionLabel, $transitionMeta);
102103
} else {
103-
$transitionOutput = $this->styleWorkflowTransition($from, $to, $transitionId, $transitionLabel, $transitionMeta);
104+
$transitionOutput = $this->styleWorkflowTransition($placeNameMap, $fromArc, $toArc, $transitionId, $transitionLabel, $transitionMeta);
104105
}
105106

106107
foreach ($transitionOutput as $line) {
@@ -122,12 +123,15 @@ public function dump(Definition $definition, ?Marking $marking = null, array $op
122123
return implode("\n", $output);
123124
}
124125

125-
private function preparePlace(int $placeId, string $placeName, array $meta, bool $isInitial, bool $hasMarking): array
126+
private function preparePlace(int $placeId, string $placeName, array $meta, bool $isInitial, bool $hasMarking, int $tokenCount): array
126127
{
127128
$placeLabel = $placeName;
128129
if (\array_key_exists('label', $meta)) {
129130
$placeLabel = $meta['label'];
130131
}
132+
if (1 < $tokenCount) {
133+
$placeLabel .= ' ('.$tokenCount.')';
134+
}
131135

132136
$placeLabel = $this->escape($placeLabel);
133137

@@ -206,7 +210,7 @@ private function styleStateMachineTransition(string $from, string $to, string $t
206210
return $transitionOutput;
207211
}
208212

209-
private function styleWorkflowTransition(string $from, string $to, int $transitionId, string $transitionLabel, array $transitionMeta): array
213+
private function styleWorkflowTransition(array $placeNameMap, Arc $from, Arc $to, int $transitionId, string $transitionLabel, array $transitionMeta): array
210214
{
211215
$transitionOutput = [];
212216

@@ -220,8 +224,11 @@ private function styleWorkflowTransition(string $from, string $to, int $transiti
220224
$transitionOutput[] = $transitionNodeStyle;
221225
}
222226

223-
$connectionStyle = '%s-->%s';
224-
$transitionOutput[] = \sprintf($connectionStyle, $from, $transitionNodeName);
227+
if ($from->weight > 1) {
228+
$transitionOutput[] = \sprintf('%s-->|%d|%s', $placeNameMap[$from->place], $from->weight, $transitionNodeName);
229+
} else {
230+
$transitionOutput[] = \sprintf('%s-->%s', $placeNameMap[$from->place], $transitionNodeName);
231+
}
225232

226233
$linkStyle = $this->styleLink($transitionMeta);
227234
if ('' !== $linkStyle) {
@@ -230,7 +237,11 @@ private function styleWorkflowTransition(string $from, string $to, int $transiti
230237

231238
++$this->linkCount;
232239

233-
$transitionOutput[] = \sprintf($connectionStyle, $transitionNodeName, $to);
240+
if ($to->weight > 1) {
241+
$transitionOutput[] = \sprintf('%s-->|%d|%s', $transitionNodeName, $to->weight, $placeNameMap[$to->place]);
242+
} else {
243+
$transitionOutput[] = \sprintf('%s-->%s', $transitionNodeName, $placeNameMap[$to->place]);
244+
}
234245

235246
$linkStyle = $this->styleLink($transitionMeta);
236247
if ('' !== $linkStyle) {

Dumper/PlantUmlDumper.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,10 @@ public function dump(Definition $definition, ?Marking $marking = null, array $op
7878
}
7979
foreach ($definition->getTransitions() as $transition) {
8080
$transitionEscaped = $this->escape($transition->getName());
81-
foreach ($transition->getFroms() as $from) {
82-
$fromEscaped = $this->escape($from);
83-
foreach ($transition->getTos() as $to) {
84-
$toEscaped = $this->escape($to);
81+
foreach ($transition->getFroms(true) as $fromArc) {
82+
$fromEscaped = $this->escape($fromArc->place);
83+
foreach ($transition->getTos(true) as $toArc) {
84+
$toEscaped = $this->escape($toArc->place);
8585

8686
$transitionEscapedWithStyle = $this->getTransitionEscapedWithStyle($workflowMetadata, $transition, $transitionEscaped);
8787

Dumper/StateMachineGraphvizDumper.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,14 +65,14 @@ protected function findEdges(Definition $definition): array
6565
$attributes['color'] = $arrowColor;
6666
}
6767

68-
foreach ($transition->getFroms() as $from) {
69-
foreach ($transition->getTos() as $to) {
68+
foreach ($transition->getFroms(true) as $fromArc) {
69+
foreach ($transition->getTos(true) as $toArc) {
7070
$edge = [
7171
'name' => $transitionName,
72-
'to' => $to,
72+
'to' => $toArc->place,
7373
'attributes' => $attributes,
7474
];
75-
$edges[$from][] = $edge;
75+
$edges[$fromArc->place][] = $edge;
7676
}
7777
}
7878
}

EventListener/AuditTrailListener.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ public function __construct(
2727

2828
public function onLeave(Event $event): void
2929
{
30-
foreach ($event->getTransition()->getFroms() as $place) {
31-
$this->logger->info(\sprintf('Leaving "%s" for subject of class "%s" in workflow "%s".', $place, $event->getSubject()::class, $event->getWorkflowName()));
30+
foreach ($event->getTransition()->getFroms(true) as $arc) {
31+
$this->logger->info(\sprintf('Leaving "%s" for subject of class "%s" in workflow "%s".', $arc->place, $event->getSubject()::class, $event->getWorkflowName()));
3232
}
3333
}
3434

@@ -39,8 +39,8 @@ public function onTransition(Event $event): void
3939

4040
public function onEnter(Event $event): void
4141
{
42-
foreach ($event->getTransition()->getTos() as $place) {
43-
$this->logger->info(\sprintf('Entering "%s" for subject of class "%s" in workflow "%s".', $place, $event->getSubject()::class, $event->getWorkflowName()));
42+
foreach ($event->getTransition()->getTos(true) as $arc) {
43+
$this->logger->info(\sprintf('Entering "%s" for subject of class "%s" in workflow "%s".', $arc->place, $event->getSubject()::class, $event->getWorkflowName()));
4444
}
4545
}
4646

Marking.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,11 @@
1818
*/
1919
class Marking
2020
{
21+
/**
22+
* @var array<string, int<0,max>> Keys are the place names and values are the number of tokens in that place
23+
*/
2124
private array $places = [];
25+
2226
private ?array $context = null;
2327

2428
/**
@@ -83,6 +87,11 @@ public function has(string $place): bool
8387
return isset($this->places[$place]);
8488
}
8589

90+
public function getTokenCount(string $place): int
91+
{
92+
return $this->places[$place] ?? 0;
93+
}
94+
8695
public function getPlaces(): array
8796
{
8897
return $this->places;

Tests/ArcTest.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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\Tests;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\Workflow\Arc;
16+
17+
class ArcTest extends TestCase
18+
{
19+
public function testConstructorWithInvalidPlaceName()
20+
{
21+
$this->expectException(\InvalidArgumentException::class);
22+
$this->expectExceptionMessage('The place name cannot be empty.');
23+
24+
new Arc('', 1);
25+
}
26+
27+
public function testConstructorWithInvalidWeight()
28+
{
29+
$this->expectException(\InvalidArgumentException::class);
30+
$this->expectExceptionMessage('The weight must be greater than 0, 0 given.');
31+
32+
new Arc('not empty', 0);
33+
}
34+
}

0 commit comments

Comments
 (0)