diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md
index 8e70fb98e42fe..da8de057510f7 100644
--- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md
+++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md
@@ -25,6 +25,7 @@ CHANGELOG
* Set `framework.rate_limiter.limiters.*.lock_factory` to `auto` by default
* Deprecate `RateLimiterFactory` autowiring aliases, use `RateLimiterFactoryInterface` instead
* Allow configuring compound rate limiters
+ * Add support for weighted transitions in workflows
7.2
---
diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
index 6b168a2d4a0fd..c1f601f64960a 100644
--- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
+++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
@@ -546,27 +546,80 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode): void
->requiresAtLeastOneElement()
->prototype('array')
->children()
- ->scalarNode('name')
+ ->stringNode('name')
->isRequired()
->cannotBeEmpty()
->end()
- ->scalarNode('guard')
+ ->stringNode('guard')
->cannotBeEmpty()
->info('An expression to block the transition.')
->example('is_fully_authenticated() and is_granted(\'ROLE_JOURNALIST\') and subject.getTitle() == \'My first article\'')
->end()
->arrayNode('from')
- ->beforeNormalization()->castToArray()->end()
+ ->beforeNormalization()
+ ->always()
+ ->then($workflowNormalizeArcs = static function ($arcs) {
+ $arcs = (array) $arcs;
+
+ // Fix XML parsing, when only one arc is defined
+ if (array_key_exists('weight', $arcs) && array_key_exists('value', $arcs)) {
+ $arcs = [$arcs];
+ }
+
+ foreach ($arcs as $k => $arc) {
+ if (is_int($k) && is_string($arc)) {
+ $arcs[$k] = [
+ 'place' => $arc,
+ 'weight' => 1,
+ ];
+ } else if (array_key_exists('weight', $arc) && array_key_exists('value', $arc)) {
+ // Fix XML parsing
+ $arcs[$k] = [
+ 'place' => $arc['value'],
+ 'weight' => $arc['weight'],
+ ];
+ }
+ }
+
+ return $arcs;
+ })
+ ->end()
->requiresAtLeastOneElement()
- ->prototype('scalar')
- ->cannotBeEmpty()
+ ->prototype('array')
+ ->children()
+ ->stringNode('place')
+ ->isRequired()
+ ->cannotBeEmpty()
+ ->end()
+ ->integerNode('weight')
+ ->isRequired()
+ ->end()
+ ->end()
->end()
->end()
->arrayNode('to')
- ->beforeNormalization()->castToArray()->end()
+ ->beforeNormalization()
+ ->always()
+ ->then($workflowNormalizeArcs)
+ ->end()
->requiresAtLeastOneElement()
- ->prototype('scalar')
- ->cannotBeEmpty()
+ ->prototype('array')
+ ->children()
+ ->stringNode('place')
+ ->isRequired()
+ ->cannotBeEmpty()
+ ->end()
+ ->integerNode('weight')
+ ->isRequired()
+ ->end()
+ ->end()
+ ->end()
+ ->end()
+ ->integerNode('weight')
+ ->defaultValue(1)
+ ->validate()
+ ->ifTrue(static fn (int $v): bool => $v < 1)
+ ->thenInvalid('The weight must be greater than 0.')
->end()
->end()
->arrayNode('metadata')
diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
index 5595e14b36329..d05ce15201f09 100644
--- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
+++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
@@ -17,11 +17,9 @@
use Doctrine\ORM\Mapping\MappedSuperclass;
use Http\Client\HttpAsyncClient;
use Http\Client\HttpClient;
-use phpDocumentor\Reflection\DocBlockFactoryInterface;
-use phpDocumentor\Reflection\Types\ContextFactory;
-use PhpParser\Parser;
use PHPStan\PhpDocParser\Parser\PhpDocParser;
use PHPUnit\Framework\TestCase;
+use PhpParser\Parser;
use Psr\Cache\CacheItemPoolInterface;
use Psr\Clock\ClockInterface as PsrClockInterface;
use Psr\Container\ContainerInterface as PsrContainerInterface;
@@ -33,10 +31,10 @@
use Symfony\Bundle\FrameworkBundle\Routing\RouteLoaderInterface;
use Symfony\Bundle\FullStack;
use Symfony\Bundle\MercureBundle\MercureBundle;
-use Symfony\Component\Asset\PackageInterface;
use Symfony\Component\AssetMapper\AssetMapper;
use Symfony\Component\AssetMapper\Compiler\AssetCompilerInterface;
use Symfony\Component\AssetMapper\Compressor\CompressorInterface;
+use Symfony\Component\Asset\PackageInterface;
use Symfony\Component\BrowserKit\AbstractBrowser;
use Symfony\Component\Cache\Adapter\AdapterInterface;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
@@ -49,9 +47,9 @@
use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\Config\Loader\LoaderInterface;
+use Symfony\Component\Config\ResourceCheckerInterface;
use Symfony\Component\Config\Resource\DirectoryResource;
use Symfony\Component\Config\Resource\FileResource;
-use Symfony\Component\Config\ResourceCheckerInterface;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
@@ -208,15 +206,16 @@
use Symfony\Component\Uid\Factory\UuidFactory;
use Symfony\Component\Uid\UuidV4;
use Symfony\Component\Validator\Constraint;
-use Symfony\Component\Validator\Constraints\ExpressionLanguageProvider;
use Symfony\Component\Validator\ConstraintValidatorInterface;
+use Symfony\Component\Validator\Constraints\ExpressionLanguageProvider;
use Symfony\Component\Validator\GroupProviderInterface;
use Symfony\Component\Validator\Mapping\Loader\PropertyInfoLoader;
use Symfony\Component\Validator\ObjectInitializerInterface;
use Symfony\Component\Validator\Validation;
-use Symfony\Component\Webhook\Controller\WebhookController;
use Symfony\Component\WebLink\HttpHeaderSerializer;
+use Symfony\Component\Webhook\Controller\WebhookController;
use Symfony\Component\Workflow;
+use Symfony\Component\Workflow\Arc;
use Symfony\Component\Workflow\WorkflowInterface;
use Symfony\Component\Yaml\Command\LintCommand as BaseYamlLintCommand;
use Symfony\Component\Yaml\Yaml;
@@ -229,6 +228,8 @@
use Symfony\Contracts\Service\ResetInterface;
use Symfony\Contracts\Service\ServiceSubscriberInterface;
use Symfony\Contracts\Translation\LocaleAwareInterface;
+use phpDocumentor\Reflection\DocBlockFactoryInterface;
+use phpDocumentor\Reflection\Types\ContextFactory;
/**
* Process the configuration and prepare the dependency injection container with
@@ -1074,6 +1075,14 @@ private function registerWorkflowConfiguration(array $config, ContainerBuilder $
// Global transition counter per workflow
$transitionCounter = 0;
foreach ($workflow['transitions'] as $transition) {
+ foreach (['from', 'to'] as $direction) {
+ foreach ($transition[$direction] as $k => $arc) {
+ $transition[$direction][$k] = new Definition(Arc::class, [
+ '$place' => $arc['place'],
+ '$weight' => $arc['weight'] ?? 1,
+ ]);
+ }
+ }
if ('workflow' === $type) {
$transitionDefinition = new Definition(Workflow\Transition::class, [$transition['name'], $transition['from'], $transition['to']]);
$transitionId = \sprintf('.%s.transition.%s', $workflowId, $transitionCounter++);
@@ -1095,7 +1104,7 @@ private function registerWorkflowConfiguration(array $config, ContainerBuilder $
} elseif ('state_machine' === $type) {
foreach ($transition['from'] as $from) {
foreach ($transition['to'] as $to) {
- $transitionDefinition = new Definition(Workflow\Transition::class, [$transition['name'], $from, $to]);
+ $transitionDefinition = new Definition(Workflow\Transition::class, [$transition['name'], [$from], [$to]]);
$transitionId = \sprintf('.%s.transition.%s', $workflowId, $transitionCounter++);
$container->setDefinition($transitionId, $transitionDefinition);
$transitions[] = new Reference($transitionId);
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd
index c4ee3486dae87..9c1ed50ef5acf 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd
+++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd
@@ -502,14 +502,22 @@
-
-
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/workflow_with_multiple_transitions_with_same_name.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/workflow_with_multiple_transitions_with_same_name.php
index f4956eccb453c..93a415fd83ee5 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/workflow_with_multiple_transitions_with_same_name.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/workflow_with_multiple_transitions_with_same_name.php
@@ -22,13 +22,14 @@
'approved_by_spellchecker',
'published',
],
+ // We also test different configuration formats here
'transitions' => [
'request_review' => [
'from' => 'draft',
'to' => ['wait_for_journalist', 'wait_for_spellchecker'],
],
'journalist_approval' => [
- 'from' => 'wait_for_journalist',
+ 'from' => ['wait_for_journalist'],
'to' => 'approved_by_journalist',
],
'spellchecker_approval' => [
@@ -36,13 +37,13 @@
'to' => 'approved_by_spellchecker',
],
'publish' => [
- 'from' => ['approved_by_journalist', 'approved_by_spellchecker'],
+ 'from' => [['place' => 'approved_by_journalist', 'weight' => 1], 'approved_by_spellchecker'],
'to' => 'published',
],
'publish_editor_in_chief' => [
'name' => 'publish',
'from' => 'draft',
- 'to' => 'published',
+ 'to' => [['place' => 'published', 'weight' => 2]],
],
],
],
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflow_with_multiple_transitions_with_same_name.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflow_with_multiple_transitions_with_same_name.xml
index 0435447b6c6ce..b7f2724a50856 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflow_with_multiple_transitions_with_same_name.xml
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflow_with_multiple_transitions_with_same_name.xml
@@ -18,6 +18,7 @@
+
draft
wait_for_journalist
@@ -32,13 +33,13 @@
approved_by_spellchecker
- approved_by_journalist
- approved_by_spellchecker
+ approved_by_journalist
+ approved_by_spellchecker
published
draft
- published
+ published
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/workflow_with_multiple_transitions_with_same_name.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/workflow_with_multiple_transitions_with_same_name.yml
index 67eccb425a84e..a3f52e05de11b 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/workflow_with_multiple_transitions_with_same_name.yml
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/workflow_with_multiple_transitions_with_same_name.yml
@@ -17,20 +17,21 @@ framework:
- wait_for_spellchecker
- approved_by_spellchecker
- published
+ # We also test different configuration formats here
transitions:
request_review:
- from: [draft]
+ from: draft
to: [wait_for_journalist, wait_for_spellchecker]
journalist_approval:
from: [wait_for_journalist]
- to: [approved_by_journalist]
+ to: approved_by_journalist
spellchecker_approval:
- from: [wait_for_spellchecker]
- to: [approved_by_spellchecker]
+ from: wait_for_spellchecker
+ to: approved_by_spellchecker
publish:
- from: [approved_by_journalist, approved_by_spellchecker]
- to: [published]
+ from: [{place: approved_by_journalist, weight: 1}, approved_by_spellchecker]
+ to: published
publish_editor_in_chief:
name: publish
- from: [draft]
- to: [published]
+ from: draft
+ to: [{place: published, weight: 2}]
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php
index d942c122c826a..cb847c216feb6 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php
@@ -91,6 +91,7 @@
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Component\Webhook\Client\RequestParser;
use Symfony\Component\Webhook\Controller\WebhookController;
+use Symfony\Component\Workflow\Arc;
use Symfony\Component\Workflow\Exception\InvalidDefinitionException;
use Symfony\Component\Workflow\Metadata\InMemoryMetadataStore;
use Symfony\Component\Workflow\WorkflowEvents;
@@ -433,61 +434,96 @@ public function testWorkflowMultipleTransitionsWithSameName()
$this->assertCount(5, $transitions);
- $this->assertSame('.workflow.article.transition.0', (string) $transitions[0]);
- $this->assertSame([
+ $assertTransitions = function(
+ string $expectedServiceId,
+ string $expectedName,
+ array $expectedFroms,
+ array $expectedTos,
+ Reference $transition
+ ) use ($container) {
+ $this->assertSame($expectedServiceId, (string) $transition);
+ $args = $container->getDefinition($transition)->getArguments();
+ $this->assertCount(3, $args);
+ $this->assertSame($expectedName, $args[0]);
+
+ $this->assertCount(count($expectedFroms), $args[1]);
+ foreach ($expectedFroms as $i => ['place' => $place, 'weight' => $weight]) {
+ $this->assertInstanceOf(Definition::class, $args[1][$i]);
+ $this->assertSame(Arc::class, $args[1][$i]->getClass());
+ $this->assertSame($place, $args[1][$i]->getArgument('$place'));
+ $this->assertSame($weight, $args[1][$i]->getArgument('$weight'));
+ }
+
+ $this->assertCount(count($expectedTos), $args[2]);
+ foreach ($expectedTos as $i => ['place' => $place, 'weight' => $weight]) {
+ $this->assertInstanceOf(Definition::class, $args[2][$i]);
+ $this->assertSame(Arc::class, $args[2][$i]->getClass());
+ $this->assertSame($place, $args[2][$i]->getArgument('$place'));
+ $this->assertSame($weight, $args[2][$i]->getArgument('$weight'));
+ }
+ };
+
+ $assertTransitions(
+ '.workflow.article.transition.0',
'request_review',
[
- 'draft',
+ ['place' => 'draft', 'weight' => 1]
],
[
- 'wait_for_journalist', 'wait_for_spellchecker',
+ ['place' => 'wait_for_journalist', 'weight' => 1],
+ ['place' => 'wait_for_spellchecker', 'weight' => 1],
],
- ], $container->getDefinition($transitions[0])->getArguments());
+ $transitions[0]
+ );
- $this->assertSame('.workflow.article.transition.1', (string) $transitions[1]);
- $this->assertSame([
+ $assertTransitions(
+ '.workflow.article.transition.1',
'journalist_approval',
[
- 'wait_for_journalist',
+ ['place' => 'wait_for_journalist', 'weight' => 1],
],
[
- 'approved_by_journalist',
+ ['place' => 'approved_by_journalist', 'weight' => 1],
],
- ], $container->getDefinition($transitions[1])->getArguments());
+ $transitions[1]
+ );
- $this->assertSame('.workflow.article.transition.2', (string) $transitions[2]);
- $this->assertSame([
+ $assertTransitions(
+ '.workflow.article.transition.2',
'spellchecker_approval',
[
- 'wait_for_spellchecker',
+ ['place' => 'wait_for_spellchecker', 'weight' => 1],
],
[
- 'approved_by_spellchecker',
+ ['place' => 'approved_by_spellchecker', 'weight' => 1],
],
- ], $container->getDefinition($transitions[2])->getArguments());
+ $transitions[2]
+ );
- $this->assertSame('.workflow.article.transition.3', (string) $transitions[3]);
- $this->assertSame([
+ $assertTransitions(
+ '.workflow.article.transition.3',
'publish',
[
- 'approved_by_journalist',
- 'approved_by_spellchecker',
+ ['place' => 'approved_by_journalist', 'weight' => 1],
+ ['place' => 'approved_by_spellchecker', 'weight' => 1],
],
[
- 'published',
+ ['place' => 'published', 'weight' => 1],
],
- ], $container->getDefinition($transitions[3])->getArguments());
+ $transitions[3]
+ );
- $this->assertSame('.workflow.article.transition.4', (string) $transitions[4]);
- $this->assertSame([
+ $assertTransitions(
+ '.workflow.article.transition.4',
'publish',
[
- 'draft',
+ ['place' => 'draft', 'weight' => 1],
],
[
- 'published',
+ ['place' => 'published', 'weight' => 2],
],
- ], $container->getDefinition($transitions[4])->getArguments());
+ $transitions[4]
+ );
}
public function testWorkflowGuardExpressions()
diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json
index 2ecedbc45660e..68c5ff5d94809 100644
--- a/src/Symfony/Bundle/FrameworkBundle/composer.json
+++ b/src/Symfony/Bundle/FrameworkBundle/composer.json
@@ -67,7 +67,7 @@
"symfony/twig-bundle": "^6.4|^7.0",
"symfony/type-info": "^7.1",
"symfony/validator": "^6.4|^7.0",
- "symfony/workflow": "^6.4|^7.0",
+ "symfony/workflow": "^7.3",
"symfony/yaml": "^6.4|^7.0",
"symfony/property-info": "^6.4|^7.0",
"symfony/json-streamer": "7.3.*",
@@ -108,7 +108,7 @@
"symfony/validator": "<6.4",
"symfony/web-profiler-bundle": "<6.4",
"symfony/webhook": "<7.2",
- "symfony/workflow": "<6.4"
+ "symfony/workflow": "<7.3"
},
"autoload": {
"psr-4": { "Symfony\\Bundle\\FrameworkBundle\\": "" },
diff --git a/src/Symfony/Component/Workflow/Arc.php b/src/Symfony/Component/Workflow/Arc.php
new file mode 100644
index 0000000000000..9b91c60d63892
--- /dev/null
+++ b/src/Symfony/Component/Workflow/Arc.php
@@ -0,0 +1,30 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Workflow;
+
+/**
+ * @author Grégoire Pineau
+ */
+final readonly class Arc
+{
+ public function __construct(
+ public string $place,
+ public int $weight,
+ ) {
+ if ($weight < 1) {
+ throw new \InvalidArgumentException(\sprintf('The weight must be greater than 0, "%s" given.', $weight));
+ }
+ if (!$place) {
+ throw new \InvalidArgumentException('The place name cannot be empty.');
+ }
+ }
+}
diff --git a/src/Symfony/Component/Workflow/CHANGELOG.md b/src/Symfony/Component/Workflow/CHANGELOG.md
index 2926da4e6428d..3b61a5013dd54 100644
--- a/src/Symfony/Component/Workflow/CHANGELOG.md
+++ b/src/Symfony/Component/Workflow/CHANGELOG.md
@@ -1,13 +1,18 @@
CHANGELOG
=========
+7.3
+---
+
+ * Add support for weighted transitions
+
7.1
---
* Add method `getEnabledTransition()` to `WorkflowInterface`
* Automatically register places from transitions
* Add support for workflows that need to store many tokens in the marking
- * Add method `getName()` in event classes to build event names in subscribers
+ * Add method `getName()` in event classes to build event names in subscribers
7.0
---
diff --git a/src/Symfony/Component/Workflow/Definition.php b/src/Symfony/Component/Workflow/Definition.php
index 0b5697b758945..1bbbd25bf5402 100644
--- a/src/Symfony/Component/Workflow/Definition.php
+++ b/src/Symfony/Component/Workflow/Definition.php
@@ -104,15 +104,15 @@ private function addPlace(string $place): void
private function addTransition(Transition $transition): void
{
- foreach ($transition->getFroms() as $from) {
- if (!\array_key_exists($from, $this->places)) {
- $this->addPlace($from);
+ foreach ($transition->getFromArcs() as $arc) {
+ if (!\array_key_exists($arc->place, $this->places)) {
+ $this->addPlace($arc->place);
}
}
- foreach ($transition->getTos() as $to) {
- if (!\array_key_exists($to, $this->places)) {
- $this->addPlace($to);
+ foreach ($transition->getToArcs() as $arc) {
+ if (!\array_key_exists($arc->place, $this->places)) {
+ $this->addPlace($arc->place);
}
}
diff --git a/src/Symfony/Component/Workflow/Dumper/GraphvizDumper.php b/src/Symfony/Component/Workflow/Dumper/GraphvizDumper.php
index 2aaf54932aae5..9a875a02fc1ce 100644
--- a/src/Symfony/Component/Workflow/Dumper/GraphvizDumper.php
+++ b/src/Symfony/Component/Workflow/Dumper/GraphvizDumper.php
@@ -199,20 +199,22 @@ protected function findEdges(Definition $definition): array
foreach ($definition->getTransitions() as $i => $transition) {
$transitionName = $workflowMetadata->getMetadata('label', $transition) ?? $transition->getName();
- foreach ($transition->getFroms() as $from) {
+ foreach ($transition->getFromArcs() as $arc) {
$dotEdges[] = [
- 'from' => $from,
+ 'from' => $arc->place,
'to' => $transitionName,
'direction' => 'from',
'transition_number' => $i,
+ 'weight' => $arc->weight,
];
}
- foreach ($transition->getTos() as $to) {
+ foreach ($transition->getToArcs() as $arc) {
$dotEdges[] = [
'from' => $transitionName,
- 'to' => $to,
+ 'to' => $arc->place,
'direction' => 'to',
'transition_number' => $i,
+ 'weight' => $arc->weight,
];
}
}
@@ -229,14 +231,16 @@ protected function addEdges(array $edges): string
foreach ($edges as $edge) {
if ('from' === $edge['direction']) {
- $code .= \sprintf(" place_%s -> transition_%s [style=\"solid\"];\n",
+ $code .= \sprintf(" place_%s -> transition_%s [style=\"solid\"%s];\n",
$this->dotize($edge['from']),
- $this->dotize($edge['transition_number'])
+ $this->dotize($edge['transition_number']),
+ $edge['weight'] > 1 ? \sprintf(',label="%s"', $this->escape($edge['weight'])) : '',
);
} else {
- $code .= \sprintf(" transition_%s -> place_%s [style=\"solid\"];\n",
+ $code .= \sprintf(" transition_%s -> place_%s [style=\"solid\"%s];\n",
$this->dotize($edge['transition_number']),
- $this->dotize($edge['to'])
+ $this->dotize($edge['to']),
+ $edge['weight'] > 1 ? \sprintf(',label="%s"', $this->escape($edge['weight'])) : '',
);
}
}
diff --git a/src/Symfony/Component/Workflow/Dumper/MermaidDumper.php b/src/Symfony/Component/Workflow/Dumper/MermaidDumper.php
index bd7a6fada2168..9caef4263076a 100644
--- a/src/Symfony/Component/Workflow/Dumper/MermaidDumper.php
+++ b/src/Symfony/Component/Workflow/Dumper/MermaidDumper.php
@@ -11,6 +11,7 @@
namespace Symfony\Component\Workflow\Dumper;
+use Symfony\Component\Workflow\Arc;
use Symfony\Component\Workflow\Definition;
use Symfony\Component\Workflow\Exception\InvalidArgumentException;
use Symfony\Component\Workflow\Marking;
@@ -69,7 +70,8 @@ public function dump(Definition $definition, ?Marking $marking = null, array $op
$place,
$meta->getPlaceMetadata($place),
\in_array($place, $definition->getInitialPlaces(), true),
- $marking?->has($place) ?? false
+ $marking?->has($place) ?? false,
+ $marking?->getTokenCount($place) ?? 0
);
$output[] = $placeNode;
@@ -91,16 +93,15 @@ public function dump(Definition $definition, ?Marking $marking = null, array $op
$transitionLabel = $transitionMeta['label'];
}
- foreach ($transition->getFroms() as $from) {
- $from = $placeNameMap[$from];
-
- foreach ($transition->getTos() as $to) {
- $to = $placeNameMap[$to];
-
+ foreach ($transition->getFromArcs() as $fromArc) {
+ foreach ($transition->getToArcs() as $toArc) {
if (self::TRANSITION_TYPE_STATEMACHINE === $this->transitionType) {
+ $from = $placeNameMap[$fromArc->place];
+ $to = $placeNameMap[$toArc->place];
+
$transitionOutput = $this->styleStateMachineTransition($from, $to, $transitionLabel, $transitionMeta);
} else {
- $transitionOutput = $this->styleWorkflowTransition($from, $to, $transitionId, $transitionLabel, $transitionMeta);
+ $transitionOutput = $this->styleWorkflowTransition($placeNameMap, $fromArc, $toArc, $transitionId, $transitionLabel, $transitionMeta);
}
foreach ($transitionOutput as $line) {
@@ -122,12 +123,15 @@ public function dump(Definition $definition, ?Marking $marking = null, array $op
return implode("\n", $output);
}
- private function preparePlace(int $placeId, string $placeName, array $meta, bool $isInitial, bool $hasMarking): array
+ private function preparePlace(int $placeId, string $placeName, array $meta, bool $isInitial, bool $hasMarking, int $tokenCount): array
{
$placeLabel = $placeName;
if (\array_key_exists('label', $meta)) {
$placeLabel = $meta['label'];
}
+ if ($tokenCount > 1) {
+ $placeLabel .= ' ('.$tokenCount.')';
+ }
$placeLabel = $this->escape($placeLabel);
@@ -206,7 +210,7 @@ private function styleStateMachineTransition(string $from, string $to, string $t
return $transitionOutput;
}
- private function styleWorkflowTransition(string $from, string $to, int $transitionId, string $transitionLabel, array $transitionMeta): array
+ private function styleWorkflowTransition(array $placeNameMap, Arc $from, Arc $to, int $transitionId, string $transitionLabel, array $transitionMeta): array
{
$transitionOutput = [];
@@ -220,8 +224,11 @@ private function styleWorkflowTransition(string $from, string $to, int $transiti
$transitionOutput[] = $transitionNodeStyle;
}
- $connectionStyle = '%s-->%s';
- $transitionOutput[] = \sprintf($connectionStyle, $from, $transitionNodeName);
+ if ($from->weight > 1) {
+ $transitionOutput[] = \sprintf('%s-->|%d|%s', $placeNameMap[$from->place], $from->weight, $transitionNodeName);
+ } else {
+ $transitionOutput[] = \sprintf('%s-->%s', $placeNameMap[$from->place], $transitionNodeName);
+ }
$linkStyle = $this->styleLink($transitionMeta);
if ('' !== $linkStyle) {
@@ -230,7 +237,11 @@ private function styleWorkflowTransition(string $from, string $to, int $transiti
++$this->linkCount;
- $transitionOutput[] = \sprintf($connectionStyle, $transitionNodeName, $to);
+ if ($to->weight > 1) {
+ $transitionOutput[] = \sprintf('%s-->|%d|%s', $transitionNodeName, $to->weight, $placeNameMap[$to->place]);
+ } else {
+ $transitionOutput[] = \sprintf('%s-->%s', $transitionNodeName, $placeNameMap[$to->place]);
+ }
$linkStyle = $this->styleLink($transitionMeta);
if ('' !== $linkStyle) {
diff --git a/src/Symfony/Component/Workflow/Dumper/PlantUmlDumper.php b/src/Symfony/Component/Workflow/Dumper/PlantUmlDumper.php
index 9bd621ad59733..3e7b8c76421da 100644
--- a/src/Symfony/Component/Workflow/Dumper/PlantUmlDumper.php
+++ b/src/Symfony/Component/Workflow/Dumper/PlantUmlDumper.php
@@ -78,10 +78,10 @@ public function dump(Definition $definition, ?Marking $marking = null, array $op
}
foreach ($definition->getTransitions() as $transition) {
$transitionEscaped = $this->escape($transition->getName());
- foreach ($transition->getFroms() as $from) {
- $fromEscaped = $this->escape($from);
- foreach ($transition->getTos() as $to) {
- $toEscaped = $this->escape($to);
+ foreach ($transition->getFromArcs() as $fromArc) {
+ $fromEscaped = $this->escape($fromArc->place);
+ foreach ($transition->getToArcs() as $toArc) {
+ $toEscaped = $this->escape($toArc->place);
$transitionEscapedWithStyle = $this->getTransitionEscapedWithStyle($workflowMetadata, $transition, $transitionEscaped);
diff --git a/src/Symfony/Component/Workflow/Dumper/StateMachineGraphvizDumper.php b/src/Symfony/Component/Workflow/Dumper/StateMachineGraphvizDumper.php
index 7bd9d730fd026..10f2fcc50838a 100644
--- a/src/Symfony/Component/Workflow/Dumper/StateMachineGraphvizDumper.php
+++ b/src/Symfony/Component/Workflow/Dumper/StateMachineGraphvizDumper.php
@@ -65,14 +65,14 @@ protected function findEdges(Definition $definition): array
$attributes['color'] = $arrowColor;
}
- foreach ($transition->getFroms() as $from) {
- foreach ($transition->getTos() as $to) {
+ foreach ($transition->getFromArcs() as $fromArc) {
+ foreach ($transition->getToArcs() as $toArc) {
$edge = [
'name' => $transitionName,
- 'to' => $to,
+ 'to' => $toArc->place,
'attributes' => $attributes,
];
- $edges[$from][] = $edge;
+ $edges[$fromArc->place][] = $edge;
}
}
}
diff --git a/src/Symfony/Component/Workflow/EventListener/AuditTrailListener.php b/src/Symfony/Component/Workflow/EventListener/AuditTrailListener.php
index fe7ccdf48f327..1916fd63e491c 100644
--- a/src/Symfony/Component/Workflow/EventListener/AuditTrailListener.php
+++ b/src/Symfony/Component/Workflow/EventListener/AuditTrailListener.php
@@ -27,8 +27,8 @@ public function __construct(
public function onLeave(Event $event): void
{
- foreach ($event->getTransition()->getFroms() as $place) {
- $this->logger->info(\sprintf('Leaving "%s" for subject of class "%s" in workflow "%s".', $place, $event->getSubject()::class, $event->getWorkflowName()));
+ foreach ($event->getTransition()->getFromArcs() as $arc) {
+ $this->logger->info(\sprintf('Leaving "%s" for subject of class "%s" in workflow "%s".', $arc->place, $event->getSubject()::class, $event->getWorkflowName()));
}
}
@@ -39,8 +39,8 @@ public function onTransition(Event $event): void
public function onEnter(Event $event): void
{
- foreach ($event->getTransition()->getTos() as $place) {
- $this->logger->info(\sprintf('Entering "%s" for subject of class "%s" in workflow "%s".', $place, $event->getSubject()::class, $event->getWorkflowName()));
+ foreach ($event->getTransition()->getToArcs() as $arc) {
+ $this->logger->info(\sprintf('Entering "%s" for subject of class "%s" in workflow "%s".', $arc->place, $event->getSubject()::class, $event->getWorkflowName()));
}
}
diff --git a/src/Symfony/Component/Workflow/Marking.php b/src/Symfony/Component/Workflow/Marking.php
index c3629a2432798..b59b60c203d80 100644
--- a/src/Symfony/Component/Workflow/Marking.php
+++ b/src/Symfony/Component/Workflow/Marking.php
@@ -83,6 +83,11 @@ public function has(string $place): bool
return isset($this->places[$place]);
}
+ public function getTokenCount(string $place): int
+ {
+ return $this->places[$place] ?? 0;
+ }
+
public function getPlaces(): array
{
return $this->places;
diff --git a/src/Symfony/Component/Workflow/Tests/ArcTest.php b/src/Symfony/Component/Workflow/Tests/ArcTest.php
new file mode 100644
index 0000000000000..a3661705696e2
--- /dev/null
+++ b/src/Symfony/Component/Workflow/Tests/ArcTest.php
@@ -0,0 +1,34 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Workflow\Tests;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Workflow\Arc;
+
+class ArcTest extends TestCase
+{
+ public function testConstructorWithInvalidPlaceName()
+ {
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage('The place name cannot be empty.');
+
+ new Arc('', 1);
+ }
+
+ public function testConstructorWithInvalidWeight()
+ {
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage('The weight must be greater than 0, "0" given.');
+
+ new Arc('not empty', 0);
+ }
+}
diff --git a/src/Symfony/Component/Workflow/Tests/StateMachineTest.php b/src/Symfony/Component/Workflow/Tests/StateMachineTest.php
index 5d10fdef89613..a630392cd2ca0 100644
--- a/src/Symfony/Component/Workflow/Tests/StateMachineTest.php
+++ b/src/Symfony/Component/Workflow/Tests/StateMachineTest.php
@@ -88,7 +88,7 @@ public function testBuildTransitionBlockerListReturnsExpectedReasonOnBranchMerge
$net = new StateMachine($definition, null, $dispatcher);
$dispatcher->addListener('workflow.guard', function (GuardEvent $event) {
- $event->addTransitionBlocker(new TransitionBlocker(\sprintf('Transition blocker of place %s', $event->getTransition()->getFroms()[0]), 'blocker'));
+ $event->addTransitionBlocker(new TransitionBlocker(\sprintf('Transition blocker of place %s', $event->getTransition()->getFromArcs()[0]->place), 'blocker'));
});
$subject = new Subject();
@@ -124,7 +124,7 @@ public function testApplyReturnsExpectedReasonOnBranchMerge()
$net = new StateMachine($definition, null, $dispatcher);
$dispatcher->addListener('workflow.guard', function (GuardEvent $event) {
- $event->addTransitionBlocker(new TransitionBlocker(\sprintf('Transition blocker of place %s', $event->getTransition()->getFroms()[0]), 'blocker'));
+ $event->addTransitionBlocker(new TransitionBlocker(\sprintf('Transition blocker of place %s', $event->getTransition()->getFromArcs()[0]->place), 'blocker'));
});
$subject = new Subject();
diff --git a/src/Symfony/Component/Workflow/Tests/TransitionTest.php b/src/Symfony/Component/Workflow/Tests/TransitionTest.php
index aee514717a3f2..01c78aa8e2e1e 100644
--- a/src/Symfony/Component/Workflow/Tests/TransitionTest.php
+++ b/src/Symfony/Component/Workflow/Tests/TransitionTest.php
@@ -12,15 +12,49 @@
namespace Symfony\Component\Workflow\Tests;
use PHPUnit\Framework\TestCase;
+use Symfony\Component\Workflow\Arc;
use Symfony\Component\Workflow\Transition;
class TransitionTest extends TestCase
{
- public function testConstructor()
+ public static function provideConstructorTests(): iterable
{
- $transition = new Transition('name', 'a', 'b');
+ yield 'plain strings' => ['a', 'b'];
+ yield 'array of strings' => [['a'], ['b']];
+ yield 'array of arcs' => [[new Arc('a', 1)], [new Arc('b', 1)]];
+ }
+
+ /**
+ * @dataProvider provideConstructorTests
+ */
+ public function testConstructor(mixed $froms, mixed $tos)
+ {
+ $transition = new Transition('name', $froms, $tos);
$this->assertSame('name', $transition->getName());
+ $this->assertCount(1, $transition->getFromArcs());
+ $this->assertSame('a', $transition->getFromArcs()[0]->place);
+ $this->assertSame(1, $transition->getFromArcs()[0]->weight);
+ $this->assertCount(1, $transition->getToArcs());
+ $this->assertSame('b', $transition->getToArcs()[0]->place);
+ $this->assertSame(1, $transition->getToArcs()[0]->weight);
+ }
+
+ public function testConstructorWithInvalidData()
+ {
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage('The type of arc is invalid. Expected string or Arc, got "bool".');
+
+ new Transition('name', [true], ['a']);
+ }
+
+ /**
+ * @group legacy
+ */
+ public function testLegacyGetter()
+ {
+ $transition = new Transition('name', 'a', 'b');
+
$this->assertSame(['a'], $transition->getFroms());
$this->assertSame(['b'], $transition->getTos());
}
diff --git a/src/Symfony/Component/Workflow/Tests/Validator/StateMachineValidatorTest.php b/src/Symfony/Component/Workflow/Tests/Validator/StateMachineValidatorTest.php
index e88408bf693dd..e34c2b46e6f51 100644
--- a/src/Symfony/Component/Workflow/Tests/Validator/StateMachineValidatorTest.php
+++ b/src/Symfony/Component/Workflow/Tests/Validator/StateMachineValidatorTest.php
@@ -12,6 +12,7 @@
namespace Symfony\Component\Workflow\Tests\Validator;
use PHPUnit\Framework\TestCase;
+use Symfony\Component\Workflow\Arc;
use Symfony\Component\Workflow\Definition;
use Symfony\Component\Workflow\Exception\InvalidDefinitionException;
use Symfony\Component\Workflow\Transition;
@@ -125,4 +126,32 @@ public function testWithTooManyInitialPlaces()
(new StateMachineValidator())->validate($definition, 'foo');
}
+
+ public function testWithArcInFromTooHeavy()
+ {
+ $places = ['a', 'b'];
+ $transitions = [
+ new Transition('t1', [new Arc('a', 2)], [new Arc('b', 1)]),
+ ];
+ $definition = new Definition($places, $transitions);
+
+ $this->expectException(InvalidDefinitionException::class);
+ $this->expectExceptionMessage('A transition in StateMachine can only have arc with weight equals to one. But the transition "t1" in StateMachine "foo" has an arc from "a" to the transition with a weight equals to 2.');
+
+ (new StateMachineValidator())->validate($definition, 'foo');
+ }
+
+ public function testWithArcInToTooHeavy()
+ {
+ $places = ['a', 'b'];
+ $transitions = [
+ new Transition('t1', [new Arc('a', 1)], [new Arc('b', 2)]),
+ ];
+ $definition = new Definition($places, $transitions);
+
+ $this->expectException(InvalidDefinitionException::class);
+ $this->expectExceptionMessage('A transition in StateMachine can only have arc with weight equals to one. But the transition "t1" in StateMachine "foo" has an arc from the transition to "b" with a weight equals to 2.');
+
+ (new StateMachineValidator())->validate($definition, 'foo');
+ }
}
diff --git a/src/Symfony/Component/Workflow/Tests/Validator/WorkflowValidatorTest.php b/src/Symfony/Component/Workflow/Tests/Validator/WorkflowValidatorTest.php
index 49f04000fe4f3..ed2acc4dd1975 100644
--- a/src/Symfony/Component/Workflow/Tests/Validator/WorkflowValidatorTest.php
+++ b/src/Symfony/Component/Workflow/Tests/Validator/WorkflowValidatorTest.php
@@ -84,4 +84,32 @@ public function testWithTooManyInitialPlaces()
(new WorkflowValidator(true))->validate($definition, 'foo');
}
+
+ public function testWithArcInFromTooHeavy()
+ {
+ $places = ['a', 'b'];
+ $transitions = [
+ new Transition('t1', [new Arc('a', 2)], [new Arc('b', 1)]),
+ ];
+ $definition = new Definition($places, $transitions);
+
+ $this->expectException(InvalidDefinitionException::class);
+ $this->expectExceptionMessage('The marking store of workflow "t1" cannot store many places. But the transition "foo" has an arc from the transition to "a" with a weight equals to 2.');
+
+ (new WorkflowValidator(true))->validate($definition, 'foo');
+ }
+
+ public function testWithArcInToTooHeavy()
+ {
+ $places = ['a', 'b'];
+ $transitions = [
+ new Transition('t1', [new Arc('a', 1)], [new Arc('b', 2)]),
+ ];
+ $definition = new Definition($places, $transitions);
+
+ $this->expectException(InvalidDefinitionException::class);
+ $this->expectExceptionMessage('The marking store of workflow "t1" cannot store many places. But the transition "foo" has an arc from "b" to the transition with a weight equals to 2.');
+
+ (new WorkflowValidator(true))->validate($definition, 'foo');
+ }
}
diff --git a/src/Symfony/Component/Workflow/Tests/WorkflowTest.php b/src/Symfony/Component/Workflow/Tests/WorkflowTest.php
index 18cbaf0dd979e..9915fc1a0255c 100644
--- a/src/Symfony/Component/Workflow/Tests/WorkflowTest.php
+++ b/src/Symfony/Component/Workflow/Tests/WorkflowTest.php
@@ -13,6 +13,7 @@
use PHPUnit\Framework\TestCase;
use Symfony\Component\EventDispatcher\EventDispatcher;
+use Symfony\Component\Workflow\Arc;
use Symfony\Component\Workflow\Definition;
use Symfony\Component\Workflow\Event\Event;
use Symfony\Component\Workflow\Event\GuardEvent;
@@ -831,6 +832,133 @@ public function testApplyWithSameNameBackTransition(string $transition)
], $marking);
}
+ public function testWithArcAndWeight()
+ {
+ // ┌───────────────────┐ ┌─────────────┐ ┌─────────────┐ 4
+ // │ prepare_leg │ ──▶ │ build_leg │ ──▶ │ leg_created │ ───────────────────────────┐
+ // └───────────────────┘ └─────────────┘ └─────────────┘ │
+ // ▲ │
+ // │ 4 │
+ // │ ▼
+ // ┌──────┐ ┌───────────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌──────┐ ┌──────────┐
+ // │ init │ ──▶ │ start │ ──▶ │ prepare_top │ ──▶ │ build_top │ ───▶ │ top_created │ ──▶ │ join │ ──▶ │ finished │
+ // └──────┘ └───────────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ └──────┘ └──────────┘
+ // │ ▲
+ // │ │
+ // ▼ │
+ // ┌───────────────────┐ │
+ // │ stopwatch_running │ ───────────────────────────────────────────────────────────────────┘
+ // └───────────────────┘
+ //
+ // 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
+
+ $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);
+
+ $this->assertTrue($workflow->can($subject, 'start'));
+ $this->assertFalse($workflow->can($subject, 'build_leg'));
+ $this->assertFalse($workflow->can($subject, 'build_top'));
+ $this->assertFalse($workflow->can($subject, 'join'));
+
+ $workflow->apply($subject, 'start');
+
+ $this->assertSame([
+ 'prepare_leg' => 4,
+ 'prepare_top' => 1,
+ 'stopwatch_running' => 1,
+ ], $subject->getMarking());
+ $this->assertTrue($workflow->can($subject, 'build_leg'));
+ $this->assertTrue($workflow->can($subject, 'build_top'));
+ $this->assertFalse($workflow->can($subject, 'join'));
+
+ $workflow->apply($subject, 'build_leg');
+
+ $this->assertSame([
+ 'prepare_leg' => 3,
+ 'prepare_top' => 1,
+ 'stopwatch_running' => 1,
+ 'leg_created' => 1,
+ ], $subject->getMarking());
+ $this->assertTrue($workflow->can($subject, 'build_leg'));
+ $this->assertTrue($workflow->can($subject, 'build_top'));
+ $this->assertFalse($workflow->can($subject, 'join'));
+
+ $workflow->apply($subject, 'build_top');
+
+ $this->assertSame([
+ 'prepare_leg' => 3,
+ 'stopwatch_running' => 1,
+ 'leg_created' => 1,
+ 'top_created' => 1,
+ ], $subject->getMarking());
+ $this->assertTrue($workflow->can($subject, 'build_leg'));
+ $this->assertFalse($workflow->can($subject, 'build_top'));
+ $this->assertFalse($workflow->can($subject, 'join'));
+
+ $workflow->apply($subject, 'build_leg');
+
+ $this->assertSame([
+ 'prepare_leg' => 2,
+ 'stopwatch_running' => 1,
+ 'leg_created' => 2,
+ 'top_created' => 1,
+ ], $subject->getMarking());
+ $this->assertTrue($workflow->can($subject, 'build_leg'));
+ $this->assertFalse($workflow->can($subject, 'build_top'));
+ $this->assertFalse($workflow->can($subject, 'join'));
+
+ $workflow->apply($subject, 'build_leg');
+ $workflow->apply($subject, 'build_leg');
+
+ $this->assertSame([
+ 'stopwatch_running' => 1,
+ 'leg_created' => 4,
+ 'top_created' => 1,
+ ], $subject->getMarking());
+ $this->assertFalse($workflow->can($subject, 'build_leg'));
+ $this->assertFalse($workflow->can($subject, 'build_top'));
+ $this->assertTrue($workflow->can($subject, 'join'));
+
+ $workflow->apply($subject, 'join');
+
+ $this->assertSame([
+ 'finished' => 1,
+ ], $subject->getMarking());
+ }
+
private function assertPlaces(array $expected, Marking $marking)
{
$places = $marking->getPlaces();
diff --git a/src/Symfony/Component/Workflow/Transition.php b/src/Symfony/Component/Workflow/Transition.php
index 05fe26771fb5c..046019d98c911 100644
--- a/src/Symfony/Component/Workflow/Transition.php
+++ b/src/Symfony/Component/Workflow/Transition.php
@@ -17,20 +17,27 @@
*/
class Transition
{
- private array $froms;
- private array $tos;
+ /**
+ * @var Arc[]
+ */
+ private array $fromArcs;
+
+ /**
+ * @var Arc[]
+ */
+ private array $toArcs;
/**
- * @param string|string[] $froms
- * @param string|string[] $tos
+ * @param string|string[]|Arc[] $froms
+ * @param string|string[]|Arc[] $tos
*/
public function __construct(
private string $name,
string|array $froms,
string|array $tos,
) {
- $this->froms = (array) $froms;
- $this->tos = (array) $tos;
+ $this->fromArcs = array_map($this->normalize(...), (array) $froms);
+ $this->toArcs = array_map($this->normalize(...), (array) $tos);
}
public function getName(): string
@@ -39,18 +46,56 @@ public function getName(): string
}
/**
+ * @deprecated since Symfony 7.3, use getFromArcs() instead
+ *
* @return string[]
*/
public function getFroms(): array
{
- return $this->froms;
+ trigger_deprecation('symfony/workflow', '7.3', 'Method "%s()" is deprecated, use "getFromArcs()" instead.', __METHOD__);
+
+ return array_map(static fn (Arc $arc): string => $arc->place, $this->fromArcs);
+ }
+
+ /**
+ * @return Arc[]
+ */
+ public function getFromArcs(): array
+ {
+ return $this->fromArcs;
}
/**
+ * @deprecated since Symfony 7.3, use getFromArcs() instead
+ *
* @return string[]
*/
public function getTos(): array
{
- return $this->tos;
+ trigger_deprecation('symfony/workflow', '7.3', 'Method "%s()" is deprecated, use "getToArcs()" instead.', __METHOD__);
+
+ return array_map(static fn (Arc $arc): string => $arc->place, $this->toArcs);
+ }
+
+ /**
+ * @return Arc[]
+ */
+ public function getToArcs(): array
+ {
+ return $this->toArcs;
+ }
+
+ // No type hint for $arc to avoid implicit cast
+ private function normalize(mixed $arc): Arc
+ {
+ if ($arc instanceof Arc) {
+ return $arc;
+ }
+
+ if (is_string($arc)) {
+ return new Arc($arc, 1);
+ }
+
+ throw new \InvalidArgumentException(sprintf('The type of arc is invalid. Expected string or Arc, got "%s".', get_debug_type($arc)));
}
}
diff --git a/src/Symfony/Component/Workflow/Validator/StateMachineValidator.php b/src/Symfony/Component/Workflow/Validator/StateMachineValidator.php
index 626a20eea8af8..2e1d1998c1dd1 100644
--- a/src/Symfony/Component/Workflow/Validator/StateMachineValidator.php
+++ b/src/Symfony/Component/Workflow/Validator/StateMachineValidator.php
@@ -24,18 +24,29 @@ public function validate(Definition $definition, string $name): void
$transitionFromNames = [];
foreach ($definition->getTransitions() as $transition) {
// Make sure that each transition has exactly one TO
- if (1 !== \count($transition->getTos())) {
- throw new InvalidDefinitionException(\sprintf('A transition in StateMachine can only have one output. But the transition "%s" in StateMachine "%s" has %d outputs.', $transition->getName(), $name, \count($transition->getTos())));
+ if (1 !== \count($transition->getToArcs())) {
+ throw new InvalidDefinitionException(\sprintf('A transition in StateMachine can only have one output. But the transition "%s" in StateMachine "%s" has %d outputs.', $transition->getName(), $name, \count($transition->getToArcs())));
+ }
+ foreach ($transition->getFromArcs() as $arc) {
+ if (1 < $arc->weight) {
+ throw new InvalidDefinitionException(\sprintf('A transition in StateMachine can only have arc with weight equals to one. But the transition "%s" in StateMachine "%s" has an arc from "%s" to the transition with a weight equals to %d.', $transition->getName(), $name, $arc->place, $arc->weight));
+ }
}
// Make sure that each transition has exactly one FROM
- $froms = $transition->getFroms();
- if (1 !== \count($froms)) {
- throw new InvalidDefinitionException(\sprintf('A transition in StateMachine can only have one input. But the transition "%s" in StateMachine "%s" has %d inputs.', $transition->getName(), $name, \count($froms)));
+ $fromArcs = $transition->getFromArcs();
+ if (1 !== \count($fromArcs)) {
+ throw new InvalidDefinitionException(\sprintf('A transition in StateMachine can only have one input. But the transition "%s" in StateMachine "%s" has %d inputs.', $transition->getName(), $name, \count($fromArcs)));
+ }
+ foreach ($transition->getToArcs() as $arc) {
+ if (1 < $arc->weight) {
+ throw new InvalidDefinitionException(\sprintf('A transition in StateMachine can only have arc with weight equals to one. But the transition "%s" in StateMachine "%s" has an arc from the transition to "%s" with a weight equals to %d.', $transition->getName(), $name, $arc->place, $arc->weight));
+ }
}
// Enforcing uniqueness of the names of transitions starting at each node
- $from = reset($froms);
+ $fromArc = reset($fromArcs);
+ $from = $fromArc->place;
if (isset($transitionFromNames[$from][$transition->getName()])) {
throw new InvalidDefinitionException(\sprintf('A transition from a place/state must have an unique name. Multiple transitions named "%s" from place/state "%s" were found on StateMachine "%s".', $transition->getName(), $from, $name));
}
diff --git a/src/Symfony/Component/Workflow/Validator/WorkflowValidator.php b/src/Symfony/Component/Workflow/Validator/WorkflowValidator.php
index f4eb2926692f0..0760ecb22e586 100644
--- a/src/Symfony/Component/Workflow/Validator/WorkflowValidator.php
+++ b/src/Symfony/Component/Workflow/Validator/WorkflowValidator.php
@@ -30,7 +30,8 @@ public function validate(Definition $definition, string $name): void
// Make sure all transitions for one place has unique name.
$places = array_fill_keys($definition->getPlaces(), []);
foreach ($definition->getTransitions() as $transition) {
- foreach ($transition->getFroms() as $from) {
+ foreach ($transition->getFromArcs() as $arc) {
+ $from = $arc->place;
if (\in_array($transition->getName(), $places[$from], true)) {
throw new InvalidDefinitionException(\sprintf('All transitions for a place must have an unique name. Multiple transitions named "%s" where found for place "%s" in workflow "%s".', $transition->getName(), $from, $name));
}
@@ -43,8 +44,19 @@ public function validate(Definition $definition, string $name): void
}
foreach ($definition->getTransitions() as $transition) {
- if (1 < \count($transition->getTos())) {
- throw new InvalidDefinitionException(\sprintf('The marking store of workflow "%s" cannot store many places. But the transition "%s" has too many output (%d). Only one is accepted.', $name, $transition->getName(), \count($transition->getTos())));
+ if (1 < \count($transition->getToArcs())) {
+ throw new InvalidDefinitionException(\sprintf('The marking store of workflow "%s" cannot store many places. But the transition "%s" has too many output (%d). Only one is accepted.', $name, $transition->getName(), \count($transition->getToArcs())));
+ }
+
+ foreach ($transition->getFromArcs() as $arc) {
+ if (1 < $arc->weight) {
+ throw new InvalidDefinitionException(\sprintf('The marking store of workflow "%s" cannot store many places. But the transition "%s" has an arc from the transition to "%s" with a weight equals to %d.', $transition->getName(), $name, $arc->place, $arc->weight));
+ }
+ }
+ foreach ($transition->getToArcs() as $arc) {
+ if (1 < $arc->weight) {
+ throw new InvalidDefinitionException(\sprintf('The marking store of workflow "%s" cannot store many places. But the transition "%s" has an arc from "%s" to the transition with a weight equals to %d.', $transition->getName(), $name, $arc->place, $arc->weight));
+ }
}
}
diff --git a/src/Symfony/Component/Workflow/Workflow.php b/src/Symfony/Component/Workflow/Workflow.php
index 04b0084dd5fbd..141805e825933 100644
--- a/src/Symfony/Component/Workflow/Workflow.php
+++ b/src/Symfony/Component/Workflow/Workflow.php
@@ -283,8 +283,8 @@ public function getMetadataStore(): MetadataStoreInterface
private function buildTransitionBlockerListForTransition(object $subject, Marking $marking, Transition $transition): TransitionBlockerList
{
- foreach ($transition->getFroms() as $place) {
- if (!$marking->has($place)) {
+ foreach ($transition->getFromArcs() as $arc) {
+ if ($marking->getTokenCount($arc->place) < $arc->weight) {
return new TransitionBlockerList([
TransitionBlocker::createBlockedByMarking($marking),
]);
@@ -321,7 +321,7 @@ private function guardTransition(object $subject, Marking $marking, Transition $
private function leave(object $subject, Transition $transition, Marking $marking, array $context = []): void
{
- $places = $transition->getFroms();
+ $arcs = $transition->getFromArcs();
if ($this->shouldDispatchEvent(WorkflowEvents::LEAVE, $context)) {
$event = new LeaveEvent($subject, $marking, $transition, $this, $context);
@@ -329,13 +329,13 @@ private function leave(object $subject, Transition $transition, Marking $marking
$this->dispatcher->dispatch($event, WorkflowEvents::LEAVE);
$this->dispatcher->dispatch($event, \sprintf('workflow.%s.leave', $this->name));
- foreach ($places as $place) {
- $this->dispatcher->dispatch($event, \sprintf('workflow.%s.leave.%s', $this->name, $place));
+ foreach ($arcs as $arc) {
+ $this->dispatcher->dispatch($event, \sprintf('workflow.%s.leave.%s', $this->name, $arc->place));
}
}
- foreach ($places as $place) {
- $marking->unmark($place);
+ foreach ($arcs as $arc) {
+ $marking->unmark($arc->place, $arc->weight);
}
}
@@ -356,7 +356,7 @@ private function transition(object $subject, Transition $transition, Marking $ma
private function enter(object $subject, Transition $transition, Marking $marking, array $context): void
{
- $places = $transition->getTos();
+ $arcs = $transition->getToArcs();
if ($this->shouldDispatchEvent(WorkflowEvents::ENTER, $context)) {
$event = new EnterEvent($subject, $marking, $transition, $this, $context);
@@ -364,13 +364,13 @@ private function enter(object $subject, Transition $transition, Marking $marking
$this->dispatcher->dispatch($event, WorkflowEvents::ENTER);
$this->dispatcher->dispatch($event, \sprintf('workflow.%s.enter', $this->name));
- foreach ($places as $place) {
- $this->dispatcher->dispatch($event, \sprintf('workflow.%s.enter.%s', $this->name, $place));
+ foreach ($arcs as $arc) {
+ $this->dispatcher->dispatch($event, \sprintf('workflow.%s.enter.%s', $this->name, $arc->place));
}
}
- foreach ($places as $place) {
- $marking->mark($place);
+ foreach ($arcs as $arc) {
+ $marking->mark($arc->place, $arc->weight);
}
}
diff --git a/src/Symfony/Component/Workflow/composer.json b/src/Symfony/Component/Workflow/composer.json
index 44a300057c04b..d259e1589d405 100644
--- a/src/Symfony/Component/Workflow/composer.json
+++ b/src/Symfony/Component/Workflow/composer.json
@@ -20,15 +20,16 @@
}
],
"require": {
- "php": ">=8.2"
+ "php": ">=8.2",
+ "symfony/deprecation-contracts": "^2.5|^3.0"
},
"require-dev": {
"psr/log": "^1|^2|^3",
"symfony/dependency-injection": "^6.4|^7.0",
- "symfony/event-dispatcher": "^6.4|^7.0",
"symfony/error-handler": "^6.4|^7.0",
- "symfony/http-kernel": "^6.4|^7.0",
+ "symfony/event-dispatcher": "^6.4|^7.0",
"symfony/expression-language": "^6.4|^7.0",
+ "symfony/http-kernel": "^6.4|^7.0",
"symfony/security-core": "^6.4|^7.0",
"symfony/stopwatch": "^6.4|^7.0",
"symfony/validator": "^6.4|^7.0"