Skip to content

Commit 207fc49

Browse files
lyrixxnicolas-grekas
authored andcommitted
[Workflow] Add support for weighted transitions
1 parent 74fc896 commit 207fc49

28 files changed

+628
-135
lines changed

src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ CHANGELOG
1313
* Add `KernelBrowser::getSession()`
1414
* Add support for configuring workflow places with glob patterns matching consts/backed enums
1515
* Add support for configuring the `CachingHttpClient`
16+
* Add support for weighted transitions in workflows
1617

1718
7.3
1819
---

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php

Lines changed: 64 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -562,11 +562,11 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode): void
562562
->requiresAtLeastOneElement()
563563
->prototype('array')
564564
->children()
565-
->scalarNode('name')
565+
->stringNode('name')
566566
->isRequired()
567567
->cannotBeEmpty()
568568
->end()
569-
->scalarNode('guard')
569+
->stringNode('guard')
570570
->cannotBeEmpty()
571571
->info('An expression to block the transition.')
572572
->example('is_fully_authenticated() and is_granted(\'ROLE_JOURNALIST\') and subject.getTitle() == \'My first article\'')
@@ -576,23 +576,79 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode): void
576576
->acceptAndWrap(['backed-enum', 'string'])
577577
->beforeNormalization()
578578
->ifArray()
579-
->then(static fn ($from) => array_map(static fn ($v) => $v instanceof \BackedEnum ? $v->value : $v, $from))
579+
->then($workflowNormalizeArcs = static function ($arcs) {
580+
// Fix XML parsing, when only one arc is defined
581+
if (\array_key_exists('value', $arcs) && \array_key_exists('weight', $arcs)) {
582+
return [[
583+
'place' => $arcs['value'],
584+
'weight' => $arcs['weight'],
585+
]];
586+
}
587+
588+
$normalizedArcs = [];
589+
foreach ($arcs as $arc) {
590+
if ($arc instanceof \BackedEnum) {
591+
$arc = $arc->value;
592+
}
593+
if (\is_string($arc)) {
594+
$arc = [
595+
'place' => $arc,
596+
'weight' => 1,
597+
];
598+
} elseif (!\is_array($arc)) {
599+
throw new InvalidConfigurationException('The "from" arcs must be a list of strings or arrays in workflow configuration.');
600+
} elseif (\array_key_exists('value', $arc) && \array_key_exists('weight', $arc)) {
601+
// Fix XML parsing
602+
$arc = [
603+
'place' => $arc['value'],
604+
'weight' => $arc['weight'],
605+
];
606+
}
607+
608+
$normalizedArcs[] = $arc;
609+
}
610+
611+
return $normalizedArcs;
612+
})
580613
->end()
581614
->requiresAtLeastOneElement()
582-
->prototype('scalar')
583-
->cannotBeEmpty()
615+
->prototype('array')
616+
->children()
617+
->stringNode('place')
618+
->isRequired()
619+
->cannotBeEmpty()
620+
->end()
621+
->integerNode('weight')
622+
->isRequired()
623+
->end()
624+
->end()
584625
->end()
585626
->end()
586627
->arrayNode('to')
587628
->performNoDeepMerging()
588629
->acceptAndWrap(['backed-enum', 'string'])
589630
->beforeNormalization()
590631
->ifArray()
591-
->then(static fn ($to) => array_map(static fn ($v) => $v instanceof \BackedEnum ? $v->value : $v, $to))
632+
->then($workflowNormalizeArcs)
592633
->end()
593634
->requiresAtLeastOneElement()
594-
->prototype('scalar')
595-
->cannotBeEmpty()
635+
->prototype('array')
636+
->children()
637+
->stringNode('place')
638+
->isRequired()
639+
->cannotBeEmpty()
640+
->end()
641+
->integerNode('weight')
642+
->isRequired()
643+
->end()
644+
->end()
645+
->end()
646+
->end()
647+
->integerNode('weight')
648+
->defaultValue(1)
649+
->validate()
650+
->ifTrue(static fn ($v) => $v < 1)
651+
->thenInvalid('The weight must be greater than 0.')
596652
->end()
597653
->end()
598654
->arrayNode('metadata')

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@
236236
use Symfony\Component\WebLink\HttpHeaderParser;
237237
use Symfony\Component\WebLink\HttpHeaderSerializer;
238238
use Symfony\Component\Workflow;
239+
use Symfony\Component\Workflow\Arc;
239240
use Symfony\Component\Workflow\WorkflowInterface;
240241
use Symfony\Component\Yaml\Command\LintCommand as BaseYamlLintCommand;
241242
use Symfony\Component\Yaml\Yaml;
@@ -1114,6 +1115,11 @@ private function registerWorkflowConfiguration(array $config, ContainerBuilder $
11141115
// Global transition counter per workflow
11151116
$transitionCounter = 0;
11161117
foreach ($workflow['transitions'] as $transition) {
1118+
foreach (['from', 'to'] as $direction) {
1119+
foreach ($transition[$direction] as $k => $arc) {
1120+
$transition[$direction][$k] = new Definition(Arc::class, [$arc['place'], $arc['weight'] ?? 1]);
1121+
}
1122+
}
11171123
if ('workflow' === $type) {
11181124
$transitionId = \sprintf('.%s.transition.%s', $workflowId, $transitionCounter++);
11191125
$container->register($transitionId, Workflow\Transition::class)
@@ -1137,7 +1143,7 @@ private function registerWorkflowConfiguration(array $config, ContainerBuilder $
11371143
foreach ($transition['to'] as $to) {
11381144
$transitionId = \sprintf('.%s.transition.%s', $workflowId, $transitionCounter++);
11391145
$container->register($transitionId, Workflow\Transition::class)
1140-
->setArguments([$transition['name'], $from, $to]);
1146+
->setArguments([$transition['name'], [$from], [$to]]);
11411147
$transitions[] = new Reference($transitionId);
11421148
if (isset($transition['guard'])) {
11431149
$eventName = \sprintf('workflow.%s.guard.%s', $name, $transition['name']);

src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -518,15 +518,23 @@
518518

519519
<xsd:complexType name="transition">
520520
<xsd:sequence>
521-
<xsd:element name="from" type="xsd:string" minOccurs="1" maxOccurs="unbounded" />
522-
<xsd:element name="to" type="xsd:string" minOccurs="1" maxOccurs="unbounded" />
521+
<xsd:element name="from" type="arc" minOccurs="1" maxOccurs="unbounded" />
522+
<xsd:element name="to" type="arc" minOccurs="1" maxOccurs="unbounded" />
523523
<xsd:element name="metadata" type="metadata" minOccurs="0" maxOccurs="unbounded" />
524524
<xsd:element name="guard" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
525525
</xsd:sequence>
526526
<xsd:attribute name="name" type="xsd:string" />
527527
<xsd:attribute name="key" type="xsd:string" />
528528
</xsd:complexType>
529529

530+
<xsd:complexType name="arc">
531+
<xsd:simpleContent>
532+
<xsd:extension base="xsd:string">
533+
<xsd:attribute name="weight" type="xsd:integer" />
534+
</xsd:extension>
535+
</xsd:simpleContent>
536+
</xsd:complexType>
537+
530538
<xsd:complexType name="place" mixed="true">
531539
<xsd:sequence>
532540
<xsd:element name="metadata" type="metadata" minOccurs="0" maxOccurs="unbounded" />

src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/workflow_with_multiple_transitions_with_same_name.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,27 +22,28 @@
2222
'approved_by_spellchecker',
2323
'published',
2424
],
25+
// We also test different configuration formats here
2526
'transitions' => [
2627
'request_review' => [
2728
'from' => 'draft',
2829
'to' => ['wait_for_journalist', 'wait_for_spellchecker'],
2930
],
3031
'journalist_approval' => [
31-
'from' => 'wait_for_journalist',
32+
'from' => ['wait_for_journalist'],
3233
'to' => 'approved_by_journalist',
3334
],
3435
'spellchecker_approval' => [
3536
'from' => 'wait_for_spellchecker',
3637
'to' => 'approved_by_spellchecker',
3738
],
3839
'publish' => [
39-
'from' => ['approved_by_journalist', 'approved_by_spellchecker'],
40+
'from' => [['place' => 'approved_by_journalist', 'weight' => 1], 'approved_by_spellchecker'],
4041
'to' => 'published',
4142
],
4243
'publish_editor_in_chief' => [
4344
'name' => 'publish',
4445
'from' => 'draft',
45-
'to' => 'published',
46+
'to' => [['place' => 'published', 'weight' => 2]],
4647
],
4748
],
4849
],

src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflow_with_multiple_transitions_with_same_name.xml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
<framework:place name="wait_for_spellchecker" />
1919
<framework:place name="approved_by_spellchecker" />
2020
<framework:place name="published" />
21+
<!-- We also test different configuration formats here -->
2122
<framework:transition name="request_review">
2223
<framework:from>draft</framework:from>
2324
<framework:to>wait_for_journalist</framework:to>
@@ -32,13 +33,13 @@
3233
<framework:to>approved_by_spellchecker</framework:to>
3334
</framework:transition>
3435
<framework:transition name="publish">
35-
<framework:from>approved_by_journalist</framework:from>
36-
<framework:from>approved_by_spellchecker</framework:from>
36+
<framework:from weight="1">approved_by_journalist</framework:from>
37+
<framework:from weight="1">approved_by_spellchecker</framework:from>
3738
<framework:to>published</framework:to>
3839
</framework:transition>
3940
<framework:transition name="publish">
4041
<framework:from>draft</framework:from>
41-
<framework:to>published</framework:to>
42+
<framework:to weight="2">published</framework:to>
4243
</framework:transition>
4344
</framework:workflow>
4445
</framework:config>

src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/workflow_with_multiple_transitions_with_same_name.yml

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,21 @@ framework:
1717
- wait_for_spellchecker
1818
- approved_by_spellchecker
1919
- published
20+
# We also test different configuration formats here
2021
transitions:
2122
request_review:
22-
from: [draft]
23+
from: draft
2324
to: [wait_for_journalist, wait_for_spellchecker]
2425
journalist_approval:
2526
from: [wait_for_journalist]
26-
to: [approved_by_journalist]
27+
to: approved_by_journalist
2728
spellchecker_approval:
28-
from: [wait_for_spellchecker]
29-
to: [approved_by_spellchecker]
29+
from: wait_for_spellchecker
30+
to: approved_by_spellchecker
3031
publish:
31-
from: [approved_by_journalist, approved_by_spellchecker]
32-
to: [published]
32+
from: [{place: approved_by_journalist, weight: 1}, approved_by_spellchecker]
33+
to: published
3334
publish_editor_in_chief:
3435
name: publish
35-
from: [draft]
36-
to: [published]
36+
from: draft
37+
to: [{place: published, weight: 2}]

0 commit comments

Comments
 (0)