Skip to content

Commit

Permalink
[Workflow] Add support for storing the marking in a property
Browse files Browse the repository at this point in the history
  • Loading branch information
lyrixx committed Jul 27, 2023
1 parent deb160a commit 93eeca2
Show file tree
Hide file tree
Showing 5 changed files with 210 additions and 22 deletions.
1 change: 1 addition & 0 deletions src/Symfony/Component/Workflow/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
---
Expand Down
89 changes: 69 additions & 20 deletions src/Symfony/Component/Workflow/MarkingStore/MethodMarkingStore.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,45 +15,42 @@
use Symfony\Component\Workflow\Marking;

/**
* MethodMarkingStore stores the marking with a subject's method.
* MethodMarkingStore stores the marking with a subject's public method if exist,
* then to a property.
*
* 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.
* 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.
* time. Use it with workflow. It uses an array to store the marking
*
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*/
final class MethodMarkingStore implements MarkingStoreInterface
{
private bool $singleState;
private string $property;
private readonly \SplObjectStorage $getters;
private readonly \SplObjectStorage $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())`
*/
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',
) {
$this->getters = new \SplObjectStorage();
$this->setters = new \SplObjectStorage();
}

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) {
Expand All @@ -68,7 +65,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);
Expand All @@ -82,12 +79,64 @@ 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
{
$propertyName = $this->property;
$methodName = 'get'.ucfirst($propertyName);

return match ($this->getters[$subject] ??= $this->getType($subject, $propertyName, $methodName)) {

Check failure on line 90 in src/Symfony/Component/Workflow/MarkingStore/MethodMarkingStore.php

View workflow job for this annotation

GitHub Actions / Psalm

InaccessibleProperty

src/Symfony/Component/Workflow/MarkingStore/MethodMarkingStore.php:90:23: InaccessibleProperty: Symfony\Component\Workflow\MarkingStore\MethodMarkingStore::$getters is marked readonly (see https://psalm.dev/054)

Check failure on line 90 in src/Symfony/Component/Workflow/MarkingStore/MethodMarkingStore.php

View workflow job for this annotation

GitHub Actions / Psalm

UnhandledMatchCondition

src/Symfony/Component/Workflow/MarkingStore/MethodMarkingStore.php:90:23: UnhandledMatchCondition: This match expression is not exhaustive - consider values Symfony\Component\Workflow\MarkingStore\MarkingStoreMethod|enum(Symfony\Component\Workflow\MarkingStore\MarkingStoreMethod::METHOD)|enum(Symfony\Component\Workflow\MarkingStore\MarkingStoreMethod::PROPERTY)|mixed (see https://psalm.dev/236)

Check failure on line 90 in src/Symfony/Component/Workflow/MarkingStore/MethodMarkingStore.php

View workflow job for this annotation

GitHub Actions / Psalm

InaccessibleProperty

src/Symfony/Component/Workflow/MarkingStore/MethodMarkingStore.php:90:23: InaccessibleProperty: Symfony\Component\Workflow\MarkingStore\MethodMarkingStore::$getters is marked readonly (see https://psalm.dev/054)

Check failure on line 90 in src/Symfony/Component/Workflow/MarkingStore/MethodMarkingStore.php

View workflow job for this annotation

GitHub Actions / Psalm

UnhandledMatchCondition

src/Symfony/Component/Workflow/MarkingStore/MethodMarkingStore.php:90:23: UnhandledMatchCondition: This match expression is not exhaustive - consider values Symfony\Component\Workflow\MarkingStore\MarkingStoreMethod|enum(Symfony\Component\Workflow\MarkingStore\MarkingStoreMethod::METHOD)|enum(Symfony\Component\Workflow\MarkingStore\MarkingStoreMethod::PROPERTY)|mixed (see https://psalm.dev/236)
MarkingStoreMethod::METHOD => static fn () => $subject->{$methodName}(),
MarkingStoreMethod::PROPERTY => static fn () => $subject->{$propertyName},
};
}

private function getSetter(object $subject): callable
{
$propertyName = $this->property;
$methodName = 'set'.ucfirst($propertyName);

return match ($this->setters[$subject] ??= $this->getType($subject, $propertyName, $methodName)) {

Check failure on line 101 in src/Symfony/Component/Workflow/MarkingStore/MethodMarkingStore.php

View workflow job for this annotation

GitHub Actions / Psalm

InaccessibleProperty

src/Symfony/Component/Workflow/MarkingStore/MethodMarkingStore.php:101:23: InaccessibleProperty: Symfony\Component\Workflow\MarkingStore\MethodMarkingStore::$setters is marked readonly (see https://psalm.dev/054)

Check failure on line 101 in src/Symfony/Component/Workflow/MarkingStore/MethodMarkingStore.php

View workflow job for this annotation

GitHub Actions / Psalm

UnhandledMatchCondition

src/Symfony/Component/Workflow/MarkingStore/MethodMarkingStore.php:101:23: UnhandledMatchCondition: This match expression is not exhaustive - consider values Symfony\Component\Workflow\MarkingStore\MarkingStoreMethod|enum(Symfony\Component\Workflow\MarkingStore\MarkingStoreMethod::METHOD)|enum(Symfony\Component\Workflow\MarkingStore\MarkingStoreMethod::PROPERTY)|mixed (see https://psalm.dev/236)

Check failure on line 101 in src/Symfony/Component/Workflow/MarkingStore/MethodMarkingStore.php

View workflow job for this annotation

GitHub Actions / Psalm

InaccessibleProperty

src/Symfony/Component/Workflow/MarkingStore/MethodMarkingStore.php:101:23: InaccessibleProperty: Symfony\Component\Workflow\MarkingStore\MethodMarkingStore::$setters is marked readonly (see https://psalm.dev/054)

Check failure on line 101 in src/Symfony/Component/Workflow/MarkingStore/MethodMarkingStore.php

View workflow job for this annotation

GitHub Actions / Psalm

UnhandledMatchCondition

src/Symfony/Component/Workflow/MarkingStore/MethodMarkingStore.php:101:23: UnhandledMatchCondition: This match expression is not exhaustive - consider values Symfony\Component\Workflow\MarkingStore\MarkingStoreMethod|enum(Symfony\Component\Workflow\MarkingStore\MarkingStoreMethod::METHOD)|enum(Symfony\Component\Workflow\MarkingStore\MarkingStoreMethod::PROPERTY)|mixed (see https://psalm.dev/236)
MarkingStoreMethod::METHOD => static fn ($marking, $context) => $subject->{$methodName}($marking, $context),
MarkingStoreMethod::PROPERTY => static fn ($marking) => $subject->{$propertyName} = $marking,
};
}

private function getType(object $subject, string $propertyName, string $method): MarkingStoreMethod
{
if (!method_exists($subject, $method)) {
throw new LogicException(sprintf('The method "%s::%s()" does not exist.', get_debug_type($subject), $method));
goto property;
}

$subject->{$method}($marking, $context);
try {
$r = new \ReflectionMethod($subject, $method);
} catch (\ReflectionException) {
property:
try {
$r = new \ReflectionProperty($subject, $propertyName);
} catch (\ReflectionException) {
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));
}

if (!$r->isPublic()) {
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));
}

return MarkingStoreMethod::PROPERTY;
}

if (!$r->isPublic()) {
throw new LogicException(sprintf('The method "%s::%s()" must be public.', get_debug_type($subject), $method));
}

return MarkingStoreMethod::METHOD;
}
}

enum MarkingStoreMethod
{
case METHOD;
case PROPERTY;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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;
use Symfony\Component\Workflow\Tests\SubjectWithProperties;

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) {
/** @var string */
private $markingValue;

public function __construct(string $markingValue)
{
$this->markingValue = $markingValue;
}

public function __toString(): string
{
return $this->markingValue;
}
};
}
}
21 changes: 21 additions & 0 deletions src/Symfony/Component/Workflow/Tests/SubjectWithProperties.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Workflow\Tests;

final class SubjectWithProperties
{
// for type=workflow
public array $marking;

// for type=state_machine
public string $place;
}

0 comments on commit 93eeca2

Please sign in to comment.