Skip to content

Commit c2a67c1

Browse files
committed
[Workflow] Add support for weighted transitions
1 parent c16162f commit c2a67c1

28 files changed

+654
-134
lines changed

src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ CHANGELOG
2525
* Set `framework.rate_limiter.limiters.*.lock_factory` to `auto` by default
2626
* Deprecate `RateLimiterFactory` autowiring aliases, use `RateLimiterFactoryInterface` instead
2727
* Allow configuring compound rate limiters
28-
28+
* Add support for weighted transitions in workflows
29+
*
2930
7.2
3031
---
3132

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

Lines changed: 63 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,8 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode): void
394394
];
395395
}
396396

397+
// dd($v);
398+
397399
return $v;
398400
})
399401
->end()
@@ -546,27 +548,80 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode): void
546548
->requiresAtLeastOneElement()
547549
->prototype('array')
548550
->children()
549-
->scalarNode('name')
551+
->stringNode('name')
550552
->isRequired()
551553
->cannotBeEmpty()
552554
->end()
553-
->scalarNode('guard')
555+
->stringNode('guard')
554556
->cannotBeEmpty()
555557
->info('An expression to block the transition.')
556558
->example('is_fully_authenticated() and is_granted(\'ROLE_JOURNALIST\') and subject.getTitle() == \'My first article\'')
557559
->end()
558560
->arrayNode('from')
559-
->beforeNormalization()->castToArray()->end()
561+
->beforeNormalization()
562+
->always()
563+
->then($workflowNormalizeArcs = function ($arcs) {
564+
$arcs = (array) $arcs;
565+
// Fix XML parsing, when only one arc is defined
566+
if (array_key_exists('weight', $arcs) && array_key_exists('value', $arcs)) {
567+
$arcs = [$arcs];
568+
}
569+
570+
foreach ($arcs as $k => $arc) {
571+
if (is_int($k) && is_string($arc)) {
572+
$arcs[$k] = [
573+
'place' => $arc,
574+
'weight' => 1,
575+
];
576+
} else if (array_key_exists('weight', $arc) && array_key_exists('value', $arc)) {
577+
// Fix XML parsing, when only one arc is defined
578+
$arcs[$k] = [
579+
'place' => $arc['value'],
580+
'weight' => $arc['weight'],
581+
];
582+
}
583+
}
584+
585+
586+
return $arcs;
587+
})
588+
->end()
560589
->requiresAtLeastOneElement()
561-
->prototype('scalar')
562-
->cannotBeEmpty()
590+
->prototype('array')
591+
->children()
592+
->stringNode('place')
593+
->isRequired()
594+
->cannotBeEmpty()
595+
->end()
596+
->integerNode('weight')
597+
->isRequired()
598+
->end()
599+
->end()
563600
->end()
564601
->end()
565602
->arrayNode('to')
566-
->beforeNormalization()->castToArray()->end()
603+
->beforeNormalization()
604+
->always()
605+
->then($workflowNormalizeArcs)
606+
->end()
567607
->requiresAtLeastOneElement()
568-
->prototype('scalar')
569-
->cannotBeEmpty()
608+
->prototype('array')
609+
->children()
610+
->stringNode('place')
611+
->isRequired()
612+
->cannotBeEmpty()
613+
->end()
614+
->integerNode('weight')
615+
->isRequired()
616+
->end()
617+
->end()
618+
->end()
619+
->end()
620+
->integerNode('weight')
621+
->defaultValue(1)
622+
->validate()
623+
->ifTrue(fn ($v) => $v < 1)
624+
->thenInvalid('The weight must be greater than 0.')
570625
->end()
571626
->end()
572627
->arrayNode('metadata')

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

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,9 @@
1717
use Doctrine\ORM\Mapping\MappedSuperclass;
1818
use Http\Client\HttpAsyncClient;
1919
use Http\Client\HttpClient;
20-
use phpDocumentor\Reflection\DocBlockFactoryInterface;
21-
use phpDocumentor\Reflection\Types\ContextFactory;
22-
use PhpParser\Parser;
2320
use PHPStan\PhpDocParser\Parser\PhpDocParser;
2421
use PHPUnit\Framework\TestCase;
22+
use PhpParser\Parser;
2523
use Psr\Cache\CacheItemPoolInterface;
2624
use Psr\Clock\ClockInterface as PsrClockInterface;
2725
use Psr\Container\ContainerInterface as PsrContainerInterface;
@@ -33,10 +31,10 @@
3331
use Symfony\Bundle\FrameworkBundle\Routing\RouteLoaderInterface;
3432
use Symfony\Bundle\FullStack;
3533
use Symfony\Bundle\MercureBundle\MercureBundle;
36-
use Symfony\Component\Asset\PackageInterface;
3734
use Symfony\Component\AssetMapper\AssetMapper;
3835
use Symfony\Component\AssetMapper\Compiler\AssetCompilerInterface;
3936
use Symfony\Component\AssetMapper\Compressor\CompressorInterface;
37+
use Symfony\Component\Asset\PackageInterface;
4038
use Symfony\Component\BrowserKit\AbstractBrowser;
4139
use Symfony\Component\Cache\Adapter\AdapterInterface;
4240
use Symfony\Component\Cache\Adapter\ArrayAdapter;
@@ -49,9 +47,9 @@
4947
use Symfony\Component\Config\Definition\ConfigurationInterface;
5048
use Symfony\Component\Config\FileLocator;
5149
use Symfony\Component\Config\Loader\LoaderInterface;
50+
use Symfony\Component\Config\ResourceCheckerInterface;
5251
use Symfony\Component\Config\Resource\DirectoryResource;
5352
use Symfony\Component\Config\Resource\FileResource;
54-
use Symfony\Component\Config\ResourceCheckerInterface;
5553
use Symfony\Component\Console\Application;
5654
use Symfony\Component\Console\Attribute\AsCommand;
5755
use Symfony\Component\Console\Command\Command;
@@ -208,15 +206,16 @@
208206
use Symfony\Component\Uid\Factory\UuidFactory;
209207
use Symfony\Component\Uid\UuidV4;
210208
use Symfony\Component\Validator\Constraint;
211-
use Symfony\Component\Validator\Constraints\ExpressionLanguageProvider;
212209
use Symfony\Component\Validator\ConstraintValidatorInterface;
210+
use Symfony\Component\Validator\Constraints\ExpressionLanguageProvider;
213211
use Symfony\Component\Validator\GroupProviderInterface;
214212
use Symfony\Component\Validator\Mapping\Loader\PropertyInfoLoader;
215213
use Symfony\Component\Validator\ObjectInitializerInterface;
216214
use Symfony\Component\Validator\Validation;
217-
use Symfony\Component\Webhook\Controller\WebhookController;
218215
use Symfony\Component\WebLink\HttpHeaderSerializer;
216+
use Symfony\Component\Webhook\Controller\WebhookController;
219217
use Symfony\Component\Workflow;
218+
use Symfony\Component\Workflow\Arc;
220219
use Symfony\Component\Workflow\WorkflowInterface;
221220
use Symfony\Component\Yaml\Command\LintCommand as BaseYamlLintCommand;
222221
use Symfony\Component\Yaml\Yaml;
@@ -229,6 +228,8 @@
229228
use Symfony\Contracts\Service\ResetInterface;
230229
use Symfony\Contracts\Service\ServiceSubscriberInterface;
231230
use Symfony\Contracts\Translation\LocaleAwareInterface;
231+
use phpDocumentor\Reflection\DocBlockFactoryInterface;
232+
use phpDocumentor\Reflection\Types\ContextFactory;
232233

