Skip to content

Commit d644e43

Browse files
committed
[Workflow] Add support for storing the marking in a property
1 parent deb160a commit d644e43

File tree

6 files changed

+300
-21
lines changed

6 files changed

+300
-21
lines changed

src/Symfony/Component/Workflow/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ CHANGELOG
66

77
* Add `with-metadata` option to the `workflow:dump` command to include places,
88
transitions and workflow's metadata into dumped graph
9+
* Add support for storing marking in a property
910

1011
6.2
1112
---

src/Symfony/Component/Workflow/MarkingStore/MethodMarkingStore.php

Lines changed: 71 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,49 +11,49 @@
1111

1212
namespace Symfony\Component\Workflow\MarkingStore;
1313

14+
use Symfony\Component\DependencyInjection\Tests\Compiler\D;
1415
use Symfony\Component\Workflow\Exception\LogicException;
1516
use Symfony\Component\Workflow\Marking;
1617

1718
/**
18-
* MethodMarkingStore stores the marking with a subject's method.
19+
* MethodMarkingStore stores the marking with a subject's public method if exist,
20+
* then to a property.
1921
*
2022
* This store deals with a "single state" or "multiple state" Marking.
2123
*
2224
* "single state" Marking means a subject can be in one and only one state at
23-
* the same time. Use it with state machine.
25+
* the same time. Use it with state machine. It uses a string to store the marking
2426
*
2527
* "multiple state" Marking means a subject can be in many states at the same
26-
* time. Use it with workflow.
28+
* time. Use it with workflow. It uses an array to store the marking
2729
*
2830
* @author Grégoire Pineau <lyrixx@lyrixx.info>
2931
*/
3032
final class MethodMarkingStore implements MarkingStoreInterface
3133
{
32-
private bool $singleState;
33-
private string $property;
34+
private readonly \SplObjectStorage $getters;
35+
private readonly \SplObjectStorage $setters;
3436

3537
/**
3638
* @param string $property Used to determine methods to call
3739
* The `getMarking` method will use `$subject->getProperty()`
3840
* The `setMarking` method will use `$subject->setProperty(string|array $places, array $context = array())`
3941
*/
40-
public function __construct(bool $singleState = false, string $property = 'marking')
41-
{
42-
$this->singleState = $singleState;
43-
$this->property = $property;
42+
public function __construct(
43+
private bool $singleState = false,
44+
private string $property = 'marking',
45+
) {
46+
$this->getters = new \SplObjectStorage();
47+
$this->setters = new \SplObjectStorage();
4448
}
4549

4650
public function getMarking(object $subject): Marking
4751
{
48-
$method = 'get'.ucfirst($this->property);
49-
50-
if (!method_exists($subject, $method)) {
51-
throw new LogicException(sprintf('The method "%s::%s()" does not exist.', get_debug_type($subject), $method));
52-
}
52+
$getAccessor = $this->getGetter($subject);
5353

5454
$marking = null;
5555
try {
56-
$marking = $subject->{$method}();
56+
$marking = $getAccessor();
5757
} catch (\Error $e) {
5858
$unInitializedPropertyMessage = sprintf('Typed property %s::$%s must not be accessed before initialization', get_debug_type($subject), $this->property);
5959
if ($e->getMessage() !== $unInitializedPropertyMessage) {
@@ -68,7 +68,7 @@ public function getMarking(object $subject): Marking
6868
if ($this->singleState) {
6969
$marking = [(string) $marking => 1];
7070
} elseif (!\is_array($marking)) {
71-
throw new LogicException(sprintf('The method "%s::%s()" did not return an array and the Workflow\'s Marking store is instantiated with $singleState=false.', get_debug_type($subject), $method));
71+
throw new LogicException(sprintf('The marking stored in "%s::%s" is not an array and the Workflow\'s Marking store is instantiated with $singleState=false.', get_debug_type($subject), $this->property));
7272
}
7373

7474
return new Marking($marking);
@@ -82,12 +82,64 @@ public function setMarking(object $subject, Marking $marking, array $context = [
8282
$marking = key($marking);
8383
}
8484

85-
$method = 'set'.ucfirst($this->property);
85+
$setter = $this->getSetter($subject);
86+
87+
$setter($marking, $context);
88+
}
89+
90+
private function getGetter(object $subject): callable
91+
{
92+
$propertyName = $this->property;
93+
94+
return $this->getters[$subject] ??= match ($this->getCallable($subject, $propertyName, $m = 'get'.ucfirst($propertyName))) {
95+
MarkingStoreMethod::METHOD => static fn () => $subject->{$m}(),
96+
MarkingStoreMethod::PROPERTY => static fn () => $subject->{$propertyName},
97+
};
98+
}
99+
100+
private function getSetter(object $subject): callable
101+
{
102+
$propertyName = $this->property;
103+
104+
return $this->setters[$subject] ??= match ($this->getCallable($subject, $propertyName, $m = 'set'.ucfirst($propertyName))) {
105+
MarkingStoreMethod::METHOD => static fn ($marking, $context) => $subject->{$m}($marking, $context),
106+
MarkingStoreMethod::PROPERTY => static fn ($marking) => $subject->{$propertyName} = $marking,
107+
};
108+
}
86109

110+
private function getCallable(object $subject, string $propertyName, string $method): MarkingStoreMethod
111+
{
87112
if (!method_exists($subject, $method)) {
88-
throw new LogicException(sprintf('The method "%s::%s()" does not exist.', get_debug_type($subject), $method));
113+
goto property;
89114
}
90115

91-
$subject->{$method}($marking, $context);
116+
try {
117+
$r = new \ReflectionMethod($subject, $method);
118+
} catch (\ReflectionException) {
119+
property:
120+
try {
121+
$r = new \ReflectionProperty($subject, $propertyName);
122+
} catch (\ReflectionException) {
123+
throw new LogicException(sprintf('The public property "%1$s::%2$s" nor the public method "%1$s::%3$s()" exist. At least one must be declared.', get_debug_type($subject), $propertyName, $method));
124+
}
125+
126+
if (!$r->isPublic()) {
127+
throw new LogicException(sprintf('The public method "%1$s::%3$s()" must be declared, or the property "%1$s::%2$s" must be public.', get_debug_type($subject), $propertyName, $method));
128+
}
129+
130+
return MarkingStoreMethod::PROPERTY;
131+
}
132+
133+
if (!$r->isPublic()) {
134+
throw new LogicException(sprintf('The method "%s::%s()" must be public.', get_debug_type($subject), $method));
135+
}
136+
137+
return MarkingStoreMethod::METHOD;
92138
}
93139
}
140+
141+
enum MarkingStoreMethod
142+
{
143+
case METHOD;
144+
case PROPERTY;
145+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Workflow\MarkingStore;
13+
14+
use Symfony\Component\Workflow\Exception\LogicException;
15+
use Symfony\Component\Workflow\Marking;
16+
17+
/**
18+
* PropertiesMarkingStore stores the marking with a subject's properties.
19+
*
20+
* This store deals with a "single state" or "multiple state" Marking.
21+
*
22+
* "single state" Marking means a subject can be in one and only one state at
23+
* the same time. Use it with state machine. It uses a string to store the marking
24+
*
25+
* "multiple state" Marking means a subject can be in many states at the same
26+
* time. Use it with workflow. It uses an array to store the marking
27+
*
28+
* @author Grégoire Pineau <lyrixx@lyrixx.info>
29+
*/
30+
final class PropertiesMarkingStore implements MarkingStoreInterface
31+
{
32+
public function __construct(
33+
private bool $singleState = false,
34+
private string $property = 'marking',
35+
private ?string $contextProperty = 'markingContext',
36+
) {
37+
}
38+
39+
public function getMarking(object $subject): Marking
40+
{
41+
if (!property_exists($subject, $this->property)) {
42+
throw new LogicException(sprintf('The property "%s::$%s" does not exist.', get_debug_type($subject), $this->property));
43+
}
44+
45+
$marking = null;
46+
try {
47+
$marking = $subject->{$this->property};
48+
} catch (\Error $e) {
49+
$unInitializedPropertyMessage = sprintf('Typed property %s::$%s must not be accessed before initialization', get_debug_type($subject), $this->property);
50+
if ($e->getMessage() !== $unInitializedPropertyMessage) {
51+
throw $e;
52+
}
53+
}
54+
55+
if (null === $marking) {
56+
return new Marking();
57+
}
58+
59+
if ($this->singleState) {
60+
$marking = [(string) $marking => 1];
61+
} elseif (!\is_array($marking)) {
62+
throw new LogicException(sprintf('The property "%s::$%s" did not return an array and the Workflow\'s Marking store is instantiated with $singleState=false.', get_debug_type($subject), $this->property));
63+
}
64+
65+
return new Marking($marking);
66+
}
67+
68+
public function setMarking(object $subject, Marking $marking, array $context = []): void
69+
{
70+
$marking = $marking->getPlaces();
71+
72+
if ($this->singleState) {
73+
$marking = key($marking);
74+
}
75+
76+
if (!property_exists($subject, $this->property)) {
77+
throw new LogicException(sprintf('The property "%s::$%s" does not exist.', get_debug_type($subject), $this->property));
78+
}
79+
$subject->{$this->property} = $marking;
80+
81+
if (null !== $this->contextProperty) {
82+
if (!property_exists($subject, $this->contextProperty)) {
83+
throw new LogicException(sprintf('The property "%s::$%s" does not exist.', get_debug_type($subject), $this->contextProperty));
84+
}
85+
$subject->{$this->contextProperty} = $context;
86+
}
87+
}
88+
}

src/Symfony/Component/Workflow/Tests/MarkingStore/MethodMarkingStoreTest.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,10 @@ public function testGetSetMarkingWithMultipleState()
2929

3030
$marking->mark('first_place');
3131

32-
$markingStore->setMarking($subject, $marking);
32+
$markingStore->setMarking($subject, $marking, ['foo' => 'bar']);
3333

3434
$this->assertSame(['first_place' => 1], $subject->getMarking());
35+
$this->assertSame(['foo' => 'bar'], $subject->getContext());
3536

3637
$marking2 = $markingStore->getMarking($subject);
3738

@@ -50,11 +51,12 @@ public function testGetSetMarkingWithSingleState()
5051

5152
$marking->mark('first_place');
5253

53-
$markingStore->setMarking($subject, $marking);
54+
$markingStore->setMarking($subject, $marking, ['foo' => 'bar']);
5455

5556
$this->assertSame('first_place', $subject->getMarking());
5657

5758
$marking2 = $markingStore->getMarking($subject);
59+
$this->assertSame(['foo' => 'bar'], $subject->getContext());
5860

5961
$this->assertEquals($marking, $marking2);
6062
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Workflow\Tests\MarkingStore;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\Workflow\MarkingStore\MethodMarkingStore;
16+
use Symfony\Component\Workflow\Tests\SubjectWithProperties;
17+
18+
class PropertiesMarkingStoreTest extends TestCase
19+
{
20+
public function testGetSetMarkingWithMultipleState()
21+
{
22+
$subject = new SubjectWithProperties();
23+
24+
$markingStore = new MethodMarkingStore(false);
25+
26+
$marking = $markingStore->getMarking($subject);
27+
28+
$this->assertCount(0, $marking->getPlaces());
29+
30+
$marking->mark('first_place');
31+
32+
$markingStore->setMarking($subject, $marking, ['foo' => 'bar']);
33+
34+
$this->assertSame(['first_place' => 1], $subject->marking);
35+
36+
$marking2 = $markingStore->getMarking($subject);
37+
38+
$this->assertEquals($marking, $marking2);
39+
}
40+
41+
public function testGetSetMarkingWithSingleState()
42+
{
43+
$subject = new SubjectWithProperties();
44+
45+
$markingStore = new MethodMarkingStore(true, 'place', 'placeContext');
46+
47+
$marking = $markingStore->getMarking($subject);
48+
49+
$this->assertCount(0, $marking->getPlaces());
50+
51+
$marking->mark('first_place');
52+
53+
$markingStore->setMarking($subject, $marking, ['foo' => 'bar']);
54+
55+
$this->assertSame('first_place', $subject->place);
56+
57+
$marking2 = $markingStore->getMarking($subject);
58+
59+
$this->assertEquals($marking, $marking2);
60+
}
61+
62+
public function testGetSetMarkingWithSingleStateAndAlmostEmptyPlaceName()
63+
{
64+
$subject = new SubjectWithProperties();
65+
$subject->place = 0;
66+
67+
$markingStore = new MethodMarkingStore(true, 'place');
68+
69+
$marking = $markingStore->getMarking($subject);
70+
71+
$this->assertCount(1, $marking->getPlaces());
72+
}
73+
74+
public function testGetMarkingWithValueObject()
75+
{
76+
$subject = new SubjectWithProperties();
77+
$subject->place = $this->createValueObject('first_place');
78+
79+
$markingStore = new MethodMarkingStore(true, 'place');
80+
81+
$marking = $markingStore->getMarking($subject);
82+
83+
$this->assertCount(1, $marking->getPlaces());
84+
$this->assertSame('first_place', (string) $subject->place);
85+
}
86+
87+
public function testGetMarkingWithUninitializedProperty()
88+
{
89+
$subject = new SubjectWithProperties();
90+
91+
$markingStore = new MethodMarkingStore(true, 'place');
92+
93+
$marking = $markingStore->getMarking($subject);
94+
95+
$this->assertCount(0, $marking->getPlaces());
96+
}
97+
98+
private function createValueObject(string $markingValue): object
99+
{
100+
return new class($markingValue) {
101+
/** @var string */
102+
private $markingValue;
103+
104+
public function __construct(string $markingValue)
105+
{
106+
$this->markingValue = $markingValue;
107+
}
108+
109+
public function __toString(): string
110+
{
111+
return $this->markingValue;
112+
}
113+
};
114+
}
115+
}

0 commit comments

Comments
 (0)