Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Workflow] Add support for storing the marking in a property #50974

Merged
merged 1 commit into from
Aug 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
Original file line number Diff line number Diff line change
Expand Up @@ -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 <lyrixx@lyrixx.info>
*/
final class MethodMarkingStore implements MarkingStoreInterface
{
private bool $singleState;
private string $property;
/** @var array<class-string, MarkingStoreMethod> */
private array $getters = [];
/** @var array<class-string, MarkingStoreMethod> */
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) {
Expand All @@ -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);
Expand All @@ -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
fabpot marked this conversation as resolved.
Show resolved Hide resolved
{
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,109 @@
<?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;

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;
}
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?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;

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

// for type=state_machine
public string $place;

private function getMarking(): array
{
return $this->marking;
}
}