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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy