Skip to content

Commit

Permalink
feature #54272 [Workflow] Add support for workflows that need to stor…
Browse files Browse the repository at this point in the history
…e many tokens in the marking (lyrixx)

This PR was merged into the 7.1 branch.

Discussion
----------

[Workflow] Add support for workflows that need to store many tokens in the marking

| Q             | A
| ------------- | ---
| Branch?       | 7.1
| Bug fix?      | yes
| New feature?  | no
| Deprecations? | no
| Issues        | Fix #53179
| License       | MIT

Commits
-------

5b4e9a9 [Workflow] Add support for workflows that need to store many tokens in the marking
  • Loading branch information
lyrixx committed Mar 15, 2024
2 parents 965283a + 5b4e9a9 commit 83872ca
Show file tree
Hide file tree
Showing 6 changed files with 207 additions and 22 deletions.
1 change: 1 addition & 0 deletions UPGRADE-7.1.md
Expand Up @@ -46,3 +46,4 @@ Workflow
--------

* Add method `getEnabledTransition()` to `WorkflowInterface`
* Add `$nbToken` argument to `Marking::mark()` and `Marking::unmark()`
1 change: 1 addition & 0 deletions src/Symfony/Component/Workflow/CHANGELOG.md
Expand Up @@ -6,6 +6,7 @@ CHANGELOG

* Add method `getEnabledTransition()` to `WorkflowInterface`
* Automatically register places from transitions
* Add support for workflows that need to store many tokens in the marking

7.0
---
Expand Down
49 changes: 43 additions & 6 deletions src/Symfony/Component/Workflow/Marking.php
Expand Up @@ -22,23 +22,60 @@ class Marking
private ?array $context = null;

/**
* @param int[] $representation Keys are the place name and values should be 1
* @param int[] $representation Keys are the place name and values should be superior or equals to 1
*/
public function __construct(array $representation = [])
{
foreach ($representation as $place => $nbToken) {
$this->mark($place);
$this->mark($place, $nbToken);
}
}

public function mark(string $place): void
/**
* @param int $nbToken
*
* @psalm-param int<1, max> $nbToken
*/
public function mark(string $place /* , int $nbToken = 1 */): void
{
$this->places[$place] = 1;
$nbToken = 1 < \func_num_args() ? func_get_arg(1) : 1;

if ($nbToken < 1) {
throw new \InvalidArgumentException(sprintf('The number of tokens must be greater than 0, "%s" given.', $nbToken));
}

$this->places[$place] ??= 0;
$this->places[$place] += $nbToken;
}

public function unmark(string $place): void
/**
* @param int $nbToken
*
* @psalm-param int<1, max> $nbToken
*/
public function unmark(string $place /* , int $nbToken = 1 */): void
{
unset($this->places[$place]);
$nbToken = 1 < \func_num_args() ? func_get_arg(1) : 1;

if ($nbToken < 1) {
throw new \InvalidArgumentException(sprintf('The number of tokens must be greater than 0, "%s" given.', $nbToken));
}

if (!$this->has($place)) {
throw new \InvalidArgumentException(sprintf('The place "%s" is not marked.', $place));
}

$tokenCount = $this->places[$place] - $nbToken;

if (0 > $tokenCount) {
throw new \InvalidArgumentException(sprintf('The place "%s" could not contain a negative token number: "%s" (initial) - "%s" (nbToken) = "%s".', $place, $this->places[$place], $nbToken, $tokenCount));
}

if (0 === $tokenCount) {
unset($this->places[$place]);
} else {
$this->places[$place] = $tokenCount;
}
}

public function has(string $place): bool
Expand Down
54 changes: 50 additions & 4 deletions src/Symfony/Component/Workflow/Tests/MarkingTest.php
Expand Up @@ -22,24 +22,70 @@ public function testMarking()

$this->assertTrue($marking->has('a'));
$this->assertFalse($marking->has('b'));
$this->assertSame(['a' => 1], $marking->getPlaces());
$this->assertPlaces(['a' => 1], $marking);

$marking->mark('b');

$this->assertTrue($marking->has('a'));
$this->assertTrue($marking->has('b'));
$this->assertSame(['a' => 1, 'b' => 1], $marking->getPlaces());
$this->assertPlaces(['a' => 1, 'b' => 1], $marking);

$marking->unmark('a');

$this->assertFalse($marking->has('a'));
$this->assertTrue($marking->has('b'));
$this->assertSame(['b' => 1], $marking->getPlaces());
$this->assertPlaces(['b' => 1], $marking);

$marking->unmark('b');

$this->assertFalse($marking->has('a'));
$this->assertFalse($marking->has('b'));
$this->assertSame([], $marking->getPlaces());
$this->assertPlaces([], $marking);

$marking->mark('a');
$this->assertPlaces(['a' => 1], $marking);

$marking->mark('a');
$this->assertPlaces(['a' => 2], $marking);

$marking->unmark('a');
$this->assertPlaces(['a' => 1], $marking);

$marking->unmark('a');
$this->assertPlaces([], $marking);
}

public function testGuardNotMarked()
{
$marking = new Marking([]);

$this->expectException(\LogicException::class);
$this->expectExceptionMessage('The place "a" is not marked.');
$marking->unmark('a');
}

public function testUnmarkGuardResultTokenCountIsNotNegative()
{
$marking = new Marking(['a' => 1]);

$this->expectException(\LogicException::class);
$this->expectExceptionMessage('The place "a" could not contain a negative token number: "1" (initial) - "2" (nbToken) = "-1".');
$marking->unmark('a', 2);
}

public function testUnmarkGuardNbTokenIsGreaterThanZero()
{
$marking = new Marking(['a' => 1]);

$this->expectException(\LogicException::class);
$this->expectExceptionMessage('The number of tokens must be greater than 0, "0" given.');
$marking->unmark('a', 0);
}

private function assertPlaces(array $expected, Marking $marking)
{
$places = $marking->getPlaces();
ksort($places);
$this->assertSame($expected, $places);
}
}
39 changes: 39 additions & 0 deletions src/Symfony/Component/Workflow/Tests/WorkflowBuilderTrait.php
Expand Up @@ -158,4 +158,43 @@ private static function createComplexStateMachineDefinition(): Definition
// | d | -------------+
// +-----+
}

private static function createWorkflowWithSameNameBackTransition(): Definition
{
$places = range('a', 'c');

$transitions = [];
$transitions[] = new Transition('a_to_bc', 'a', ['b', 'c']);
$transitions[] = new Transition('back1', 'b', 'a');
$transitions[] = new Transition('back1', 'c', 'b');
$transitions[] = new Transition('back2', 'c', 'b');
$transitions[] = new Transition('back2', 'b', 'a');
$transitions[] = new Transition('c_to_cb', 'c', ['b', 'c']);

return new Definition($places, $transitions);

// The graph looks like:
// +-----------------------------------------------------------------+
// | |
// | |
// | +---------------------------------------------+ |
// v | v |
// +---+ +---------+ +-------+ +---------+ +---+ +-------+
// | a | --> | a_to_bc | --> | | --> | back2 | --> | | --> | back2 |
// +---+ +---------+ | | +---------+ | | +-------+
// ^ | | | |
// | | c | <-----+ | b |
// | | | | | |
// | | | +---------+ | | +-------+
// | | | --> | c_to_cb | --> | | --> | back1 |
// | +-------+ +---------+ +---+ +-------+
// | | ^ |
// | | | |
// | v | |
// | +-------+ | |
// | | back1 | ----------------------+ |
// | +-------+ |
// | |
// +-----------------------------------------------------------------+
}
}
85 changes: 73 additions & 12 deletions src/Symfony/Component/Workflow/Tests/WorkflowTest.php
Expand Up @@ -319,28 +319,32 @@ public function testApplyWithSameNameTransition()

$marking = $workflow->apply($subject, 'a_to_bc');

$this->assertFalse($marking->has('a'));
$this->assertTrue($marking->has('b'));
$this->assertTrue($marking->has('c'));
$this->assertPlaces([
'b' => 1,
'c' => 1,
], $marking);

$marking = $workflow->apply($subject, 'to_a');

$this->assertTrue($marking->has('a'));
$this->assertFalse($marking->has('b'));
$this->assertFalse($marking->has('c'));
// Two tokens in "a"
$this->assertPlaces([
'a' => 2,
], $marking);

$workflow->apply($subject, 'a_to_bc');
$marking = $workflow->apply($subject, 'b_to_c');

$this->assertFalse($marking->has('a'));
$this->assertFalse($marking->has('b'));
$this->assertTrue($marking->has('c'));
$this->assertPlaces([
'a' => 1,
'c' => 2,
], $marking);

$marking = $workflow->apply($subject, 'to_a');

$this->assertTrue($marking->has('a'));
$this->assertFalse($marking->has('b'));
$this->assertFalse($marking->has('c'));
$this->assertPlaces([
'a' => 2,
'c' => 1,
], $marking);
}

public function testApplyWithSameNameTransition2()
Expand Down Expand Up @@ -776,6 +780,63 @@ public function testGetEnabledTransitionsWithSameNameTransition()
$this->assertSame('to_a', $transitions[1]->getName());
$this->assertSame('to_a', $transitions[2]->getName());
}

/**
* @@testWith ["back1"]
* ["back2"]
*/
public function testApplyWithSameNameBackTransition(string $transition)
{
$definition = $this->createWorkflowWithSameNameBackTransition();
$workflow = new Workflow($definition, new MethodMarkingStore());

$subject = new Subject();

$marking = $workflow->apply($subject, 'a_to_bc');
$this->assertPlaces([
'b' => 1,
'c' => 1,
], $marking);

$marking = $workflow->apply($subject, $transition);
$this->assertPlaces([
'a' => 1,
'b' => 1,
], $marking);

$marking = $workflow->apply($subject, $transition);
$this->assertPlaces([
'a' => 2,
], $marking);

$marking = $workflow->apply($subject, 'a_to_bc');
$this->assertPlaces([
'a' => 1,
'b' => 1,
'c' => 1,
], $marking);

$marking = $workflow->apply($subject, 'c_to_cb');
$this->assertPlaces([
'a' => 1,
'b' => 2,
'c' => 1,
], $marking);

$marking = $workflow->apply($subject, 'c_to_cb');
$this->assertPlaces([
'a' => 1,
'b' => 3,
'c' => 1,
], $marking);
}

private function assertPlaces(array $expected, Marking $marking)
{
$places = $marking->getPlaces();
ksort($places);
$this->assertSame($expected, $places);
}
}

class EventDispatcherMock implements \Symfony\Contracts\EventDispatcher\EventDispatcherInterface
Expand Down

0 comments on commit 83872ca

Please sign in to comment.