diff --git a/src/Symfony/Component/Workflow/CHANGELOG.md b/src/Symfony/Component/Workflow/CHANGELOG.md index ff7162853d0fa..1fc1373198bbd 100644 --- a/src/Symfony/Component/Workflow/CHANGELOG.md +++ b/src/Symfony/Component/Workflow/CHANGELOG.md @@ -6,6 +6,7 @@ CHANGELOG * Add `with-metadata` option to the `workflow:dump` command to include places, transitions and workflow's metadata into dumped graph + * Add support for storing marking in a property 6.2 --- diff --git a/src/Symfony/Component/Workflow/MarkingStore/MethodMarkingStore.php b/src/Symfony/Component/Workflow/MarkingStore/MethodMarkingStore.php index 78d3307e6ac6c..773328f150e14 100644 --- a/src/Symfony/Component/Workflow/MarkingStore/MethodMarkingStore.php +++ b/src/Symfony/Component/Workflow/MarkingStore/MethodMarkingStore.php @@ -15,45 +15,43 @@ use Symfony\Component\Workflow\Marking; /** - * MethodMarkingStore stores the marking with a subject's method. + * MethodMarkingStore stores the marking with a subject's public method + * or public property. * - * This store deals with a "single state" or "multiple state" Marking. + * This store deals with a "single state" or "multiple state" marking. * - * "single state" Marking means a subject can be in one and only one state at - * the same time. Use it with state machine. + * "single state" marking means a subject can be in one and only one state at + * the same time. Use it with state machine. It uses a string to store the + * marking. * - * "multiple state" Marking means a subject can be in many states at the same - * time. Use it with workflow. + * "multiple state" marking means a subject can be in many states at the same + * time. Use it with workflow. It uses an array of strings to store the marking. * * @author Grégoire Pineau */ final class MethodMarkingStore implements MarkingStoreInterface { - private bool $singleState; - private string $property; + /** @var array */ + private array $getters = []; + /** @var array */ + private array $setters = []; /** - * @param string $property Used to determine methods to call - * The `getMarking` method will use `$subject->getProperty()` - * The `setMarking` method will use `$subject->setProperty(string|array $places, array $context = array())` + * @param string $property Used to determine methods or property to call + * The `getMarking` method will use `$subject->getProperty()` or `$subject->property` + * The `setMarking` method will use `$subject->setProperty(string|array $places, array $context = [])` or `$subject->property = string|array $places` */ - public function __construct(bool $singleState = false, string $property = 'marking') - { - $this->singleState = $singleState; - $this->property = $property; + public function __construct( + private bool $singleState = false, + private string $property = 'marking', + ) { } public function getMarking(object $subject): Marking { - $method = 'get'.ucfirst($this->property); - - if (!method_exists($subject, $method)) { - throw new LogicException(sprintf('The method "%s::%s()" does not exist.', get_debug_type($subject), $method)); - } - $marking = null; try { - $marking = $subject->{$method}(); + $marking = ($this->getGetter($subject))(); } catch (\Error $e) { $unInitializedPropertyMessage = sprintf('Typed property %s::$%s must not be accessed before initialization', get_debug_type($subject), $this->property); if ($e->getMessage() !== $unInitializedPropertyMessage) { @@ -68,7 +66,7 @@ public function getMarking(object $subject): Marking if ($this->singleState) { $marking = [(string) $marking => 1]; } elseif (!\is_array($marking)) { - 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)); + 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)); } return new Marking($marking); @@ -82,12 +80,53 @@ public function setMarking(object $subject, Marking $marking, array $context = [ $marking = key($marking); } - $method = 'set'.ucfirst($this->property); + ($this->getSetter($subject))($marking, $context); + } + + private function getGetter(object $subject): callable + { + $property = $this->property; + $method = 'get'.ucfirst($property); + + return match ($this->getters[$subject::class] ??= $this->getType($subject, $property, $method)) { + MarkingStoreMethod::METHOD => $subject->{$method}(...), + MarkingStoreMethod::PROPERTY => static fn () => $subject->{$property}, + }; + } - if (!method_exists($subject, $method)) { - throw new LogicException(sprintf('The method "%s::%s()" does not exist.', get_debug_type($subject), $method)); + private function getSetter(object $subject): callable + { + $property = $this->property; + $method = 'set'.ucfirst($property); + + return match ($this->setters[$subject::class] ??= $this->getType($subject, $property, $method)) { + MarkingStoreMethod::METHOD => $subject->{$method}(...), + MarkingStoreMethod::PROPERTY => static fn ($marking) => $subject->{$property} = $marking, + }; + } + + private static function getType(object $subject, string $property, string $method): MarkingStoreMethod + { + if (method_exists($subject, $method) && (new \ReflectionMethod($subject, $method))->isPublic()) { + return MarkingStoreMethod::METHOD; + } + + try { + if ((new \ReflectionProperty($subject, $property))->isPublic()) { + return MarkingStoreMethod::PROPERTY; + } + } catch (\ReflectionException) { } - $subject->{$method}($marking, $context); + throw new LogicException(sprintf('Cannot store marking: class "%s" should have either a public method named "%s()" or a public property named "$%s"; none found.', get_debug_type($subject), $method, $property)); } } + +/** + * @internal + */ +enum MarkingStoreMethod +{ + case METHOD; + case PROPERTY; +} diff --git a/src/Symfony/Component/Workflow/Tests/MarkingStore/MethodMarkingStoreTest.php b/src/Symfony/Component/Workflow/Tests/MarkingStore/MethodMarkingStoreTest.php index 1efe40667bc7d..af0be682329be 100644 --- a/src/Symfony/Component/Workflow/Tests/MarkingStore/MethodMarkingStoreTest.php +++ b/src/Symfony/Component/Workflow/Tests/MarkingStore/MethodMarkingStoreTest.php @@ -29,9 +29,10 @@ public function testGetSetMarkingWithMultipleState() $marking->mark('first_place'); - $markingStore->setMarking($subject, $marking); + $markingStore->setMarking($subject, $marking, ['foo' => 'bar']); $this->assertSame(['first_place' => 1], $subject->getMarking()); + $this->assertSame(['foo' => 'bar'], $subject->getContext()); $marking2 = $markingStore->getMarking($subject); @@ -50,11 +51,12 @@ public function testGetSetMarkingWithSingleState() $marking->mark('first_place'); - $markingStore->setMarking($subject, $marking); + $markingStore->setMarking($subject, $marking, ['foo' => 'bar']); $this->assertSame('first_place', $subject->getMarking()); $marking2 = $markingStore->getMarking($subject); + $this->assertSame(['foo' => 'bar'], $subject->getContext()); $this->assertEquals($marking, $marking2); } diff --git a/src/Symfony/Component/Workflow/Tests/MarkingStore/PropertiesMarkingStoreTest.php b/src/Symfony/Component/Workflow/Tests/MarkingStore/PropertiesMarkingStoreTest.php new file mode 100644 index 0000000000000..10548e5c5cf49 --- /dev/null +++ b/src/Symfony/Component/Workflow/Tests/MarkingStore/PropertiesMarkingStoreTest.php @@ -0,0 +1,109 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Workflow\Tests\MarkingStore; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Workflow\MarkingStore\MethodMarkingStore; + +class PropertiesMarkingStoreTest extends TestCase +{ + public function testGetSetMarkingWithMultipleState() + { + $subject = new SubjectWithProperties(); + $markingStore = new MethodMarkingStore(false); + + $marking = $markingStore->getMarking($subject); + + $this->assertCount(0, $marking->getPlaces()); + + $marking->mark('first_place'); + + $markingStore->setMarking($subject, $marking, ['foo' => 'bar']); + + $this->assertSame(['first_place' => 1], $subject->marking); + + $marking2 = $markingStore->getMarking($subject); + + $this->assertEquals($marking, $marking2); + } + + public function testGetSetMarkingWithSingleState() + { + $subject = new SubjectWithProperties(); + $markingStore = new MethodMarkingStore(true, 'place', 'placeContext'); + + $marking = $markingStore->getMarking($subject); + + $this->assertCount(0, $marking->getPlaces()); + + $marking->mark('first_place'); + + $markingStore->setMarking($subject, $marking, ['foo' => 'bar']); + + $this->assertSame('first_place', $subject->place); + + $marking2 = $markingStore->getMarking($subject); + + $this->assertEquals($marking, $marking2); + } + + public function testGetSetMarkingWithSingleStateAndAlmostEmptyPlaceName() + { + $subject = new SubjectWithProperties(); + $subject->place = 0; + + $markingStore = new MethodMarkingStore(true, 'place'); + + $marking = $markingStore->getMarking($subject); + + $this->assertCount(1, $marking->getPlaces()); + } + + public function testGetMarkingWithValueObject() + { + $subject = new SubjectWithProperties(); + $subject->place = $this->createValueObject('first_place'); + + $markingStore = new MethodMarkingStore(true, 'place'); + + $marking = $markingStore->getMarking($subject); + + $this->assertCount(1, $marking->getPlaces()); + $this->assertSame('first_place', (string) $subject->place); + } + + public function testGetMarkingWithUninitializedProperty() + { + $subject = new SubjectWithProperties(); + + $markingStore = new MethodMarkingStore(true, 'place'); + + $marking = $markingStore->getMarking($subject); + + $this->assertCount(0, $marking->getPlaces()); + } + + private function createValueObject(string $markingValue): object + { + return new class($markingValue) { + public function __construct( + private string $markingValue, + ) { + } + + public function __toString(): string + { + return $this->markingValue; + } + }; + } +} diff --git a/src/Symfony/Component/Workflow/Tests/MarkingStore/SubjectWithProperties.php b/src/Symfony/Component/Workflow/Tests/MarkingStore/SubjectWithProperties.php new file mode 100644 index 0000000000000..7759448d72f7f --- /dev/null +++ b/src/Symfony/Component/Workflow/Tests/MarkingStore/SubjectWithProperties.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Workflow\Tests\MarkingStore; + +final class SubjectWithProperties +{ + // for type=workflow + public array $marking; + + // for type=state_machine + public string $place; + + private function getMarking(): array + { + return $this->marking; + } +}