Added support for many inital places

This commit is contained in:
Grégoire Pineau 2019-03-06 21:59:33 +01:00
parent fc826aac4c
commit 1af1bf29ef
37 changed files with 250 additions and 45 deletions

View File

@ -127,6 +127,27 @@ Security
}
```
Workflow
--------
* `initial_place` is deprecated in favour of `initial_places`.
Before:
```yaml
framework:
workflows:
article:
initial_place: draft
```
After:
```yaml
framework:
workflows:
article:
initial_places: [draft]
```
Yaml
----

View File

@ -368,6 +368,7 @@ Workflow
* `SupportStrategyInterface` has been removed, use `WorkflowSupportStrategyInterface` instead.
* `ClassInstanceSupportStrategy` has been removed, use `InstanceOfSupportStrategy` instead.
* `MarkingStoreInterface::setMarking()` has a third argument: `array $context = []`.
* Removed support of `initial_place`. Use `initial_places` instead.
Yaml
----

View File

@ -231,7 +231,7 @@ class Configuration implements ConfigurationInterface
$workflows = [];
}
if (1 === \count($workflows) && isset($workflows['workflows']) && array_keys($workflows['workflows']) !== range(0, \count($workflows) - 1) && !empty(array_diff(array_keys($workflows['workflows']), ['audit_trail', 'type', 'marking_store', 'supports', 'support_strategy', 'initial_place', 'places', 'transitions']))) {
if (1 === \count($workflows) && isset($workflows['workflows']) && array_keys($workflows['workflows']) !== range(0, \count($workflows) - 1) && !empty(array_diff(array_keys($workflows['workflows']), ['audit_trail', 'type', 'marking_store', 'supports', 'support_strategy', 'initial_places', 'places', 'transitions']))) {
$workflows = $workflows['workflows'];
}
@ -258,6 +258,7 @@ class Configuration implements ConfigurationInterface
->prototype('array')
->fixXmlConfig('support')
->fixXmlConfig('place')
->fixXmlConfig('initial_place')
->fixXmlConfig('transition')
->children()
->arrayNode('audit_trail')
@ -312,8 +313,17 @@ class Configuration implements ConfigurationInterface
->cannotBeEmpty()
->end()
->scalarNode('initial_place')
->setDeprecated('The "%path%.%node%" configuration key has been deprecated in Symfony 4.3, use the "initial_places" configuration key instead.')
->defaultNull()
->end()
->arrayNode('initial_places')
->beforeNormalization()
->ifTrue(function ($v) { return !\is_array($v); })
->then(function ($v) { return [$v]; })
->end()
->defaultValue([])
->prototype('scalar')->end()
->end()
->arrayNode('places')
->beforeNormalization()
->always()

View File

@ -615,14 +615,14 @@ class FrameworkExtension extends Extension
// Create places
$places = array_column($workflow['places'], 'name');
$initialPlace = $workflow['initial_place'] ?? null;
$initialPlaces = $workflow['initial_places'] ?? $workflow['initial_place'] ?? [];
// Create a Definition
$definitionDefinition = new Definition(Workflow\Definition::class);
$definitionDefinition->setPublic(false);
$definitionDefinition->addArgument($places);
$definitionDefinition->addArgument($transitions);
$definitionDefinition->addArgument($initialPlace);
$definitionDefinition->addArgument($initialPlaces);
$definitionDefinition->addArgument($metadataStoreDefinition);
// Create MarkingStore
@ -670,7 +670,7 @@ class FrameworkExtension extends Extension
->addTransitions(array_map(function (Reference $ref) use ($container): Workflow\Transition {
return $container->get((string) $ref);
}, $transitions))
->setInitialPlace($initialPlace)
->setInitialPlace($initialPlaces)
->build()
;
$validator->validate($realDefinition, $name);

View File

@ -270,6 +270,7 @@
<xsd:complexType name="workflow">
<xsd:sequence>
<xsd:element name="initial-place" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="marking-store" type="marking_store" minOccurs="0" maxOccurs="1" />
<xsd:element name="support" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="place" type="place" minOccurs="0" maxOccurs="unbounded" />

View File

@ -0,0 +1,25 @@
<?php
use Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTest;
$container->loadFromExtension('framework', [
'workflows' => [
'legacy' => [
'type' => 'workflow',
'supports' => [
stdClass::class,
],
'initial_place' => 'draft',
'places' => [
'draft',
'published',
],
'transitions' => [
'publish' => [
'from' => 'draft',
'to' => 'published',
],
],
],
],
]);

View File

@ -12,7 +12,7 @@ $container->loadFromExtension('framework', [
'supports' => [
FrameworkExtensionTest::class,
],
'initial_place' => 'draft',
'initial_places' => ['draft'],
'places' => [
'draft',
'wait_for_journalist',

View File

@ -12,7 +12,7 @@ $container->loadFromExtension('framework', [
'supports' => [
FrameworkExtensionTest::class,
],
'initial_place' => 'draft',
'initial_places' => ['draft'],
'places' => [
'draft',
'wait_for_journalist',

View File

@ -12,7 +12,7 @@ $container->loadFromExtension('framework', [
'supports' => [
FrameworkExtensionTest::class,
],
'initial_place' => 'draft',
'initial_places' => ['draft'],
'places' => [
'draft',
'wait_for_journalist',
@ -47,7 +47,7 @@ $container->loadFromExtension('framework', [
'supports' => [
FrameworkExtensionTest::class,
],
'initial_place' => 'start',
'initial_places' => ['start'],
'metadata' => [
'title' => 'workflow title',
],

View File

@ -6,7 +6,7 @@ $container->loadFromExtension('framework', [
'foo' => [
'type' => 'workflow',
'supports' => ['Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTest'],
'initial_place' => 'bar',
'initial_places' => ['bar'],
'places' => ['bar', 'baz'],
'transitions' => [
'bar_baz' => [

View File

@ -6,7 +6,7 @@ $container->loadFromExtension('framework', [
'workflows' => [
'type' => 'workflow',
'supports' => ['Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTest'],
'initial_place' => 'bar',
'initial_places' => ['bar'],
'places' => ['bar', 'baz'],
'transitions' => [
'bar_baz' => [

View File

@ -0,0 +1,20 @@
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:framework="http://symfony.com/schema/dic/symfony"
xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd
http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
<framework:config>
<framework:workflow name="legacy" type="workflow" initial-place="draft">
<framework:support>stdClass</framework:support>
<framework:place name="draft"></framework:place>
<framework:place name="published"></framework:place>
<framework:transition name="publish">
<framework:from>draft</framework:from>
<framework:to>published</framework:to>
</framework:transition>
</framework:workflow>
</framework:config>
</container>

View File

@ -7,7 +7,8 @@
http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
<framework:config>
<framework:workflow name="article" type="workflow" initial-place="draft">
<framework:workflow name="article" type="workflow">
<framework:initial-place>draft</framework:initial-place>
<framework:marking-store type="multiple_state">
<framework:argument>a</framework:argument>
<framework:argument>a</framework:argument>

View File

@ -7,7 +7,8 @@
http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
<framework:config>
<framework:workflow name="article" type="workflow" initial-place="draft">
<framework:workflow name="article" type="workflow">
<framework:initial-place>draft</framework:initial-place>
<framework:marking-store type="multiple_state">
<framework:argument>a</framework:argument>
<framework:argument>a</framework:argument>

View File

@ -7,7 +7,8 @@
http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
<framework:config>
<framework:workflow name="article" type="workflow" initial-place="draft">
<framework:workflow name="article" type="workflow">
<framework:initial-place>draft</framework:initial-place>
<framework:marking-store type="multiple_state">
<framework:argument>a</framework:argument>
<framework:argument>a</framework:argument>

View File

@ -6,7 +6,8 @@
http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
<framework:config>
<framework:workflow enabled="true" name="foo" type="workflow" initial-place="bar">
<framework:workflow enabled="true" name="foo" type="workflow">
<framework:initial-place>bar</framework:initial-place>
<framework:support>Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTest</framework:support>
<framework:place>bar</framework:place>
<framework:place>baz</framework:place>

View File

@ -6,7 +6,8 @@
http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
<framework:config>
<framework:workflow enabled="true" name="workflows" type="workflow" initial-place="bar">
<framework:workflow enabled="true" name="workflows" type="workflow">
<framework:initial-place>bar</framework:initial-place>
<framework:support>Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTest</framework:support>
<framework:place>bar</framework:place>
<framework:place>baz</framework:place>

View File

@ -0,0 +1,14 @@
framework:
workflows:
legacy:
type: workflow
initial_place: draft
supports:
- stdClass
places:
- draft
- published
transitions:
publish:
from: draft
to: published

View File

@ -6,7 +6,7 @@ framework:
type: multiple_state
supports:
- Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTest
initial_place: draft
initial_places: [draft]
places:
- draft
- wait_for_journalist

View File

@ -6,7 +6,7 @@ framework:
type: multiple_state
supports:
- Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTest
initial_place: draft
initial_places: [draft]
places:
- draft
- wait_for_journalist

View File

@ -6,7 +6,7 @@ framework:
type: multiple_state
supports:
- Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTest
initial_place: draft
initial_places: [draft]
places:
# simple format
- draft
@ -33,7 +33,7 @@ framework:
type: single_state
supports:
- Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTest
initial_place: start
initial_places: [start]
metadata:
title: workflow title
places:

View File

@ -6,7 +6,7 @@ framework:
type: workflow
supports:
- Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTest
initial_place: bar
initial_places: [bar]
places:
- bar
- baz

View File

@ -5,7 +5,7 @@ framework:
type: workflow
supports:
- Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTest
initial_place: bar
initial_places: [bar]
places:
- bar
- baz

View File

@ -217,7 +217,7 @@ abstract class FrameworkExtensionTest extends TestCase
'Places are passed to the workflow definition'
);
$this->assertCount(4, $workflowDefinition->getArgument(1));
$this->assertSame('draft', $workflowDefinition->getArgument(2));
$this->assertSame(['draft'], $workflowDefinition->getArgument(2));
$this->assertTrue($container->hasDefinition('state_machine.pull_request'), 'State machine is registered as a service');
$this->assertSame('state_machine.abstract', $container->getDefinition('state_machine.pull_request')->getParent());
@ -238,7 +238,7 @@ abstract class FrameworkExtensionTest extends TestCase
'Places are passed to the state machine definition'
);
$this->assertCount(9, $stateMachineDefinition->getArgument(1));
$this->assertSame('start', $stateMachineDefinition->getArgument(2));
$this->assertSame(['start'], $stateMachineDefinition->getArgument(2));
$metadataStoreDefinition = $stateMachineDefinition->getArgument(3);
$this->assertInstanceOf(Definition::class, $metadataStoreDefinition);
@ -271,6 +271,28 @@ abstract class FrameworkExtensionTest extends TestCase
$this->assertGreaterThan(0, \count($registryDefinition->getMethodCalls()));
}
public function testWorkflowLegacy()
{
$container = $this->createContainerFromFile('workflow-legacy');
$this->assertTrue($container->hasDefinition('workflow.legacy'), 'Workflow is registered as a service');
$this->assertSame('workflow.abstract', $container->getDefinition('workflow.legacy')->getParent());
$this->assertTrue($container->hasDefinition('workflow.legacy.definition'), 'Workflow definition is registered as a service');
$workflowDefinition = $container->getDefinition('workflow.legacy.definition');
$this->assertSame(['draft'], $workflowDefinition->getArgument(2));
$this->assertSame(
[
'draft',
'published',
],
$workflowDefinition->getArgument(0),
'Places are passed to the workflow definition'
);
}
/**
* @expectedException \Symfony\Component\Workflow\Exception\InvalidDefinitionException
* @expectedExceptionMessage A transition from a place/state must have an unique name. Multiple transitions named "go" from place/state "first" where found on StateMachine "my_workflow".

View File

@ -54,7 +54,7 @@
"symfony/twig-bundle": "~2.8|~3.2|~4.0",
"symfony/validator": "^4.1",
"symfony/var-dumper": "~3.4|~4.0",
"symfony/workflow": "^4.1",
"symfony/workflow": "^4.3",
"symfony/yaml": "~3.4|~4.0",
"symfony/property-info": "~3.4|~4.0",
"symfony/lock": "~3.4|~4.0",
@ -79,7 +79,7 @@
"symfony/translation": "<4.3",
"symfony/twig-bridge": "<4.1.1",
"symfony/validator": "<4.1",
"symfony/workflow": "<4.1"
"symfony/workflow": "<4.3"
},
"suggest": {
"ext-apcu": "For best performance of the system caches",

View File

@ -26,6 +26,7 @@ CHANGELOG
* Dispatch `EnteredEvent` on `workflow.entered`
* Dispatch `CompletedEvent` on `workflow.completed`
* Dispatch `AnnounceEvent` on `workflow.announce`
* Added support for many `initialPlaces`
4.1.0
-----

View File

@ -24,14 +24,15 @@ final class Definition
{
private $places = [];
private $transitions = [];
private $initialPlace;
private $initialPlaces = [];
private $metadataStore;
/**
* @param string[] $places
* @param Transition[] $transitions
* @param string[] $places
* @param Transition[] $transitions
* @param string|string[]|null $initialPlaces
*/
public function __construct(array $places, array $transitions, string $initialPlace = null, MetadataStoreInterface $metadataStore = null)
public function __construct(array $places, array $transitions, $initialPlaces = null, MetadataStoreInterface $metadataStore = null)
{
foreach ($places as $place) {
$this->addPlace($place);
@ -41,17 +42,33 @@ final class Definition
$this->addTransition($transition);
}
$this->setInitialPlace($initialPlace);
$this->setInitialPlaces($initialPlaces);
$this->metadataStore = $metadataStore ?: new InMemoryMetadataStore();
}
/**
* @deprecated since Symfony 4.3. Use the getInitialPlaces() instead.
*
* @return string|null
*/
public function getInitialPlace()
{
return $this->initialPlace;
@trigger_error(sprintf('Calling %s::getInitialPlace() is deprecated. Call %s::getInitialPlaces() instead.', __CLASS__, __CLASS__));
if (!$this->initialPlaces) {
return null;
}
return reset($this->initialPlaces);
}
/**
* @return string[]
*/
public function getInitialPlaces(): array
{
return $this->initialPlaces;
}
/**
@ -75,23 +92,27 @@ final class Definition
return $this->metadataStore;
}
private function setInitialPlace(string $place = null)
private function setInitialPlaces($places = null)
{
if (null === $place) {
if (null === $places) {
return;
}
if (!isset($this->places[$place])) {
throw new LogicException(sprintf('Place "%s" cannot be the initial place as it does not exist.', $place));
$places = (array) $places;
foreach ($places as $place) {
if (!isset($this->places[$place])) {
throw new LogicException(sprintf('Place "%s" cannot be the initial place as it does not exist.', $place));
}
}
$this->initialPlace = $place;
$this->initialPlaces = $places;
}
private function addPlace(string $place)
{
if (!\count($this->places)) {
$this->initialPlace = $place;
$this->initialPlaces = [$place];
}
$this->places[$place] = $place;

View File

@ -69,7 +69,7 @@ class GraphvizDumper implements DumperInterface
foreach ($definition->getPlaces() as $place) {
$attributes = [];
if ($place === $definition->getInitialPlace()) {
if (\in_array($place, $definition->getInitialPlaces(), true)) {
$attributes['style'] = 'filled';
}
if ($marking && $marking->has($place)) {

View File

@ -199,7 +199,7 @@ class PlantUmlDumper implements DumperInterface
$placeEscaped = $this->escape($place);
$output = "state $placeEscaped".
($definition->getInitialPlace() === $place ? ' '.self::INITIAL : '').
(\in_array($place, $definition->getInitialPlaces(), true) ? ' '.self::INITIAL : '').
($marking && $marking->has($place) ? ' '.self::MARKED : '');
$backgroundColor = $workflowMetadata->getMetadata('bg_color', $place);

View File

@ -15,7 +15,7 @@ class DefinitionBuilderTest extends TestCase
$builder->setInitialPlace('b');
$definition = $builder->build();
$this->assertEquals('b', $definition->getInitialPlace());
$this->assertEquals(['b'], $definition->getInitialPlaces());
}
public function testAddTransition()

View File

@ -15,7 +15,7 @@ class DefinitionTest extends TestCase
$this->assertCount(5, $definition->getPlaces());
$this->assertEquals('a', $definition->getInitialPlace());
$this->assertEquals(['a'], $definition->getInitialPlaces());
}
public function testSetInitialPlace()
@ -23,7 +23,15 @@ class DefinitionTest extends TestCase
$places = range('a', 'e');
$definition = new Definition($places, [], $places[3]);
$this->assertEquals($places[3], $definition->getInitialPlace());
$this->assertEquals([$places[3]], $definition->getInitialPlaces());
}
public function testSetInitialPlaces()
{
$places = range('a', 'e');
$definition = new Definition($places, [], ['a', 'e']);
$this->assertEquals(['a', 'e'], $definition->getInitialPlaces());
}
/**

View File

@ -109,4 +109,32 @@ class StateMachineValidatorTest extends TestCase
// | t2 | --> | c |
// +----+ +----+
}
/**
* @expectedException \Symfony\Component\Workflow\Exception\InvalidDefinitionException
* @expectedExceptionMessage The state machine "foo" can not store many places. But the definition has 2 initial places. Only one is supported.
*/
public function testWithTooManyInitialPlaces()
{
$places = range('a', 'c');
$transitions = [];
$definition = new Definition($places, $transitions, ['a', 'b']);
(new StateMachineValidator())->validate($definition, 'foo');
// the test ensures that the validation does not fail (i.e. it does not throw any exceptions)
$this->addToAssertionCount(1);
// The graph looks like:
//
// +----+ +----+ +---+
// | a | --> | t1 | --> | b |
// +----+ +----+ +---+
// |
// |
// v
// +----+ +----+
// | t2 | --> | c |
// +----+ +----+
}
}

View File

@ -51,6 +51,20 @@ class WorkflowValidatorTest extends TestCase
(new WorkflowValidator())->validate($definition, 'foo');
}
/**
* @expectedException \Symfony\Component\Workflow\Exception\InvalidDefinitionException
* @expectedExceptionMessage The marking store of workflow "foo" can not store many places. But the definition has 2 initial places. Only one is supported.
*/
public function testWithTooManyInitialPlaces()
{
$places = range('a', 'c');
$transitions = [];
$definition = new Definition($places, $transitions, ['a', 'b']);
(new WorkflowValidator(true))->validate($definition, 'foo');
}
public function testSameTransitionNameButNotSamePlace()
{
$places = range('a', 'd');

View File

@ -16,6 +16,7 @@ use Symfony\Component\Workflow\Exception\InvalidDefinitionException;
/**
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*/
interface DefinitionValidatorInterface
{

View File

@ -37,10 +37,15 @@ class StateMachineValidator implements DefinitionValidatorInterface
// Enforcing uniqueness of the names of transitions starting at each node
$from = reset($froms);
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" where found on StateMachine "%s". ', $transition->getName(), $from, $name));
throw new InvalidDefinitionException(sprintf('A transition from a place/state must have an unique name. Multiple transitions named "%s" from place/state "%s" where found on StateMachine "%s".', $transition->getName(), $from, $name));
}
$transitionFromNames[$from][$transition->getName()] = true;
}
$initialPlaces = $definition->getInitialPlaces();
if (2 <= count($initialPlaces)) {
throw new InvalidDefinitionException(sprintf('The state machine "%s" can not store many places. But the definition has %s initial places. Only one is supported.', $name, \count($initialPlaces)));
}
}
}

View File

@ -16,6 +16,7 @@ use Symfony\Component\Workflow\Exception\InvalidDefinitionException;
/**
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*/
class WorkflowValidator implements DefinitionValidatorInterface
{
@ -48,5 +49,10 @@ class WorkflowValidator implements DefinitionValidatorInterface
throw new InvalidDefinitionException(sprintf('The marking store of workflow "%s" can not store many places. But the transition "%s" has too many output (%d). Only one is accepted.', $name, $transition->getName(), \count($transition->getTos())));
}
}
$initialPlaces = $definition->getInitialPlaces();
if (2 <= count($initialPlaces)) {
throw new InvalidDefinitionException(sprintf('The marking store of workflow "%s" can not store many places. But the definition has %s initial places. Only one is supported.', $name, \count($initialPlaces)));
}
}
}

View File

@ -61,10 +61,12 @@ class Workflow implements WorkflowInterface
// check if the subject is already in the workflow
if (!$marking->getPlaces()) {
if (!$this->definition->getInitialPlace()) {
if (!$this->definition->getInitialPlaces()) {
throw new LogicException(sprintf('The Marking is empty and there is no initial place for workflow "%s".', $this->name));
}
$marking->mark($this->definition->getInitialPlace());
foreach ($this->definition->getInitialPlaces() as $place) {
$marking->mark($place);
}
// update the subject with the new marking
$this->markingStore->setMarking($subject, $marking);