Skip to content

[Workflow] Allow use of UnitEnum and BackedEnum in workflow places #44306

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

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,24 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode)
->end()
->arrayNode('initial_marking')
->beforeNormalization()->castToArray()->end()
->defaultValue([])
->beforeNormalization()
->ifTrue(function ($v) {
foreach ($v as $value) {
if (!is_scalar($value) && !$value instanceof \UnitEnum) {
return true;
}
}

return false;
})
->thenInvalid('Initial marking must either be a scalar or an UnitEnum.')
->end()
->beforeNormalization()
->always()
->then(function ($places) {
return array_map(static fn ($element) => $element instanceof \UnitEnum ? \get_class($element).'::'.$element->name : $element, $places);
})
->end()
->prototype('scalar')->end()
->end()
->variableNode('events_to_dispatch')
Expand Down Expand Up @@ -431,9 +448,9 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode)
->always()
->then(function ($places) {
// It's an indexed array of shape ['place1', 'place2']
if (isset($places[0]) && \is_string($places[0])) {
return array_map(function (string $place) {
return ['name' => $place];
if (isset($places[0]) && (\is_string($places[0]) || $places[0] instanceof \UnitEnum)) {
return array_map(function (string|\UnitEnum $place) {
return ['name' => $place instanceof \UnitEnum ? \get_class($place).'::'.$place->name : $place];
}, $places);
}

Expand Down Expand Up @@ -505,19 +522,49 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode)
->example('is_fully_authenticated() and is_granted(\'ROLE_JOURNALIST\') and subject.getTitle() == \'My first article\'')
->end()
->arrayNode('from')
->beforeNormalization()->castToArray()->end()
->beforeNormalization()
->ifTrue(function ($v) {
foreach ($v as $value) {
if (!is_scalar($value) && !$value instanceof \UnitEnum) {
return true;
}
}

return false;
})
->thenInvalid('"From" places must either be scalars or UnitEnum.')
->end()
->beforeNormalization()
->ifString()
->then(function ($v) { return [$v]; })
->always()
->then(function ($places) {
return array_map(static fn ($element) => $element instanceof \UnitEnum ? \get_class($element).'::'.$element->name : $element, $places);
})
->end()
->requiresAtLeastOneElement()
->prototype('scalar')
->cannotBeEmpty()
->end()
->end()
->arrayNode('to')
->beforeNormalization()->castToArray()->end()
->beforeNormalization()
->ifTrue(function ($v) {
foreach ($v as $value) {
if (!is_scalar($value) && !$value instanceof \UnitEnum) {
return true;
}
}

return false;
})
->thenInvalid('"To" places must either be scalars or UnitEnum.')
->end()
->beforeNormalization()
->ifString()
->then(function ($v) { return [$v]; })
->always()
->then(function ($places) {
return array_map(static fn ($element) => $element instanceof \UnitEnum ? \get_class($element).'::'.$element->name : $element, $places);
})
->end()
->requiresAtLeastOneElement()
->prototype('scalar')
Expand Down
5 changes: 5 additions & 0 deletions src/Symfony/Component/Workflow/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
CHANGELOG
=========

6.1
---

* Allow use of UnitEnum and BackedEnum in workflow places

6.0
---

Expand Down
25 changes: 14 additions & 11 deletions src/Symfony/Component/Workflow/Definition.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use Symfony\Component\Workflow\Exception\LogicException;
use Symfony\Component\Workflow\Metadata\InMemoryMetadataStore;
use Symfony\Component\Workflow\Metadata\MetadataStoreInterface;
use Symfony\Component\Workflow\Utils\PlaceEnumerationUtils;

/**
* @author Fabien Potencier <fabien@symfony.com>
Expand All @@ -28,11 +29,11 @@ final class Definition
private MetadataStoreInterface $metadataStore;

/**
* @param string[] $places
* @param Transition[] $transitions
* @param string|string[]|null $initialPlaces
* @param string[]|\UnitEnum[] $places
* @param Transition[] $transitions
* @param string|string[]|\UnitEnum|\UnitEnum[]|null $initialPlaces
*/
public function __construct(array $places, array $transitions, string|array $initialPlaces = null, MetadataStoreInterface $metadataStore = null)
public function __construct(array $places, array $transitions, string|\UnitEnum|array $initialPlaces = null, MetadataStoreInterface $metadataStore = null)
{
foreach ($places as $place) {
$this->addPlace($place);
Expand All @@ -52,7 +53,7 @@ public function __construct(array $places, array $transitions, string|array $ini
*/
public function getInitialPlaces(): array
{
return $this->initialPlaces;
return array_map(static fn ($element) => PlaceEnumerationUtils::getTypedValue($element), $this->initialPlaces);
}

/**
Expand All @@ -76,43 +77,45 @@ public function getMetadataStore(): MetadataStoreInterface
return $this->metadataStore;
}

private function setInitialPlaces(string|array $places = null)
private function setInitialPlaces(string|\UnitEnum|array $places = null)
{
if (!$places) {
return;
}

$places = (array) $places;
$places = array_map(static fn ($element) => PlaceEnumerationUtils::getPlaceKey($element), \is_array($places) ? $places : [$places]);

foreach ($places as $place) {
if (!isset($this->places[$place])) {
throw new LogicException(sprintf('Place "%s" cannot be the initial place as it does not exist.', $place));
throw new LogicException(sprintf('Place "%s" cannot be the initial place as it does not exist.', PlaceEnumerationUtils::getPlaceKey($place)));
}
}

$this->initialPlaces = $places;
}

private function addPlace(string $place)
private function addPlace(string|\UnitEnum $place)
{
if (!\count($this->places)) {
$this->initialPlaces = [$place];
$this->initialPlaces = [PlaceEnumerationUtils::getPlaceKey($place)];
}

$this->places[$place] = $place;
$this->places[PlaceEnumerationUtils::getPlaceKey($place)] = $place;
}

private function addTransition(Transition $transition)
{
$name = $transition->getName();

foreach ($transition->getFroms() as $from) {
$from = PlaceEnumerationUtils::getPlaceKey($from);
if (!isset($this->places[$from])) {
throw new LogicException(sprintf('Place "%s" referenced in transition "%s" does not exist.', $from, $name));
}
}

foreach ($transition->getTos() as $to) {
$to = PlaceEnumerationUtils::getPlaceKey($to);
if (!isset($this->places[$to])) {
throw new LogicException(sprintf('Place "%s" referenced in transition "%s" does not exist.', $to, $name));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Workflow\Event\Event;
use Symfony\Component\Workflow\Utils\PlaceEnumerationUtils;

/**
* @author Grégoire Pineau <lyrixx@lyrixx.info>
Expand All @@ -30,7 +31,7 @@ public function __construct(LoggerInterface $logger)
public function onLeave(Event $event)
{
foreach ($event->getTransition()->getFroms() as $place) {
$this->logger->info(sprintf('Leaving "%s" for subject of class "%s" in workflow "%s".', $place, \get_class($event->getSubject()), $event->getWorkflowName()));
$this->logger->info(sprintf('Leaving "%s" for subject of class "%s" in workflow "%s".', PlaceEnumerationUtils::getPlaceKey($place), \get_class($event->getSubject()), $event->getWorkflowName()));
}
}

Expand All @@ -42,7 +43,7 @@ public function onTransition(Event $event)
public function onEnter(Event $event)
{
foreach ($event->getTransition()->getTos() as $place) {
$this->logger->info(sprintf('Entering "%s" for subject of class "%s" in workflow "%s".', $place, \get_class($event->getSubject()), $event->getWorkflowName()));
$this->logger->info(sprintf('Entering "%s" for subject of class "%s" in workflow "%s".', PlaceEnumerationUtils::getPlaceKey($place), \get_class($event->getSubject()), $event->getWorkflowName()));
}
}

Expand Down
33 changes: 23 additions & 10 deletions src/Symfony/Component/Workflow/Marking.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

namespace Symfony\Component\Workflow;

use Symfony\Component\Workflow\Utils\PlaceEnumerationUtils;

/**
* Marking contains the place of every tokens.
*
Expand All @@ -22,33 +24,44 @@ class Marking
private ?array $context = null;

/**
* @param int[] $representation Keys are the place name and values should be 1
* @param int[]|\UnitEnum[] $representation Keys are the place name and values should be 1, unless UnitEnums
* are used as workflow places
*/
public function __construct(array $representation = [])
{
foreach ($representation as $place => $nbToken) {
$this->mark($place);
foreach ($representation as $place => $token) {
$this->mark($token instanceof \UnitEnum ? $token : $place);
}
}

public function mark(string $place)
public function mark(string|\UnitEnum $place)
{
$this->places[$place] = 1;
$this->places[PlaceEnumerationUtils::getPlaceKey($place)] = 1;
}

public function unmark(string $place)
public function unmark(string|\UnitEnum $place)
{
unset($this->places[$place]);
unset($this->places[PlaceEnumerationUtils::getPlaceKey($place)]);
}

public function has(string $place)
public function has(string|\UnitEnum $place)
{
return isset($this->places[$place]);
return isset($this->places[PlaceEnumerationUtils::getPlaceKey($place)]);
}

public function getPlaces()
{
return $this->places;
$places = [];
foreach ($this->places as $key => $value) {
$typedKey = PlaceEnumerationUtils::getTypedValue($key);
if ($typedKey instanceof \UnitEnum) {
$places[$key] = $typedKey;
} else {
$places[$typedKey] = 1;
}
}

return $places;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

use Symfony\Component\Workflow\Exception\LogicException;
use Symfony\Component\Workflow\Marking;
use Symfony\Component\Workflow\Utils\PlaceEnumerationUtils;

/**
* MethodMarkingStore stores the marking with a subject's method.
Expand Down Expand Up @@ -61,10 +62,17 @@ public function getMarking(object $subject): Marking
}

if ($this->singleState) {
$marking = [(string) $marking => 1];
$markingRepresentation = [PlaceEnumerationUtils::getPlaceKey($marking) => 1];
} else {
$markingRepresentation = [];
foreach ($marking as $key => $item) {
// When using enumerations, as the enumeration case can't be used as an array key, the value is actually
// stored in the item instead of the key.
$markingRepresentation[PlaceEnumerationUtils::getPlaceKey($item instanceof \UnitEnum ? $item : $key)] = 1;
}
}

return new Marking($marking);
return new Marking($markingRepresentation);
}

/**
Expand All @@ -75,7 +83,18 @@ public function setMarking(object $subject, Marking $marking, array $context = [
$marking = $marking->getPlaces();

if ($this->singleState) {
$marking = key($marking);
$markingResult = PlaceEnumerationUtils::getTypedValue(key($marking));
} else {
$markingResult = [];
foreach ($marking as $key => $item) {
$value = PlaceEnumerationUtils::getTypedValue($key);
if ($value instanceof \UnitEnum) {
// UnitEnum can't be used as array key, put it as a simple value without specific index.
$markingResult[] = $value;
} else {
$markingResult[$value] = 1;
}
}
}

$method = 'set'.ucfirst($this->property);
Expand All @@ -84,6 +103,6 @@ public function setMarking(object $subject, Marking $marking, array $context = [
throw new LogicException(sprintf('The method "%s::%s()" does not exist.', get_debug_type($subject), $method));
}

$subject->{$method}($marking, $context);
$subject->{$method}($markingResult, $context);
}
}
Loading