diff --git a/Attribute/BuildEventNameTrait.php b/Attribute/BuildEventNameTrait.php index 93eeee7..d6d3c26 100644 --- a/Attribute/BuildEventNameTrait.php +++ b/Attribute/BuildEventNameTrait.php @@ -24,16 +24,16 @@ private static function buildEventName(string $keyword, string $argument, ?strin { if (null === $workflow) { if (null !== $node) { - throw new LogicException(sprintf('The "%s" argument of "%s" cannot be used without a "workflow" argument.', $argument, self::class)); + throw new LogicException(\sprintf('The "%s" argument of "%s" cannot be used without a "workflow" argument.', $argument, self::class)); } - return sprintf('workflow.%s', $keyword); + return \sprintf('workflow.%s', $keyword); } if (null === $node) { - return sprintf('workflow.%s.%s', $workflow, $keyword); + return \sprintf('workflow.%s.%s', $workflow, $keyword); } - return sprintf('workflow.%s.%s.%s', $workflow, $keyword, $node); + return \sprintf('workflow.%s.%s.%s', $workflow, $keyword, $node); } } diff --git a/CHANGELOG.md b/CHANGELOG.md index 2926da4..5a37ead 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,18 @@ CHANGELOG ========= +7.3 +--- + + * Deprecate `Event::getWorkflow()` method + 7.1 --- * Add method `getEnabledTransition()` to `WorkflowInterface` * Automatically register places from transitions * Add support for workflows that need to store many tokens in the marking - * Add method `getName()` in event classes to build event names in subscribers + * Add method `getName()` in event classes to build event names in subscribers 7.0 --- diff --git a/DataCollector/WorkflowDataCollector.php b/DataCollector/WorkflowDataCollector.php index ef09b98..6ce732b 100644 --- a/DataCollector/WorkflowDataCollector.php +++ b/DataCollector/WorkflowDataCollector.php @@ -88,33 +88,44 @@ public function getCallsCount(): int return $i; } + public function hash(string $string): string + { + return hash('xxh128', $string); + } + + public function buildMermaidLiveLink(string $name): string + { + $payload = [ + 'code' => $this->data['workflows'][$name]['dump'], + 'mermaid' => '{"theme": "default"}', + 'autoSync' => false, + ]; + + $compressed = zlib_encode(json_encode($payload), \ZLIB_ENCODING_DEFLATE); + + $suffix = rtrim(strtr(base64_encode($compressed), '+/', '-_'), '='); + + return "https://mermaid.live/edit#pako:{$suffix}"; + } + protected function getCasters(): array { - $casters = [ + return [ ...parent::getCasters(), - TransitionBlocker::class => function ($v, array $a, Stub $s, $isNested) { - unset( - $a[sprintf(Caster::PATTERN_PRIVATE, $v::class, 'code')], - $a[sprintf(Caster::PATTERN_PRIVATE, $v::class, 'parameters')], - ); + TransitionBlocker::class => static function ($v, array $a, Stub $s) { + unset($a[\sprintf(Caster::PATTERN_PRIVATE, $v::class, 'code')]); + unset($a[\sprintf(Caster::PATTERN_PRIVATE, $v::class, 'parameters')]); $s->cut += 2; return $a; }, - Marking::class => function ($v, array $a, Stub $s, $isNested) { + Marking::class => static function ($v, array $a) { $a[Caster::PREFIX_VIRTUAL.'.places'] = array_keys($v->getPlaces()); return $a; }, ]; - - return $casters; - } - - public function hash(string $string): string - { - return hash('xxh128', $string); } private function getEventListeners(WorkflowInterface $workflow): array @@ -129,9 +140,9 @@ private function getEventListeners(WorkflowInterface $workflow): array 'entered', ]; foreach ($subEventNames as $subEventName) { - $eventNames[] = sprintf('workflow.%s', $subEventName); - $eventNames[] = sprintf('workflow.%s.%s', $workflow->getName(), $subEventName); - $eventNames[] = sprintf('workflow.%s.%s.%s', $workflow->getName(), $subEventName, $place); + $eventNames[] = \sprintf('workflow.%s', $subEventName); + $eventNames[] = \sprintf('workflow.%s.%s', $workflow->getName(), $subEventName); + $eventNames[] = \sprintf('workflow.%s.%s.%s', $workflow->getName(), $subEventName, $place); } foreach ($eventNames as $eventName) { foreach ($this->eventDispatcher->getListeners($eventName) as $listener) { @@ -151,9 +162,9 @@ private function getEventListeners(WorkflowInterface $workflow): array 'announce', ]; foreach ($subEventNames as $subEventName) { - $eventNames[] = sprintf('workflow.%s', $subEventName); - $eventNames[] = sprintf('workflow.%s.%s', $workflow->getName(), $subEventName); - $eventNames[] = sprintf('workflow.%s.%s.%s', $workflow->getName(), $subEventName, $transition->getName()); + $eventNames[] = \sprintf('workflow.%s', $subEventName); + $eventNames[] = \sprintf('workflow.%s.%s', $workflow->getName(), $subEventName); + $eventNames[] = \sprintf('workflow.%s.%s.%s', $workflow->getName(), $subEventName, $transition->getName()); } foreach ($eventNames as $eventName) { foreach ($this->eventDispatcher->getListeners($eventName) as $listener) { diff --git a/Debug/TraceableWorkflow.php b/Debug/TraceableWorkflow.php index 6d0afd8..c783e63 100644 --- a/Debug/TraceableWorkflow.php +++ b/Debug/TraceableWorkflow.php @@ -30,6 +30,7 @@ class TraceableWorkflow implements WorkflowInterface public function __construct( private readonly WorkflowInterface $workflow, private readonly Stopwatch $stopwatch, + protected readonly ?\Closure $disabled = null, ) { } @@ -90,6 +91,9 @@ public function getCalls(): array private function callInner(string $method, array $args): mixed { + if ($this->disabled?->__invoke()) { + return $this->workflow->{$method}(...$args); + } $sMethod = $this->workflow::class.'::'.$method; $this->stopwatch->start($sMethod, 'workflow'); diff --git a/Definition.php b/Definition.php index 96ec1fe..0b5697b 100644 --- a/Definition.php +++ b/Definition.php @@ -86,7 +86,7 @@ private function setInitialPlaces(string|array|null $places): void 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.', $place)); } } diff --git a/DependencyInjection/WorkflowDebugPass.php b/DependencyInjection/WorkflowDebugPass.php index 634605d..042aaba 100644 --- a/DependencyInjection/WorkflowDebugPass.php +++ b/DependencyInjection/WorkflowDebugPass.php @@ -31,6 +31,7 @@ public function process(ContainerBuilder $container): void ->setArguments([ new Reference("debug.{$id}.inner"), new Reference('debug.stopwatch'), + new Reference('profiler.is_disabled_state_checker', ContainerBuilder::IGNORE_ON_INVALID_REFERENCE), ]); } } diff --git a/DependencyInjection/WorkflowGuardListenerPass.php b/DependencyInjection/WorkflowGuardListenerPass.php index ba81a7b..ccf00f0 100644 --- a/DependencyInjection/WorkflowGuardListenerPass.php +++ b/DependencyInjection/WorkflowGuardListenerPass.php @@ -38,7 +38,7 @@ public function process(ContainerBuilder $container): void foreach ($servicesNeeded as $service) { if (!$container->has($service)) { - throw new LogicException(sprintf('The "%s" service is needed to be able to use the workflow guard listener.', $service)); + throw new LogicException(\sprintf('The "%s" service is needed to be able to use the workflow guard listener.', $service)); } } } diff --git a/DependencyInjection/WorkflowValidatorPass.php b/DependencyInjection/WorkflowValidatorPass.php new file mode 100644 index 0000000..d1e4622 --- /dev/null +++ b/DependencyInjection/WorkflowValidatorPass.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Workflow\DependencyInjection; + +use Symfony\Component\Config\Resource\FileResource; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; + +/** + * @author Grégoire Pineau + */ +class WorkflowValidatorPass implements CompilerPassInterface +{ + public function process(ContainerBuilder $container): void + { + foreach ($container->findTaggedServiceIds('workflow') as $attributes) { + foreach ($attributes as $attribute) { + foreach ($attribute['definition_validators'] ?? [] as $validatorClass) { + $container->addResource(new FileResource($container->getReflectionClass($validatorClass)->getFileName())); + + $realDefinition = $container->get($attribute['definition_id'] ?? throw new \LogicException('The "definition_id" attribute is required.')); + (new $validatorClass())->validate($realDefinition, $attribute['name'] ?? throw new \LogicException('The "name" attribute is required.')); + } + } + } + } +} diff --git a/Dumper/GraphvizDumper.php b/Dumper/GraphvizDumper.php index 7e6ffa4..ad7b0c2 100644 --- a/Dumper/GraphvizDumper.php +++ b/Dumper/GraphvizDumper.php @@ -141,7 +141,7 @@ protected function findTransitions(Definition $definition, bool $withMetadata): /** * @internal */ - protected function addPlaces(array $places, float $withMetadata): string + protected function addPlaces(array $places, bool $withMetadata): string { $code = ''; @@ -154,14 +154,14 @@ protected function addPlaces(array $places, float $withMetadata): string } if ($withMetadata) { - $escapedLabel = sprintf('<%s%s>', $this->escape($placeName), $this->addMetadata($place['attributes']['metadata'])); + $escapedLabel = \sprintf('<%s%s>', $this->escape($placeName), $this->addMetadata($place['attributes']['metadata'])); // Don't include metadata in default attributes used to format the place unset($place['attributes']['metadata']); } else { - $escapedLabel = sprintf('"%s"', $this->escape($placeName)); + $escapedLabel = \sprintf('"%s"', $this->escape($placeName)); } - $code .= sprintf(" place_%s [label=%s, shape=circle%s];\n", $this->dotize($id), $escapedLabel, $this->addAttributes($place['attributes'])); + $code .= \sprintf(" place_%s [label=%s, shape=circle%s];\n", $this->dotize($id), $escapedLabel, $this->addAttributes($place['attributes'])); } return $code; @@ -176,12 +176,12 @@ protected function addTransitions(array $transitions, bool $withMetadata): strin foreach ($transitions as $i => $place) { if ($withMetadata) { - $escapedLabel = sprintf('<%s%s>', $this->escape($place['name']), $this->addMetadata($place['metadata'])); + $escapedLabel = \sprintf('<%s%s>', $this->escape($place['name']), $this->addMetadata($place['metadata'])); } else { $escapedLabel = '"'.$this->escape($place['name']).'"'; } - $code .= sprintf(" transition_%s [label=%s,%s];\n", $this->dotize($i), $escapedLabel, $this->addAttributes($place['attributes'])); + $code .= \sprintf(" transition_%s [label=%s,%s];\n", $this->dotize($i), $escapedLabel, $this->addAttributes($place['attributes'])); } return $code; @@ -229,12 +229,12 @@ protected function addEdges(array $edges): string foreach ($edges as $edge) { if ('from' === $edge['direction']) { - $code .= sprintf(" place_%s -> transition_%s [style=\"solid\"];\n", + $code .= \sprintf(" place_%s -> transition_%s [style=\"solid\"];\n", $this->dotize($edge['from']), $this->dotize($edge['transition_number']) ); } else { - $code .= sprintf(" transition_%s -> place_%s [style=\"solid\"];\n", + $code .= \sprintf(" transition_%s -> place_%s [style=\"solid\"];\n", $this->dotize($edge['transition_number']), $this->dotize($edge['to']) ); @@ -249,9 +249,9 @@ protected function addEdges(array $edges): string */ protected function startDot(array $options, string $label): string { - return sprintf("digraph workflow {\n %s%s\n node [%s];\n edge [%s];\n\n", + return \sprintf("digraph workflow {\n %s%s\n node [%s];\n edge [%s];\n\n", $this->addOptions($options['graph']), - '""' !== $label && '<>' !== $label ? sprintf(' label=%s', $label) : '', + '""' !== $label && '<>' !== $label ? \sprintf(' label=%s', $label) : '', $this->addOptions($options['node']), $this->addOptions($options['edge']) ); @@ -289,7 +289,7 @@ protected function addAttributes(array $attributes): string $code = []; foreach ($attributes as $k => $v) { - $code[] = sprintf('%s="%s"', $k, $this->escape($v)); + $code[] = \sprintf('%s="%s"', $k, $this->escape($v)); } return $code ? ' '.implode(' ', $code) : ''; @@ -303,23 +303,23 @@ protected function addAttributes(array $attributes): string * * @internal */ - protected function formatLabel(Definition $definition, string $withMetadata, array $options): string + protected function formatLabel(Definition $definition, bool $withMetadata, array $options): string { $currentLabel = $options['label'] ?? ''; if (!$withMetadata) { // Only currentLabel to handle. If null, will be translated to empty string - return sprintf('"%s"', $this->escape($currentLabel)); + return \sprintf('"%s"', $this->escape($currentLabel)); } $workflowMetadata = $definition->getMetadataStore()->getWorkflowMetadata(); if ('' === $currentLabel) { // Only metadata to handle - return sprintf('<%s>', $this->addMetadata($workflowMetadata, false)); + return \sprintf('<%s>', $this->addMetadata($workflowMetadata, false)); } // currentLabel and metadata to handle - return sprintf('<%s%s>', $this->escape($currentLabel), $this->addMetadata($workflowMetadata)); + return \sprintf('<%s%s>', $this->escape($currentLabel), $this->addMetadata($workflowMetadata)); } private function addOptions(array $options): string @@ -327,7 +327,7 @@ private function addOptions(array $options): string $code = []; foreach ($options as $k => $v) { - $code[] = sprintf('%s="%s"', $k, $v); + $code[] = \sprintf('%s="%s"', $k, $v); } return implode(' ', $code); @@ -344,10 +344,10 @@ private function addMetadata(array $metadata, bool $lineBreakFirstIfNotEmpty = t foreach ($metadata as $key => $value) { if ($skipSeparator) { - $code[] = sprintf('%s: %s', $this->escape($key), $this->escape($value)); + $code[] = \sprintf('%s: %s', $this->escape($key), $this->escape($value)); $skipSeparator = false; } else { - $code[] = sprintf('%s%s: %s', '
', $this->escape($key), $this->escape($value)); + $code[] = \sprintf('%s%s: %s', '
', $this->escape($key), $this->escape($value)); } } diff --git a/Dumper/MermaidDumper.php b/Dumper/MermaidDumper.php index ed67c6f..bd7a6fa 100644 --- a/Dumper/MermaidDumper.php +++ b/Dumper/MermaidDumper.php @@ -39,22 +39,18 @@ class MermaidDumper implements DumperInterface self::TRANSITION_TYPE_WORKFLOW, ]; - private string $direction; - private string $transitionType; - /** * Just tracking the transition id is in some cases inaccurate to * get the link's number for styling purposes. */ private int $linkCount = 0; - public function __construct(string $transitionType, string $direction = self::DIRECTION_LEFT_TO_RIGHT) - { + public function __construct( + private string $transitionType, + private string $direction = self::DIRECTION_LEFT_TO_RIGHT, + ) { $this->validateDirection($direction); $this->validateTransitionType($transitionType); - - $this->direction = $direction; - $this->transitionType = $transitionType; } public function dump(Definition $definition, ?Marking $marking = null, array $options = []): string @@ -142,7 +138,7 @@ private function preparePlace(int $placeId, string $placeName, array $meta, bool $placeNodeName = 'place'.$placeId; $placeNodeFormat = '%s'.$labelShape; - $placeNode = sprintf($placeNodeFormat, $placeNodeName, $placeLabel); + $placeNode = \sprintf($placeNodeFormat, $placeNodeName, $placeLabel); $placeStyle = $this->styleNode($meta, $placeNodeName, $hasMarking); @@ -154,7 +150,7 @@ private function styleNode(array $meta, string $nodeName, bool $hasMarking = fal $nodeStyles = []; if (\array_key_exists('bg_color', $meta)) { - $nodeStyles[] = sprintf( + $nodeStyles[] = \sprintf( 'fill:%s', $meta['bg_color'] ); @@ -168,7 +164,7 @@ private function styleNode(array $meta, string $nodeName, bool $hasMarking = fal return ''; } - return sprintf('style %s %s', $nodeName, implode(',', $nodeStyles)); + return \sprintf('style %s %s', $nodeName, implode(',', $nodeStyles)); } /** @@ -179,26 +175,26 @@ private function escape(string $label): string { $label = str_replace('"', '#quot;', $label); - return sprintf('"%s"', $label); + return \sprintf('"%s"', $label); } public function validateDirection(string $direction): void { if (!\in_array($direction, self::VALID_DIRECTIONS, true)) { - throw new InvalidArgumentException(sprintf('Direction "%s" is not valid, valid directions are: "%s".', $direction, implode(', ', self::VALID_DIRECTIONS))); + throw new InvalidArgumentException(\sprintf('Direction "%s" is not valid, valid directions are: "%s".', $direction, implode(', ', self::VALID_DIRECTIONS))); } } private function validateTransitionType(string $transitionType): void { if (!\in_array($transitionType, self::VALID_TRANSITION_TYPES, true)) { - throw new InvalidArgumentException(sprintf('Transition type "%s" is not valid, valid types are: "%s".', $transitionType, implode(', ', self::VALID_TRANSITION_TYPES))); + throw new InvalidArgumentException(\sprintf('Transition type "%s" is not valid, valid types are: "%s".', $transitionType, implode(', ', self::VALID_TRANSITION_TYPES))); } } private function styleStateMachineTransition(string $from, string $to, string $transitionLabel, array $transitionMeta): array { - $transitionOutput = [sprintf('%s-->|%s|%s', $from, str_replace("\n", ' ', $this->escape($transitionLabel)), $to)]; + $transitionOutput = [\sprintf('%s-->|%s|%s', $from, str_replace("\n", ' ', $this->escape($transitionLabel)), $to)]; $linkStyle = $this->styleLink($transitionMeta); if ('' !== $linkStyle) { @@ -217,7 +213,7 @@ private function styleWorkflowTransition(string $from, string $to, int $transiti $transitionLabel = $this->escape($transitionLabel); $transitionNodeName = 'transition'.$transitionId; - $transitionOutput[] = sprintf('%s[%s]', $transitionNodeName, $transitionLabel); + $transitionOutput[] = \sprintf('%s[%s]', $transitionNodeName, $transitionLabel); $transitionNodeStyle = $this->styleNode($transitionMeta, $transitionNodeName); if ('' !== $transitionNodeStyle) { @@ -225,7 +221,7 @@ private function styleWorkflowTransition(string $from, string $to, int $transiti } $connectionStyle = '%s-->%s'; - $transitionOutput[] = sprintf($connectionStyle, $from, $transitionNodeName); + $transitionOutput[] = \sprintf($connectionStyle, $from, $transitionNodeName); $linkStyle = $this->styleLink($transitionMeta); if ('' !== $linkStyle) { @@ -234,7 +230,7 @@ private function styleWorkflowTransition(string $from, string $to, int $transiti ++$this->linkCount; - $transitionOutput[] = sprintf($connectionStyle, $transitionNodeName, $to); + $transitionOutput[] = \sprintf($connectionStyle, $transitionNodeName, $to); $linkStyle = $this->styleLink($transitionMeta); if ('' !== $linkStyle) { @@ -249,7 +245,7 @@ private function styleWorkflowTransition(string $from, string $to, int $transiti private function styleLink(array $transitionMeta): string { if (\array_key_exists('color', $transitionMeta)) { - return sprintf('linkStyle %d stroke:%s', $this->linkCount, $transitionMeta['color']); + return \sprintf('linkStyle %d stroke:%s', $this->linkCount, $transitionMeta['color']); } return ''; diff --git a/Dumper/PlantUmlDumper.php b/Dumper/PlantUmlDumper.php index 2a232d4..9bd621a 100644 --- a/Dumper/PlantUmlDumper.php +++ b/Dumper/PlantUmlDumper.php @@ -51,14 +51,12 @@ class PlantUmlDumper implements DumperInterface ], ]; - private string $transitionType = self::STATEMACHINE_TRANSITION; - - public function __construct(string $transitionType) - { + public function __construct( + private string $transitionType, + ) { if (!\in_array($transitionType, self::TRANSITION_TYPES, true)) { throw new \InvalidArgumentException("Transition type '$transitionType' does not exist."); } - $this->transitionType = $transitionType; } public function dump(Definition $definition, ?Marking $marking = null, array $options = []): string @@ -117,7 +115,7 @@ public function dump(Definition $definition, ?Marking $marking = null, array $op } } - return $this->startPuml($options).$this->getLines($code).$this->endPuml($options); + return $this->startPuml().$this->getLines($code).$this->endPuml(); } private function isWorkflowTransitionType(): bool @@ -125,15 +123,12 @@ private function isWorkflowTransitionType(): bool return self::WORKFLOW_TRANSITION === $this->transitionType; } - private function startPuml(array $options): string + private function startPuml(): string { - $start = '@startuml'.\PHP_EOL; - $start .= 'allow_mixing'.\PHP_EOL; - - return $start; + return '@startuml'.\PHP_EOL.'allow_mixing'.\PHP_EOL; } - private function endPuml(array $options): string + private function endPuml(): string { return \PHP_EOL.'@enduml'; } @@ -228,9 +223,9 @@ private function getTransitionEscapedWithStyle(MetadataStoreInterface $workflowM if (null !== $color) { // Close and open before and after every '\n' string, // so that the style is applied properly on every line - $to = str_replace('\n', sprintf('\n', $color), $to); + $to = str_replace('\n', \sprintf('\n', $color), $to); - $to = sprintf( + $to = \sprintf( '%2$s', $color, $to @@ -247,7 +242,7 @@ private function getTransitionColor(string $color): string $color = '#'.$color; } - return sprintf('[%s]', $color); + return \sprintf('[%s]', $color); } private function getColorId(string $color): string diff --git a/Dumper/StateMachineGraphvizDumper.php b/Dumper/StateMachineGraphvizDumper.php index e054cb4..7bd9d73 100644 --- a/Dumper/StateMachineGraphvizDumper.php +++ b/Dumper/StateMachineGraphvizDumper.php @@ -89,7 +89,7 @@ protected function addEdges(array $edges): string foreach ($edges as $id => $edges) { foreach ($edges as $edge) { - $code .= sprintf( + $code .= \sprintf( " place_%s -> place_%s [label=\"%s\" style=\"%s\"%s];\n", $this->dotize($id), $this->dotize($edge['to']), diff --git a/Event/Event.php b/Event/Event.php index 1efbc79..c13818b 100644 --- a/Event/Event.php +++ b/Event/Event.php @@ -23,17 +23,12 @@ */ class Event extends BaseEvent { - private object $subject; - private Marking $marking; - private ?Transition $transition; - private ?WorkflowInterface $workflow; - - public function __construct(object $subject, Marking $marking, ?Transition $transition = null, ?WorkflowInterface $workflow = null) - { - $this->subject = $subject; - $this->marking = $marking; - $this->transition = $transition; - $this->workflow = $workflow; + public function __construct( + private object $subject, + private Marking $marking, + private ?Transition $transition = null, + private ?WorkflowInterface $workflow = null, + ) { } public function getMarking(): Marking @@ -51,8 +46,13 @@ public function getTransition(): ?Transition return $this->transition; } + /** + * @deprecated since Symfony 7.3, inject the workflow in the constructor where you need it + */ public function getWorkflow(): WorkflowInterface { + trigger_deprecation('symfony/workflow', '7.3', 'The "%s()" method is deprecated, inject the workflow in the constructor where you need it.', __METHOD__); + return $this->workflow; } diff --git a/Event/EventNameTrait.php b/Event/EventNameTrait.php index 97b804c..1f77b37 100644 --- a/Event/EventNameTrait.php +++ b/Event/EventNameTrait.php @@ -49,13 +49,13 @@ private static function computeName(?string $workflowName, ?string $transitionOr throw new \InvalidArgumentException('Missing workflow name.'); } - return sprintf('workflow.%s', $eventName); + return \sprintf('workflow.%s', $eventName); } if (null === $transitionOrPlaceName) { - return sprintf('workflow.%s.%s', $workflowName, $eventName); + return \sprintf('workflow.%s.%s', $workflowName, $eventName); } - return sprintf('workflow.%s.%s.%s', $workflowName, $eventName, $transitionOrPlaceName); + return \sprintf('workflow.%s.%s.%s', $workflowName, $eventName, $transitionOrPlaceName); } } diff --git a/EventListener/AuditTrailListener.php b/EventListener/AuditTrailListener.php index b5e2734..fe7ccdf 100644 --- a/EventListener/AuditTrailListener.php +++ b/EventListener/AuditTrailListener.php @@ -20,29 +20,27 @@ */ class AuditTrailListener implements EventSubscriberInterface { - private LoggerInterface $logger; - - public function __construct(LoggerInterface $logger) - { - $this->logger = $logger; + public function __construct( + private LoggerInterface $logger, + ) { } public function onLeave(Event $event): void { foreach ($event->getTransition()->getFroms() as $place) { - $this->logger->info(sprintf('Leaving "%s" for subject of class "%s" in workflow "%s".', $place, $event->getSubject()::class, $event->getWorkflowName())); + $this->logger->info(\sprintf('Leaving "%s" for subject of class "%s" in workflow "%s".', $place, $event->getSubject()::class, $event->getWorkflowName())); } } public function onTransition(Event $event): void { - $this->logger->info(sprintf('Transition "%s" for subject of class "%s" in workflow "%s".', $event->getTransition()->getName(), $event->getSubject()::class, $event->getWorkflowName())); + $this->logger->info(\sprintf('Transition "%s" for subject of class "%s" in workflow "%s".', $event->getTransition()->getName(), $event->getSubject()::class, $event->getWorkflowName())); } public function onEnter(Event $event): void { foreach ($event->getTransition()->getTos() as $place) { - $this->logger->info(sprintf('Entering "%s" for subject of class "%s" in workflow "%s".', $place, $event->getSubject()::class, $event->getWorkflowName())); + $this->logger->info(\sprintf('Entering "%s" for subject of class "%s" in workflow "%s".', $place, $event->getSubject()::class, $event->getWorkflowName())); } } diff --git a/EventListener/ExpressionLanguage.php b/EventListener/ExpressionLanguage.php index a70e5f7..257f885 100644 --- a/EventListener/ExpressionLanguage.php +++ b/EventListener/ExpressionLanguage.php @@ -26,9 +26,9 @@ protected function registerFunctions(): void { parent::registerFunctions(); - $this->register('is_granted', fn ($attributes, $object = 'null') => sprintf('$auth_checker->isGranted(%s, %s)', $attributes, $object), fn (array $variables, $attributes, $object = null) => $variables['auth_checker']->isGranted($attributes, $object)); + $this->register('is_granted', fn ($attributes, $object = 'null') => \sprintf('$auth_checker->isGranted(%s, %s)', $attributes, $object), fn (array $variables, $attributes, $object = null) => $variables['auth_checker']->isGranted($attributes, $object)); - $this->register('is_valid', fn ($object = 'null', $groups = 'null') => sprintf('0 === count($validator->validate(%s, null, %s))', $object, $groups), function (array $variables, $object = null, $groups = null) { + $this->register('is_valid', fn ($object = 'null', $groups = 'null') => \sprintf('0 === count($validator->validate(%s, null, %s))', $object, $groups), function (array $variables, $object = null, $groups = null) { if (!$variables['validator'] instanceof ValidatorInterface) { throw new RuntimeException('"is_valid" cannot be used as the Validator component is not installed. Try running "composer require symfony/validator".'); } diff --git a/EventListener/GuardExpression.php b/EventListener/GuardExpression.php index 27bf8dc..deb148d 100644 --- a/EventListener/GuardExpression.php +++ b/EventListener/GuardExpression.php @@ -15,13 +15,10 @@ class GuardExpression { - private Transition $transition; - private string $expression; - - public function __construct(Transition $transition, string $expression) - { - $this->transition = $transition; - $this->expression = $expression; + public function __construct( + private Transition $transition, + private string $expression, + ) { } public function getTransition(): Transition diff --git a/EventListener/GuardListener.php b/EventListener/GuardListener.php index 6972a89..23cdd7a 100644 --- a/EventListener/GuardListener.php +++ b/EventListener/GuardListener.php @@ -24,23 +24,15 @@ */ class GuardListener { - private array $configuration; - private ExpressionLanguage $expressionLanguage; - private TokenStorageInterface $tokenStorage; - private AuthorizationCheckerInterface $authorizationChecker; - private AuthenticationTrustResolverInterface $trustResolver; - private ?RoleHierarchyInterface $roleHierarchy; - private ?ValidatorInterface $validator; - - public function __construct(array $configuration, ExpressionLanguage $expressionLanguage, TokenStorageInterface $tokenStorage, AuthorizationCheckerInterface $authorizationChecker, AuthenticationTrustResolverInterface $trustResolver, ?RoleHierarchyInterface $roleHierarchy = null, ?ValidatorInterface $validator = null) - { - $this->configuration = $configuration; - $this->expressionLanguage = $expressionLanguage; - $this->tokenStorage = $tokenStorage; - $this->authorizationChecker = $authorizationChecker; - $this->trustResolver = $trustResolver; - $this->roleHierarchy = $roleHierarchy; - $this->validator = $validator; + public function __construct( + private array $configuration, + private ExpressionLanguage $expressionLanguage, + private TokenStorageInterface $tokenStorage, + private AuthorizationCheckerInterface $authorizationChecker, + private AuthenticationTrustResolverInterface $trustResolver, + private ?RoleHierarchyInterface $roleHierarchy = null, + private ?ValidatorInterface $validator = null, + ) { } public function onTransition(GuardEvent $event, string $eventName): void diff --git a/Exception/NotEnabledTransitionException.php b/Exception/NotEnabledTransitionException.php index 4144caf..26d1c8d 100644 --- a/Exception/NotEnabledTransitionException.php +++ b/Exception/NotEnabledTransitionException.php @@ -15,19 +15,20 @@ use Symfony\Component\Workflow\WorkflowInterface; /** - * Thrown by Workflow when a not enabled transition is applied on a subject. + * Thrown when a transition cannot be applied on a subject. * * @author Grégoire Pineau */ class NotEnabledTransitionException extends TransitionException { - private TransitionBlockerList $transitionBlockerList; - - public function __construct(object $subject, string $transitionName, WorkflowInterface $workflow, TransitionBlockerList $transitionBlockerList, array $context = []) - { - parent::__construct($subject, $transitionName, $workflow, sprintf('Transition "%s" is not enabled for workflow "%s".', $transitionName, $workflow->getName()), $context); - - $this->transitionBlockerList = $transitionBlockerList; + public function __construct( + object $subject, + string $transitionName, + WorkflowInterface $workflow, + private TransitionBlockerList $transitionBlockerList, + array $context = [], + ) { + parent::__construct($subject, $transitionName, $workflow, \sprintf('Cannot apply transition "%s" on workflow "%s".', $transitionName, $workflow->getName()), $context); } public function getTransitionBlockerList(): TransitionBlockerList diff --git a/Exception/TransitionException.php b/Exception/TransitionException.php index 0cf1afb..e5c3846 100644 --- a/Exception/TransitionException.php +++ b/Exception/TransitionException.php @@ -19,19 +19,14 @@ */ class TransitionException extends LogicException { - private object $subject; - private string $transitionName; - private WorkflowInterface $workflow; - private array $context; - - public function __construct(object $subject, string $transitionName, WorkflowInterface $workflow, string $message, array $context = []) - { + public function __construct( + private object $subject, + private string $transitionName, + private WorkflowInterface $workflow, + string $message, + private array $context = [], + ) { parent::__construct($message); - - $this->subject = $subject; - $this->transitionName = $transitionName; - $this->workflow = $workflow; - $this->context = $context; } public function getSubject(): object diff --git a/Exception/UndefinedTransitionException.php b/Exception/UndefinedTransitionException.php index 75d3848..5a8ecf8 100644 --- a/Exception/UndefinedTransitionException.php +++ b/Exception/UndefinedTransitionException.php @@ -22,6 +22,6 @@ class UndefinedTransitionException extends TransitionException { public function __construct(object $subject, string $transitionName, WorkflowInterface $workflow, array $context = []) { - parent::__construct($subject, $transitionName, $workflow, sprintf('Transition "%s" is not defined for workflow "%s".', $transitionName, $workflow->getName()), $context); + parent::__construct($subject, $transitionName, $workflow, \sprintf('Transition "%s" is not defined for workflow "%s".', $transitionName, $workflow->getName()), $context); } } diff --git a/Marking.php b/Marking.php index 58f5ec6..c3629a2 100644 --- a/Marking.php +++ b/Marking.php @@ -41,7 +41,7 @@ public function mark(string $place /* , int $nbToken = 1 */): void $nbToken = 1 < \func_num_args() ? func_get_arg(1) : 1; if ($nbToken < 1) { - throw new \InvalidArgumentException(sprintf('The number of tokens must be greater than 0, "%s" given.', $nbToken)); + throw new \InvalidArgumentException(\sprintf('The number of tokens must be greater than 0, "%s" given.', $nbToken)); } $this->places[$place] ??= 0; @@ -58,17 +58,17 @@ public function unmark(string $place /* , int $nbToken = 1 */): void $nbToken = 1 < \func_num_args() ? func_get_arg(1) : 1; if ($nbToken < 1) { - throw new \InvalidArgumentException(sprintf('The number of tokens must be greater than 0, "%s" given.', $nbToken)); + throw new \InvalidArgumentException(\sprintf('The number of tokens must be greater than 0, "%s" given.', $nbToken)); } if (!$this->has($place)) { - throw new \InvalidArgumentException(sprintf('The place "%s" is not marked.', $place)); + throw new \InvalidArgumentException(\sprintf('The place "%s" is not marked.', $place)); } $tokenCount = $this->places[$place] - $nbToken; if (0 > $tokenCount) { - throw new \InvalidArgumentException(sprintf('The place "%s" could not contain a negative token number: "%s" (initial) - "%s" (nbToken) = "%s".', $place, $this->places[$place], $nbToken, $tokenCount)); + throw new \InvalidArgumentException(\sprintf('The place "%s" could not contain a negative token number: "%s" (initial) - "%s" (nbToken) = "%s".', $place, $this->places[$place], $nbToken, $tokenCount)); } if (0 === $tokenCount) { diff --git a/MarkingStore/MethodMarkingStore.php b/MarkingStore/MethodMarkingStore.php index 773328f..a2844b7 100644 --- a/MarkingStore/MethodMarkingStore.php +++ b/MarkingStore/MethodMarkingStore.php @@ -53,7 +53,7 @@ public function getMarking(object $subject): Marking try { $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); + $unInitializedPropertyMessage = \sprintf('Typed property %s::$%s must not be accessed before initialization', get_debug_type($subject), $this->property); if ($e->getMessage() !== $unInitializedPropertyMessage) { throw $e; } @@ -66,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 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)); + 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); @@ -88,7 +88,7 @@ private function getGetter(object $subject): callable $property = $this->property; $method = 'get'.ucfirst($property); - return match ($this->getters[$subject::class] ??= $this->getType($subject, $property, $method)) { + return match ($this->getters[$subject::class] ??= self::getType($subject, $property, $method)) { MarkingStoreMethod::METHOD => $subject->{$method}(...), MarkingStoreMethod::PROPERTY => static fn () => $subject->{$property}, }; @@ -99,7 +99,7 @@ private function getSetter(object $subject): callable $property = $this->property; $method = 'set'.ucfirst($property); - return match ($this->setters[$subject::class] ??= $this->getType($subject, $property, $method)) { + return match ($this->setters[$subject::class] ??= self::getType($subject, $property, $method)) { MarkingStoreMethod::METHOD => $subject->{$method}(...), MarkingStoreMethod::PROPERTY => static fn ($marking) => $subject->{$property} = $marking, }; @@ -118,7 +118,7 @@ private static function getType(object $subject, string $property, string $metho } catch (\ReflectionException) { } - 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)); + 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)); } } diff --git a/Metadata/InMemoryMetadataStore.php b/Metadata/InMemoryMetadataStore.php index d13f956..b88514b 100644 --- a/Metadata/InMemoryMetadataStore.php +++ b/Metadata/InMemoryMetadataStore.php @@ -20,17 +20,16 @@ final class InMemoryMetadataStore implements MetadataStoreInterface { use GetMetadataTrait; - private array $workflowMetadata; - private array $placesMetadata; private \SplObjectStorage $transitionsMetadata; /** * @param \SplObjectStorage|null $transitionsMetadata */ - public function __construct(array $workflowMetadata = [], array $placesMetadata = [], ?\SplObjectStorage $transitionsMetadata = null) - { - $this->workflowMetadata = $workflowMetadata; - $this->placesMetadata = $placesMetadata; + public function __construct( + private array $workflowMetadata = [], + private array $placesMetadata = [], + ?\SplObjectStorage $transitionsMetadata = null, + ) { $this->transitionsMetadata = $transitionsMetadata ?? new \SplObjectStorage(); } diff --git a/Registry.php b/Registry.php index 787bc21..08017a3 100644 --- a/Registry.php +++ b/Registry.php @@ -49,13 +49,13 @@ public function get(object $subject, ?string $workflowName = null): WorkflowInte } if (!$matched) { - throw new InvalidArgumentException(sprintf('Unable to find a workflow for class "%s".', get_debug_type($subject))); + throw new InvalidArgumentException(\sprintf('Unable to find a workflow for class "%s".', get_debug_type($subject))); } if (2 <= \count($matched)) { $names = array_map(static fn (WorkflowInterface $workflow): string => $workflow->getName(), $matched); - throw new InvalidArgumentException(sprintf('Too many workflows (%s) match this subject (%s); set a different name on each and use the second (name) argument of this method.', implode(', ', $names), get_debug_type($subject))); + throw new InvalidArgumentException(\sprintf('Too many workflows (%s) match this subject (%s); set a different name on each and use the second (name) argument of this method.', implode(', ', $names), get_debug_type($subject))); } return $matched[0]; diff --git a/SupportStrategy/InstanceOfSupportStrategy.php b/SupportStrategy/InstanceOfSupportStrategy.php index 86bd107..8d8a4b5 100644 --- a/SupportStrategy/InstanceOfSupportStrategy.php +++ b/SupportStrategy/InstanceOfSupportStrategy.php @@ -19,11 +19,9 @@ */ final class InstanceOfSupportStrategy implements WorkflowSupportStrategyInterface { - private string $className; - - public function __construct(string $className) - { - $this->className = $className; + public function __construct( + private string $className, + ) { } public function supports(WorkflowInterface $workflow, object $subject): bool diff --git a/Tests/Attribute/AsListenerTest.php b/Tests/Attribute/AsListenerTest.php index a858626..0a8c232 100644 --- a/Tests/Attribute/AsListenerTest.php +++ b/Tests/Attribute/AsListenerTest.php @@ -64,7 +64,7 @@ public static function provideOkTests(): iterable public function testTransitionThrowException(string $class) { $this->expectException(LogicException::class); - $this->expectExceptionMessage(sprintf('The "transition" argument of "%s" cannot be used without a "workflow" argument.', $class)); + $this->expectExceptionMessage(\sprintf('The "transition" argument of "%s" cannot be used without a "workflow" argument.', $class)); new $class(transition: 'some'); } @@ -83,7 +83,7 @@ public static function provideTransitionThrowException(): iterable public function testPlaceThrowException(string $class) { $this->expectException(LogicException::class); - $this->expectExceptionMessage(sprintf('The "place" argument of "%s" cannot be used without a "workflow" argument.', $class)); + $this->expectExceptionMessage(\sprintf('The "place" argument of "%s" cannot be used without a "workflow" argument.', $class)); new $class(place: 'some'); } diff --git a/Tests/Debug/TraceableWorkflowTest.php b/Tests/Debug/TraceableWorkflowTest.php index 3d8e699..257ad66 100644 --- a/Tests/Debug/TraceableWorkflowTest.php +++ b/Tests/Debug/TraceableWorkflowTest.php @@ -21,7 +21,7 @@ class TraceableWorkflowTest extends TestCase { - private MockObject|Workflow $innerWorkflow; + private MockObject&Workflow $innerWorkflow; private Stopwatch $stopwatch; diff --git a/Tests/DependencyInjection/WorkflowValidatorPassTest.php b/Tests/DependencyInjection/WorkflowValidatorPassTest.php new file mode 100644 index 0000000..213e0d4 --- /dev/null +++ b/Tests/DependencyInjection/WorkflowValidatorPassTest.php @@ -0,0 +1,74 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Workflow\Tests\DependencyInjection; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\Workflow\Definition; +use Symfony\Component\Workflow\DependencyInjection\WorkflowValidatorPass; +use Symfony\Component\Workflow\Validator\DefinitionValidatorInterface; +use Symfony\Component\Workflow\WorkflowInterface; + +class WorkflowValidatorPassTest extends TestCase +{ + private ContainerBuilder $container; + private WorkflowValidatorPass $compilerPass; + + protected function setUp(): void + { + $this->container = new ContainerBuilder(); + $this->compilerPass = new WorkflowValidatorPass(); + } + + public function testNothingToDo() + { + $this->compilerPass->process($this->container); + + $this->assertFalse(DefinitionValidator::$called); + } + + public function testValidate() + { + $this + ->container + ->register('my.workflow', WorkflowInterface::class) + ->addTag('workflow', [ + 'definition_id' => 'my.workflow.definition', + 'name' => 'my.workflow', + 'definition_validators' => [DefinitionValidator::class], + ]) + ; + + $this + ->container + ->register('my.workflow.definition', Definition::class) + ->setArguments([ + '$places' => [], + '$transitions' => [], + ]) + ; + + $this->compilerPass->process($this->container); + + $this->assertTrue(DefinitionValidator::$called); + } +} + +class DefinitionValidator implements DefinitionValidatorInterface +{ + public static bool $called = false; + + public function validate(Definition $definition, string $name): void + { + self::$called = true; + } +} diff --git a/Tests/Dumper/MermaidDumperTest.php b/Tests/Dumper/MermaidDumperTest.php index 3a29da6..a8d1978 100644 --- a/Tests/Dumper/MermaidDumperTest.php +++ b/Tests/Dumper/MermaidDumperTest.php @@ -104,7 +104,7 @@ public static function provideWorkflowDefinitionWithoutMarking(): iterable ."transition4-->place6\n" ."transition5[\"t6\"]\n" ."place5-->transition5\n" - ."transition5-->place6", + .'transition5-->place6', ]; yield [ self::createWorkflowWithSameNameTransition(), @@ -124,7 +124,7 @@ public static function provideWorkflowDefinitionWithoutMarking(): iterable ."transition2-->place0\n" ."transition3[\"to_a\"]\n" ."place2-->transition3\n" - ."transition3-->place0", + .'transition3-->place0', ]; yield [ self::createSimpleWorkflowDefinition(), @@ -140,7 +140,7 @@ public static function provideWorkflowDefinitionWithoutMarking(): iterable ."linkStyle 1 stroke:Grey\n" ."transition1[\"t2\"]\n" ."place1-->transition1\n" - ."transition1-->place2", + .'transition1-->place2', ]; } @@ -169,7 +169,7 @@ public static function provideWorkflowWithReservedWords(): iterable ."place1-->transition0\n" ."transition1[\"t1\"]\n" ."place2-->transition1\n" - ."transition1-->place3", + .'transition1-->place3', ]; } @@ -186,7 +186,7 @@ public static function provideStateMachine(): iterable ."place3-->|\"My custom transition label 3\"|place1\n" ."linkStyle 1 stroke:Grey\n" ."place1-->|\"t2\"|place2\n" - ."place1-->|\"t3\"|place3", + .'place1-->|"t3"|place3', ]; } @@ -212,7 +212,7 @@ public static function provideWorkflowWithMarking(): iterable ."linkStyle 1 stroke:Grey\n" ."transition1[\"t2\"]\n" ."place1-->transition1\n" - ."transition1-->place2", + .'transition1-->place2', ]; } } diff --git a/Tests/StateMachineTest.php b/Tests/StateMachineTest.php index e991707..5d10fde 100644 --- a/Tests/StateMachineTest.php +++ b/Tests/StateMachineTest.php @@ -88,7 +88,7 @@ public function testBuildTransitionBlockerListReturnsExpectedReasonOnBranchMerge $net = new StateMachine($definition, null, $dispatcher); $dispatcher->addListener('workflow.guard', function (GuardEvent $event) { - $event->addTransitionBlocker(new TransitionBlocker(sprintf('Transition blocker of place %s', $event->getTransition()->getFroms()[0]), 'blocker')); + $event->addTransitionBlocker(new TransitionBlocker(\sprintf('Transition blocker of place %s', $event->getTransition()->getFroms()[0]), 'blocker')); }); $subject = new Subject(); @@ -124,7 +124,7 @@ public function testApplyReturnsExpectedReasonOnBranchMerge() $net = new StateMachine($definition, null, $dispatcher); $dispatcher->addListener('workflow.guard', function (GuardEvent $event) { - $event->addTransitionBlocker(new TransitionBlocker(sprintf('Transition blocker of place %s', $event->getTransition()->getFroms()[0]), 'blocker')); + $event->addTransitionBlocker(new TransitionBlocker(\sprintf('Transition blocker of place %s', $event->getTransition()->getFroms()[0]), 'blocker')); }); $subject = new Subject(); diff --git a/Tests/Validator/WorkflowValidatorTest.php b/Tests/Validator/WorkflowValidatorTest.php index 34eeda6..50c3abd 100644 --- a/Tests/Validator/WorkflowValidatorTest.php +++ b/Tests/Validator/WorkflowValidatorTest.php @@ -55,4 +55,32 @@ public function testSameTransitionNameButNotSamePlace() // the test ensures that the validation does not fail (i.e. it does not throw any exceptions) $this->addToAssertionCount(1); } + + public function testWithTooManyOutput() + { + $places = ['a', 'b', 'c']; + $transitions = [ + new Transition('t1', 'a', ['b', 'c']), + ]; + $definition = new Definition($places, $transitions); + + $this->expectException(InvalidDefinitionException::class); + $this->expectExceptionMessage('The marking store of workflow "foo" cannot store many places. But the transition "t1" has too many output (2). Only one is accepted.'); + + (new WorkflowValidator(true))->validate($definition, 'foo'); + } + + public function testWithTooManyInitialPlaces() + { + $places = ['a', 'b', 'c']; + $transitions = [ + new Transition('t1', 'a', 'b'), + ]; + $definition = new Definition($places, $transitions, ['a', 'b']); + + $this->expectException(InvalidDefinitionException::class); + $this->expectExceptionMessage('The marking store of workflow "foo" cannot store many places. But the definition has 2 initial places. Only one is supported.'); + + (new WorkflowValidator(true))->validate($definition, 'foo'); + } } diff --git a/Tests/WorkflowTest.php b/Tests/WorkflowTest.php index 83af790..48e2209 100644 --- a/Tests/WorkflowTest.php +++ b/Tests/WorkflowTest.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\Workflow\Definition; +use Symfony\Component\Workflow\Event\EnteredEvent; use Symfony\Component\Workflow\Event\Event; use Symfony\Component\Workflow\Event\GuardEvent; use Symfony\Component\Workflow\Event\TransitionEvent; @@ -286,7 +287,7 @@ public function testApplyWithNotEnabledTransition() $this->fail('Should throw an exception'); } catch (NotEnabledTransitionException $e) { - $this->assertSame('Transition "t2" is not enabled for workflow "unnamed".', $e->getMessage()); + $this->assertSame('Cannot apply transition "t2" on workflow "unnamed".', $e->getMessage()); $this->assertCount(1, $e->getTransitionBlockerList()); $list = iterator_to_array($e->getTransitionBlockerList()); $this->assertSame('The marking does not enable the transition.', $list[0]->getMessage()); @@ -689,6 +690,44 @@ public function testEventDefaultInitialContext() $workflow->apply($subject, 't1'); } + public function testEventWhenAlreadyInThisPlace() + { + // ┌──────┐ ┌──────────────────────┐ ┌───┐ ┌─────────────┐ ┌───┐ + // │ init │ ──▶ │ from_init_to_a_and_b │ ──▶ │ B │ ──▶ │ from_b_to_c │ ──▶ │ C │ + // └──────┘ └──────────────────────┘ └───┘ └─────────────┘ └───┘ + // │ + // │ + // ▼ + // ┌───────────────────────────────┐ + // │ A │ + // └───────────────────────────────┘ + $definition = new Definition( + ['init', 'A', 'B', 'C'], + [ + new Transition('from_init_to_a_and_b', 'init', ['A', 'B']), + new Transition('from_b_to_c', 'B', 'C'), + ], + ); + + $subject = new Subject(); + $dispatcher = new EventDispatcher(); + $name = 'workflow_name'; + $workflow = new Workflow($definition, new MethodMarkingStore(), $dispatcher, $name); + + $calls = []; + $listener = function (Event $event) use (&$calls) { + $calls[] = $event; + }; + $dispatcher->addListener("workflow.$name.entered.A", $listener); + + $workflow->apply($subject, 'from_init_to_a_and_b'); + $workflow->apply($subject, 'from_b_to_c'); + + $this->assertCount(1, $calls); + $this->assertInstanceOf(EnteredEvent::class, $calls[0]); + $this->assertSame('from_init_to_a_and_b', $calls[0]->getTransition()->getName()); + } + public function testMarkingStateOnApplyWithEventDispatcher() { $definition = new Definition(range('a', 'f'), [new Transition('t', range('a', 'c'), range('d', 'f'))]); @@ -734,7 +773,7 @@ public function testGetEnabledTransitions() }); $workflow = new Workflow($definition, new MethodMarkingStore(), $eventDispatcher, 'workflow_name'); - $this->assertEmpty($workflow->getEnabledTransitions($subject)); + $this->assertSame([], $workflow->getEnabledTransitions($subject)); $subject->setMarking(['d' => 1]); $transitions = $workflow->getEnabledTransitions($subject); diff --git a/Transition.php b/Transition.php index 50d834b..05fe267 100644 --- a/Transition.php +++ b/Transition.php @@ -17,7 +17,6 @@ */ class Transition { - private string $name; private array $froms; private array $tos; @@ -25,9 +24,11 @@ class Transition * @param string|string[] $froms * @param string|string[] $tos */ - public function __construct(string $name, string|array $froms, string|array $tos) - { - $this->name = $name; + public function __construct( + private string $name, + string|array $froms, + string|array $tos, + ) { $this->froms = (array) $froms; $this->tos = (array) $tos; } diff --git a/TransitionBlocker.php b/TransitionBlocker.php index 4864598..6a745a2 100644 --- a/TransitionBlocker.php +++ b/TransitionBlocker.php @@ -20,21 +20,17 @@ final class TransitionBlocker public const BLOCKED_BY_EXPRESSION_GUARD_LISTENER = '326a1e9c-0c12-11e8-ba89-0ed5f89f718b'; public const UNKNOWN = 'e8b5bbb9-5913-4b98-bfa6-65dbd228a82a'; - private string $message; - private string $code; - private array $parameters; - /** * @param string $code Code is a machine-readable string, usually an UUID * @param array $parameters This is useful if you would like to pass around the condition values, that * blocked the transition. E.g. for a condition "distance must be larger than * 5 miles", you might want to pass around the value of 5. */ - public function __construct(string $message, string $code, array $parameters = []) - { - $this->message = $message; - $this->code = $code; - $this->parameters = $parameters; + public function __construct( + private string $message, + private string $code, + private array $parameters = [], + ) { } /** diff --git a/Validator/StateMachineValidator.php b/Validator/StateMachineValidator.php index 521fc88..626a20e 100644 --- a/Validator/StateMachineValidator.php +++ b/Validator/StateMachineValidator.php @@ -25,19 +25,19 @@ public function validate(Definition $definition, string $name): void foreach ($definition->getTransitions() as $transition) { // Make sure that each transition has exactly one TO if (1 !== \count($transition->getTos())) { - throw new InvalidDefinitionException(sprintf('A transition in StateMachine can only have one output. But the transition "%s" in StateMachine "%s" has %d outputs.', $transition->getName(), $name, \count($transition->getTos()))); + throw new InvalidDefinitionException(\sprintf('A transition in StateMachine can only have one output. But the transition "%s" in StateMachine "%s" has %d outputs.', $transition->getName(), $name, \count($transition->getTos()))); } // Make sure that each transition has exactly one FROM $froms = $transition->getFroms(); if (1 !== \count($froms)) { - throw new InvalidDefinitionException(sprintf('A transition in StateMachine can only have one input. But the transition "%s" in StateMachine "%s" has %d inputs.', $transition->getName(), $name, \count($froms))); + throw new InvalidDefinitionException(\sprintf('A transition in StateMachine can only have one input. But the transition "%s" in StateMachine "%s" has %d inputs.', $transition->getName(), $name, \count($froms))); } // Enforcing uniqueness of the names of transitions starting at each node $from = reset($froms); if (isset($transitionFromNames[$from][$transition->getName()])) { - throw new InvalidDefinitionException(sprintf('A transition from a place/state must have an unique name. Multiple transitions named "%s" from place/state "%s" were found on StateMachine "%s".', $transition->getName(), $from, $name)); + throw new InvalidDefinitionException(\sprintf('A transition from a place/state must have an unique name. Multiple transitions named "%s" from place/state "%s" were found on StateMachine "%s".', $transition->getName(), $from, $name)); } $transitionFromNames[$from][$transition->getName()] = true; @@ -45,7 +45,7 @@ public function validate(Definition $definition, string $name): void $initialPlaces = $definition->getInitialPlaces(); if (2 <= \count($initialPlaces)) { - throw new InvalidDefinitionException(sprintf('The state machine "%s" cannot store many places. But the definition has %d initial places. Only one is supported.', $name, \count($initialPlaces))); + throw new InvalidDefinitionException(\sprintf('The state machine "%s" cannot store many places. But the definition has %d initial places. Only one is supported.', $name, \count($initialPlaces))); } } } diff --git a/Validator/WorkflowValidator.php b/Validator/WorkflowValidator.php index 2afefb7..f4eb292 100644 --- a/Validator/WorkflowValidator.php +++ b/Validator/WorkflowValidator.php @@ -20,11 +20,9 @@ */ class WorkflowValidator implements DefinitionValidatorInterface { - private bool $singlePlace; - - public function __construct(bool $singlePlace = false) - { - $this->singlePlace = $singlePlace; + public function __construct( + private bool $singlePlace = false, + ) { } public function validate(Definition $definition, string $name): void @@ -34,7 +32,7 @@ public function validate(Definition $definition, string $name): void foreach ($definition->getTransitions() as $transition) { foreach ($transition->getFroms() as $from) { if (\in_array($transition->getName(), $places[$from], true)) { - throw new InvalidDefinitionException(sprintf('All transitions for a place must have an unique name. Multiple transitions named "%s" where found for place "%s" in workflow "%s".', $transition->getName(), $from, $name)); + throw new InvalidDefinitionException(\sprintf('All transitions for a place must have an unique name. Multiple transitions named "%s" where found for place "%s" in workflow "%s".', $transition->getName(), $from, $name)); } $places[$from][] = $transition->getName(); } @@ -46,13 +44,13 @@ public function validate(Definition $definition, string $name): void foreach ($definition->getTransitions() as $transition) { if (1 < \count($transition->getTos())) { - throw new InvalidDefinitionException(sprintf('The marking store of workflow "%s" cannot store many places. But the transition "%s" has too many output (%d). Only one is accepted.', $name, $transition->getName(), \count($transition->getTos()))); + throw new InvalidDefinitionException(\sprintf('The marking store of workflow "%s" cannot store many places. But the transition "%s" has too many output (%d). Only one is accepted.', $name, $transition->getName(), \count($transition->getTos()))); } } $initialPlaces = $definition->getInitialPlaces(); if (2 <= \count($initialPlaces)) { - throw new InvalidDefinitionException(sprintf('The marking store of workflow "%s" cannot store many places. But the definition has %d initial places. Only one is supported.', $name, \count($initialPlaces))); + throw new InvalidDefinitionException(\sprintf('The marking store of workflow "%s" cannot store many places. But the definition has %d initial places. Only one is supported.', $name, \count($initialPlaces))); } } } diff --git a/Workflow.php b/Workflow.php index cfad75f..9165ebb 100644 --- a/Workflow.php +++ b/Workflow.php @@ -52,28 +52,22 @@ class Workflow implements WorkflowInterface WorkflowEvents::ANNOUNCE => self::DISABLE_ANNOUNCE_EVENT, ]; - private Definition $definition; private MarkingStoreInterface $markingStore; - private ?EventDispatcherInterface $dispatcher; - private string $name; /** - * When `null` fire all events (the default behaviour). - * Setting this to an empty array `[]` means no events are dispatched (except the {@see GuardEvent}). - * Passing an array with WorkflowEvents will allow only those events to be dispatched plus - * the {@see GuardEvent}. - * - * @var array|string[]|null + * @param array|string[]|null $eventsToDispatch When `null` fire all events (the default behaviour). + * Setting this to an empty array `[]` means no events are dispatched (except the {@see GuardEvent}). + * Passing an array with WorkflowEvents will allow only those events to be dispatched plus + * the {@see GuardEvent}. */ - private ?array $eventsToDispatch = null; - - public function __construct(Definition $definition, ?MarkingStoreInterface $markingStore = null, ?EventDispatcherInterface $dispatcher = null, string $name = 'unnamed', ?array $eventsToDispatch = null) - { - $this->definition = $definition; + public function __construct( + private Definition $definition, + ?MarkingStoreInterface $markingStore = null, + private ?EventDispatcherInterface $dispatcher = null, + private string $name = 'unnamed', + private ?array $eventsToDispatch = null, + ) { $this->markingStore = $markingStore ?? new MethodMarkingStore(); - $this->dispatcher = $dispatcher; - $this->name = $name; - $this->eventsToDispatch = $eventsToDispatch; } public function getMarking(object $subject, array $context = []): Marking @@ -83,7 +77,7 @@ public function getMarking(object $subject, array $context = []): Marking // check if the subject is already in the workflow if (!$marking->getPlaces()) { if (!$this->definition->getInitialPlaces()) { - throw new LogicException(sprintf('The Marking is empty and there is no initial place for workflow "%s".', $this->name)); + throw new LogicException(\sprintf('The Marking is empty and there is no initial place for workflow "%s".', $this->name)); } foreach ($this->definition->getInitialPlaces() as $place) { $marking->mark($place); @@ -103,7 +97,7 @@ public function getMarking(object $subject, array $context = []): Marking $places = $this->definition->getPlaces(); foreach ($marking->getPlaces() as $placeName => $nbToken) { if (!isset($places[$placeName])) { - $message = sprintf('Place "%s" is not valid for workflow "%s".', $placeName, $this->name); + $message = \sprintf('Place "%s" is not valid for workflow "%s".', $placeName, $this->name); if (!$places) { $message .= ' It seems you forgot to add places to the current workflow.'; } @@ -319,8 +313,8 @@ private function guardTransition(object $subject, Marking $marking, Transition $ $event = new GuardEvent($subject, $marking, $transition, $this); $this->dispatcher->dispatch($event, WorkflowEvents::GUARD); - $this->dispatcher->dispatch($event, sprintf('workflow.%s.guard', $this->name)); - $this->dispatcher->dispatch($event, sprintf('workflow.%s.guard.%s', $this->name, $transition->getName())); + $this->dispatcher->dispatch($event, \sprintf('workflow.%s.guard', $this->name)); + $this->dispatcher->dispatch($event, \sprintf('workflow.%s.guard.%s', $this->name, $transition->getName())); return $event; } @@ -333,10 +327,10 @@ private function leave(object $subject, Transition $transition, Marking $marking $event = new LeaveEvent($subject, $marking, $transition, $this, $context); $this->dispatcher->dispatch($event, WorkflowEvents::LEAVE); - $this->dispatcher->dispatch($event, sprintf('workflow.%s.leave', $this->name)); + $this->dispatcher->dispatch($event, \sprintf('workflow.%s.leave', $this->name)); foreach ($places as $place) { - $this->dispatcher->dispatch($event, sprintf('workflow.%s.leave.%s', $this->name, $place)); + $this->dispatcher->dispatch($event, \sprintf('workflow.%s.leave.%s', $this->name, $place)); } } @@ -354,8 +348,8 @@ private function transition(object $subject, Transition $transition, Marking $ma $event = new TransitionEvent($subject, $marking, $transition, $this, $context); $this->dispatcher->dispatch($event, WorkflowEvents::TRANSITION); - $this->dispatcher->dispatch($event, sprintf('workflow.%s.transition', $this->name)); - $this->dispatcher->dispatch($event, sprintf('workflow.%s.transition.%s', $this->name, $transition->getName())); + $this->dispatcher->dispatch($event, \sprintf('workflow.%s.transition', $this->name)); + $this->dispatcher->dispatch($event, \sprintf('workflow.%s.transition.%s', $this->name, $transition->getName())); return $event->getContext(); } @@ -368,10 +362,10 @@ private function enter(object $subject, Transition $transition, Marking $marking $event = new EnterEvent($subject, $marking, $transition, $this, $context); $this->dispatcher->dispatch($event, WorkflowEvents::ENTER); - $this->dispatcher->dispatch($event, sprintf('workflow.%s.enter', $this->name)); + $this->dispatcher->dispatch($event, \sprintf('workflow.%s.enter', $this->name)); foreach ($places as $place) { - $this->dispatcher->dispatch($event, sprintf('workflow.%s.enter.%s', $this->name, $place)); + $this->dispatcher->dispatch($event, \sprintf('workflow.%s.enter.%s', $this->name, $place)); } } @@ -389,10 +383,16 @@ private function entered(object $subject, ?Transition $transition, Marking $mark $event = new EnteredEvent($subject, $marking, $transition, $this, $context); $this->dispatcher->dispatch($event, WorkflowEvents::ENTERED); - $this->dispatcher->dispatch($event, sprintf('workflow.%s.entered', $this->name)); + $this->dispatcher->dispatch($event, \sprintf('workflow.%s.entered', $this->name)); - foreach ($marking->getPlaces() as $placeName => $nbToken) { - $this->dispatcher->dispatch($event, sprintf('workflow.%s.entered.%s', $this->name, $placeName)); + $placeNames = []; + if ($transition) { + $placeNames = $transition->getTos(); + } elseif ($this->definition->getInitialPlaces()) { + $placeNames = $this->definition->getInitialPlaces(); + } + foreach ($placeNames as $placeName) { + $this->dispatcher->dispatch($event, \sprintf('workflow.%s.entered.%s', $this->name, $placeName)); } } @@ -405,8 +405,8 @@ private function completed(object $subject, Transition $transition, Marking $mar $event = new CompletedEvent($subject, $marking, $transition, $this, $context); $this->dispatcher->dispatch($event, WorkflowEvents::COMPLETED); - $this->dispatcher->dispatch($event, sprintf('workflow.%s.completed', $this->name)); - $this->dispatcher->dispatch($event, sprintf('workflow.%s.completed.%s', $this->name, $transition->getName())); + $this->dispatcher->dispatch($event, \sprintf('workflow.%s.completed', $this->name)); + $this->dispatcher->dispatch($event, \sprintf('workflow.%s.completed.%s', $this->name, $transition->getName())); } private function announce(object $subject, Transition $initialTransition, Marking $marking, array $context): void @@ -418,10 +418,10 @@ private function announce(object $subject, Transition $initialTransition, Markin $event = new AnnounceEvent($subject, $marking, $initialTransition, $this, $context); $this->dispatcher->dispatch($event, WorkflowEvents::ANNOUNCE); - $this->dispatcher->dispatch($event, sprintf('workflow.%s.announce', $this->name)); + $this->dispatcher->dispatch($event, \sprintf('workflow.%s.announce', $this->name)); foreach ($this->getEnabledTransitions($subject) as $transition) { - $this->dispatcher->dispatch($event, sprintf('workflow.%s.announce.%s', $this->name, $transition->getName())); + $this->dispatcher->dispatch($event, \sprintf('workflow.%s.announce.%s', $this->name, $transition->getName())); } } diff --git a/WorkflowInterface.php b/WorkflowInterface.php index 8e0faef..6f5bff2 100644 --- a/WorkflowInterface.php +++ b/WorkflowInterface.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Workflow; use Symfony\Component\Workflow\Exception\LogicException; +use Symfony\Component\Workflow\Exception\UndefinedTransitionException; use Symfony\Component\Workflow\MarkingStore\MarkingStoreInterface; use Symfony\Component\Workflow\Metadata\MetadataStoreInterface; @@ -38,6 +39,8 @@ public function can(object $subject, string $transitionName): bool; /** * Builds a TransitionBlockerList to know why a transition is blocked. + * + * @throws UndefinedTransitionException If the transition is not defined */ public function buildTransitionBlockerList(object $subject, string $transitionName): TransitionBlockerList; diff --git a/composer.json b/composer.json index 44a3000..3e2c50a 100644 --- a/composer.json +++ b/composer.json @@ -20,15 +20,17 @@ } ], "require": { - "php": ">=8.2" + "php": ">=8.2", + "symfony/deprecation-contracts": "2.5|^3" }, "require-dev": { "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0", "symfony/dependency-injection": "^6.4|^7.0", - "symfony/event-dispatcher": "^6.4|^7.0", "symfony/error-handler": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0", "symfony/expression-language": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", "symfony/security-core": "^6.4|^7.0", "symfony/stopwatch": "^6.4|^7.0", "symfony/validator": "^6.4|^7.0"