233234
/**
234235
* Process the configuration and prepare the dependency injection container with
@@ -1074,6 +1075,14 @@ private function registerWorkflowConfiguration(array $config, ContainerBuilder $
10741075
// Global transition counter per workflow
10751076
$transitionCounter = 0;
10761077
foreach ($workflow['transitions'] as $transition) {
1078+
foreach (['from', 'to'] as $direction) {
1079+
foreach ($transition[$direction] as $k => $arc) {
1080+
$transition[$direction][$k] = new Definition(Arc::class, [
1081+
'$place' => $arc['place'],
1082+
'$weight' => $arc['weight'] ?? 1,
1083+
]);
1084+
}
1085+
}
10771086
if ('workflow' === $type) {
10781087
$transitionDefinition = new Definition(Workflow\Transition::class, [$transition['name'], $transition['from'], $transition['to']]);
10791088
$transitionId = \sprintf('.%s.transition.%s', $workflowId, $transitionCounter++);
@@ -1095,7 +1104,7 @@ private function registerWorkflowConfiguration(array $config, ContainerBuilder $
10951104
} elseif ('state_machine' === $type) {
10961105
foreach ($transition['from'] as $from) {
10971106
foreach ($transition['to'] as $to) {
1098-
$transitionDefinition = new Definition(Workflow\Transition::class, [$transition['name'], $from, $to]);
1107+
$transitionDefinition = new Definition(Workflow\Transition::class, [$transition['name'], [$from], [$to]]);
10991108
$transitionId = \sprintf('.%s.transition.%s', $workflowId, $transitionCounter++);
11001109
$container->setDefinition($transitionId, $transitionDefinition);
11011110
$transitions[] = new Reference($transitionId);

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
@@ -502,14 +502,22 @@
502502

503503
<xsd:complexType name="transition">
504504
<xsd:sequence>
505-
<xsd:element name="from" type="xsd:string" minOccurs="1" maxOccurs="unbounded" />
506-
<xsd:element name="to" type="xsd:string" minOccurs="1" maxOccurs="unbounded" />
505+
<xsd:element name="from" type="arc" minOccurs="1" maxOccurs="unbounded" />
506+
<xsd:element name="to" type="arc" minOccurs="1" maxOccurs="unbounded" />
507507
<xsd:element name="metadata" type="metadata" minOccurs="0" maxOccurs="unbounded" />
508508
<xsd:element name="guard" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
509509
</xsd:sequence>
510510
<xsd:attribute name="name" type="xsd:string" use="required" />
511511
</xsd:complexType>
512512

513+
<xsd:complexType name="arc">
514+
<xsd:simpleContent>
515+
<xsd:extension base="xsd:string">
516+
<xsd:attribute name="weight" type="xsd:integer" use="optional" />
517+
</xsd:extension>
518+
</xsd:simpleContent>
519+
</xsd:complexType>
520+
513521
<xsd:complexType name="place" mixed="true">
514522
<xsd:sequence>
515523
<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 format 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: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,13 @@
3232
<framework:to>approved_by_spellchecker</framework:to>
3333
</framework:transition>
3434
<framework:transition name="publish">
35-
<framework:from>approved_by_journalist</framework:from>
36-
<framework:from>approved_by_spellchecker</framework:from>
35+
<framework:from weight="1">approved_by_journalist</framework:from>
36+
<framework:from weight="1">approved_by_spellchecker</framework:from>
3737
<framework:to>published</framework:to>
3838
</framework:transition>
3939
<framework:transition name="publish">
4040
<framework:from>draft</framework:from>
41-
<framework:to>published</framework:to>
41+
<framework:to weight="2">published</framework:to>
4242
</framework:transition>
4343
</framework:workflow>
4444
</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 format 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)