diff --git a/Attribute/AsAnnounceListener.php b/Attribute/AsAnnounceListener.php index 12a1a1a..8afa4ca 100644 --- a/Attribute/AsAnnounceListener.php +++ b/Attribute/AsAnnounceListener.php @@ -14,6 +14,8 @@ use Symfony\Component\EventDispatcher\Attribute\AsEventListener; /** + * Defines a listener for the "announce" event of a workflow. + * * @author Grégoire Pineau */ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] @@ -21,6 +23,13 @@ final class AsAnnounceListener extends AsEventListener { use BuildEventNameTrait; + /** + * @param string|null $workflow The id of the workflow to listen to + * @param string|null $transition The transition name to which the listener listens to + * @param string|null $method The method to run when the listened event is triggered + * @param int $priority The priority of this listener if several are declared for the same transition + * @param string|null $dispatcher The service id of the event dispatcher to listen to + */ public function __construct( ?string $workflow = null, ?string $transition = null, diff --git a/Attribute/AsCompletedListener.php b/Attribute/AsCompletedListener.php index ac55f80..82bfe9d 100644 --- a/Attribute/AsCompletedListener.php +++ b/Attribute/AsCompletedListener.php @@ -14,6 +14,8 @@ use Symfony\Component\EventDispatcher\Attribute\AsEventListener; /** + * Defines a listener for the "completed" event of a workflow. + * * @author Grégoire Pineau */ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] @@ -21,6 +23,13 @@ final class AsCompletedListener extends AsEventListener { use BuildEventNameTrait; + /** + * @param string|null $workflow The id of the workflow to listen to + * @param string|null $transition The transition name to which the listener listens to + * @param string|null $method The method to run when the listened event is triggered + * @param int $priority The priority of this listener if several are declared for the same transition + * @param string|null $dispatcher The service id of the event dispatcher to listen to + */ public function __construct( ?string $workflow = null, ?string $transition = null, diff --git a/Attribute/AsEnterListener.php b/Attribute/AsEnterListener.php index bc4c93c..97e7917 100644 --- a/Attribute/AsEnterListener.php +++ b/Attribute/AsEnterListener.php @@ -14,6 +14,8 @@ use Symfony\Component\EventDispatcher\Attribute\AsEventListener; /** + * Defines a listener for the "enter" event of a workflow. + * * @author Grégoire Pineau */ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] @@ -21,6 +23,13 @@ final class AsEnterListener extends AsEventListener { use BuildEventNameTrait; + /** + * @param string|null $workflow The id of the workflow to listen to + * @param string|null $place The place name to which the listener listens to + * @param string|null $method The method to run when the listened event is triggered + * @param int $priority The priority of this listener if several are declared for the same place + * @param string|null $dispatcher The service id of the event dispatcher to listen to + */ public function __construct( ?string $workflow = null, ?string $place = null, diff --git a/Attribute/AsEnteredListener.php b/Attribute/AsEnteredListener.php index 7486a97..0824628 100644 --- a/Attribute/AsEnteredListener.php +++ b/Attribute/AsEnteredListener.php @@ -14,6 +14,8 @@ use Symfony\Component\EventDispatcher\Attribute\AsEventListener; /** + * Defines a listener for the "entered" event of a workflow. + * * @author Grégoire Pineau */ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] @@ -21,6 +23,13 @@ final class AsEnteredListener extends AsEventListener { use BuildEventNameTrait; + /** + * @param string|null $workflow The id of the workflow to listen to + * @param string|null $place The place name to which the listener listens to + * @param string|null $method The method to run when the listened event is triggered + * @param int $priority The priority of this listener if several are declared for the same place + * @param string|null $dispatcher The service id of the event dispatcher to listen to + */ public function __construct( ?string $workflow = null, ?string $place = null, diff --git a/Attribute/AsGuardListener.php b/Attribute/AsGuardListener.php index e0105a5..e2e783f 100644 --- a/Attribute/AsGuardListener.php +++ b/Attribute/AsGuardListener.php @@ -14,6 +14,8 @@ use Symfony\Component\EventDispatcher\Attribute\AsEventListener; /** + * Defines a listener for a guard event of a workflow. + * * @author Grégoire Pineau */ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] @@ -21,6 +23,13 @@ final class AsGuardListener extends AsEventListener { use BuildEventNameTrait; + /** + * @param string|null $workflow The id of the workflow to listen to + * @param string|null $transition The transition name to which the listener listens to + * @param string|null $method The method to run when the listened event is triggered + * @param int $priority The priority of this listener if several are declared for the same transition + * @param string|null $dispatcher The service id of the event dispatcher to listen to + */ public function __construct( ?string $workflow = null, ?string $transition = null, diff --git a/Attribute/AsLeaveListener.php b/Attribute/AsLeaveListener.php index 7dfe8f8..3ef6b4d 100644 --- a/Attribute/AsLeaveListener.php +++ b/Attribute/AsLeaveListener.php @@ -14,6 +14,8 @@ use Symfony\Component\EventDispatcher\Attribute\AsEventListener; /** + * Defines a listener for the "leave" event of a workflow. + * * @author Grégoire Pineau */ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] @@ -21,6 +23,13 @@ final class AsLeaveListener extends AsEventListener { use BuildEventNameTrait; + /** + * @param string|null $workflow The id of the workflow to listen to + * @param string|null $place The place name to which the listener listens to + * @param string|null $method The method to run when the listened event is triggered + * @param int $priority The priority of this listener if several are declared for the same place + * @param string|null $dispatcher The service id of the event dispatcher to listen to + */ public function __construct( ?string $workflow = null, ?string $place = null, diff --git a/Attribute/AsTransitionListener.php b/Attribute/AsTransitionListener.php index 46169f0..dc49749 100644 --- a/Attribute/AsTransitionListener.php +++ b/Attribute/AsTransitionListener.php @@ -14,6 +14,8 @@ use Symfony\Component\EventDispatcher\Attribute\AsEventListener; /** + * Defines a listener for a transition event of a workflow. + * * @author Grégoire Pineau */ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] @@ -21,6 +23,13 @@ final class AsTransitionListener extends AsEventListener { use BuildEventNameTrait; + /** + * @param string|null $workflow The id of the workflow to listen to + * @param string|null $transition The transition name to which the listener listens to + * @param string|null $method The method to run when the listened event is triggered + * @param int $priority The priority of this listener if several are declared for the same transition + * @param string|null $dispatcher The service id of the event dispatcher to listen to + */ public function __construct( ?string $workflow = null, ?string $transition = null, 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 009bb3e..5a37ead 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,26 @@ 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 + +7.0 +--- + + * Require explicit argument when calling `Definition::setInitialPlaces()` + * `GuardEvent::getContext()` method has been removed. Method was not supposed to be called within guard event listeners as it always returned an empty array anyway. + * Remove `GuardEvent::getContext()` method without replacement + 6.4 --- diff --git a/DataCollector/WorkflowDataCollector.php b/DataCollector/WorkflowDataCollector.php index cf15802..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) { @@ -171,9 +182,9 @@ private function summarizeListener(callable $callable, ?string $eventName = null if ($callable instanceof \Closure) { $r = new \ReflectionFunction($callable); - if (str_contains($r->name, '{closure')) { + if ($r->isAnonymous()) { $title = (string) $r; - } elseif ($class = \PHP_VERSION_ID >= 80111 ? $r->getClosureCalledClass() : $r->getClosureScopeClass()) { + } elseif ($class = $r->getClosureCalledClass()) { $title = $class->name.'::'.$r->name.'()'; } else { $title = $r->name; 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 e876b9f..0b5697b 100644 --- a/Definition.php +++ b/Definition.php @@ -76,11 +76,8 @@ public function getMetadataStore(): MetadataStoreInterface return $this->metadataStore; } - private function setInitialPlaces(string|array|null $places = null): void + private function setInitialPlaces(string|array|null $places): void { - if (1 > \func_num_args()) { - trigger_deprecation('symfony/workflow', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); - } if (!$places) { return; } @@ -89,7 +86,7 @@ private function setInitialPlaces(string|array|null $places = null): 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)); } } @@ -107,17 +104,15 @@ private function addPlace(string $place): void private function addTransition(Transition $transition): void { - $name = $transition->getName(); - foreach ($transition->getFroms() as $from) { - if (!isset($this->places[$from])) { - throw new LogicException(sprintf('Place "%s" referenced in transition "%s" does not exist.', $from, $name)); + if (!\array_key_exists($from, $this->places)) { + $this->addPlace($from); } } foreach ($transition->getTos() as $to) { - if (!isset($this->places[$to])) { - throw new LogicException(sprintf('Place "%s" referenced in transition "%s" does not exist.', $to, $name)); + if (!\array_key_exists($to, $this->places)) { + $this->addPlace($to); } } 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 9a99690..ad7b0c2 100644 --- a/Dumper/GraphvizDumper.php +++ b/Dumper/GraphvizDumper.php @@ -27,7 +27,7 @@ class GraphvizDumper implements DumperInterface { // All values should be strings - protected static $defaultOptions = [ + protected static array $defaultOptions = [ 'graph' => ['ratio' => 'compress', 'rankdir' => 'LR'], 'node' => ['fontsize' => '9', 'fontname' => 'Arial', 'color' => '#333333', 'fillcolor' => 'lightblue', 'fixedsize' => 'false', 'width' => '1'], 'edge' => ['fontsize' => '9', 'fontname' => 'Arial', 'color' => '#333333', 'arrowhead' => 'normal', 'arrowsize' => '0.5'], @@ -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 d2f2d6e..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 @@ -72,7 +68,7 @@ public function dump(Definition $definition, ?Marking $marking = null, array $op $placeId, $place, $meta->getPlaceMetadata($place), - \in_array($place, $definition->getInitialPlaces()), + \in_array($place, $definition->getInitialPlaces(), true), $marking?->has($place) ?? false ); @@ -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/AnnounceEvent.php b/Event/AnnounceEvent.php index 7d3d740..0bff3dc 100644 --- a/Event/AnnounceEvent.php +++ b/Event/AnnounceEvent.php @@ -11,6 +11,21 @@ namespace Symfony\Component\Workflow\Event; +use Symfony\Component\Workflow\Marking; +use Symfony\Component\Workflow\Transition; +use Symfony\Component\Workflow\WorkflowInterface; + final class AnnounceEvent extends Event { + use EventNameTrait { + getNameForTransition as public getName; + } + use HasContextTrait; + + public function __construct(object $subject, Marking $marking, ?Transition $transition = null, ?WorkflowInterface $workflow = null, array $context = []) + { + parent::__construct($subject, $marking, $transition, $workflow); + + $this->context = $context; + } } diff --git a/Event/CompletedEvent.php b/Event/CompletedEvent.php index 883390e..885826f 100644 --- a/Event/CompletedEvent.php +++ b/Event/CompletedEvent.php @@ -11,6 +11,21 @@ namespace Symfony\Component\Workflow\Event; +use Symfony\Component\Workflow\Marking; +use Symfony\Component\Workflow\Transition; +use Symfony\Component\Workflow\WorkflowInterface; + final class CompletedEvent extends Event { + use EventNameTrait { + getNameForTransition as public getName; + } + use HasContextTrait; + + public function __construct(object $subject, Marking $marking, ?Transition $transition = null, ?WorkflowInterface $workflow = null, array $context = []) + { + parent::__construct($subject, $marking, $transition, $workflow); + + $this->context = $context; + } } diff --git a/Event/EnterEvent.php b/Event/EnterEvent.php index 3296f29..46e1041 100644 --- a/Event/EnterEvent.php +++ b/Event/EnterEvent.php @@ -11,6 +11,21 @@ namespace Symfony\Component\Workflow\Event; +use Symfony\Component\Workflow\Marking; +use Symfony\Component\Workflow\Transition; +use Symfony\Component\Workflow\WorkflowInterface; + final class EnterEvent extends Event { + use EventNameTrait { + getNameForPlace as public getName; + } + use HasContextTrait; + + public function __construct(object $subject, Marking $marking, ?Transition $transition = null, ?WorkflowInterface $workflow = null, array $context = []) + { + parent::__construct($subject, $marking, $transition, $workflow); + + $this->context = $context; + } } diff --git a/Event/EnteredEvent.php b/Event/EnteredEvent.php index ea3624b..a71610d 100644 --- a/Event/EnteredEvent.php +++ b/Event/EnteredEvent.php @@ -11,6 +11,21 @@ namespace Symfony\Component\Workflow\Event; +use Symfony\Component\Workflow\Marking; +use Symfony\Component\Workflow\Transition; +use Symfony\Component\Workflow\WorkflowInterface; + final class EnteredEvent extends Event { + use EventNameTrait { + getNameForPlace as public getName; + } + use HasContextTrait; + + public function __construct(object $subject, Marking $marking, ?Transition $transition = null, ?WorkflowInterface $workflow = null, array $context = []) + { + parent::__construct($subject, $marking, $transition, $workflow); + + $this->context = $context; + } } diff --git a/Event/Event.php b/Event/Event.php index 1b9f5b7..c13818b 100644 --- a/Event/Event.php +++ b/Event/Event.php @@ -23,68 +23,46 @@ */ class Event extends BaseEvent { - protected $context; - 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, array $context = []) - { - $this->subject = $subject; - $this->marking = $marking; - $this->transition = $transition; - $this->workflow = $workflow; - $this->context = $context; + public function __construct( + private object $subject, + private Marking $marking, + private ?Transition $transition = null, + private ?WorkflowInterface $workflow = null, + ) { } - /** - * @return Marking - */ - public function getMarking() + public function getMarking(): Marking { return $this->marking; } - /** - * @return object - */ - public function getSubject() + public function getSubject(): object { return $this->subject; } - /** - * @return Transition|null - */ - public function getTransition() + 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; } - /** - * @return string - */ - public function getWorkflowName() + public function getWorkflowName(): string { return $this->workflow->getName(); } - /** - * @return mixed - */ - public function getMetadata(string $key, string|Transition|null $subject) + public function getMetadata(string $key, string|Transition|null $subject): mixed { return $this->workflow->getMetadataStore()->getMetadata($key, $subject); } - - public function getContext(): array - { - return $this->context; - } } diff --git a/Event/EventNameTrait.php b/Event/EventNameTrait.php new file mode 100644 index 0000000..1f77b37 --- /dev/null +++ b/Event/EventNameTrait.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Workflow\Event; + +use Symfony\Component\Workflow\Exception\InvalidArgumentException; + +/** + * @author Nicolas Rigaud + * + * @internal + */ +trait EventNameTrait +{ + /** + * Gets the event name for workflow and transition. + * + * @throws InvalidArgumentException If $transitionName is provided without $workflowName + */ + private static function getNameForTransition(?string $workflowName, ?string $transitionName): string + { + return self::computeName($workflowName, $transitionName); + } + + /** + * Gets the event name for workflow and place. + * + * @throws InvalidArgumentException If $placeName is provided without $workflowName + */ + private static function getNameForPlace(?string $workflowName, ?string $placeName): string + { + return self::computeName($workflowName, $placeName); + } + + private static function computeName(?string $workflowName, ?string $transitionOrPlaceName): string + { + $eventName = strtolower(basename(str_replace('\\', '/', static::class), 'Event')); + + if (null === $workflowName) { + if (null !== $transitionOrPlaceName) { + throw new \InvalidArgumentException('Missing workflow name.'); + } + + return \sprintf('workflow.%s', $eventName); + } + + if (null === $transitionOrPlaceName) { + return \sprintf('workflow.%s.%s', $workflowName, $eventName); + } + + return \sprintf('workflow.%s.%s.%s', $workflowName, $eventName, $transitionOrPlaceName); + } +} diff --git a/Event/GuardEvent.php b/Event/GuardEvent.php index 10ff17d..fbbcf22 100644 --- a/Event/GuardEvent.php +++ b/Event/GuardEvent.php @@ -23,6 +23,10 @@ */ final class GuardEvent extends Event { + use EventNameTrait { + getNameForTransition as public getName; + } + private TransitionBlockerList $transitionBlockerList; public function __construct(object $subject, Marking $marking, Transition $transition, ?WorkflowInterface $workflow = null) @@ -32,13 +36,6 @@ public function __construct(object $subject, Marking $marking, Transition $trans $this->transitionBlockerList = new TransitionBlockerList(); } - public function getContext(): array - { - trigger_deprecation('symfony/workflow', '6.4', 'The %s::getContext() method is deprecated and will be removed in 7.0. You should no longer call this method as it always returns an empty array when invoked within a guard listener.', __CLASS__); - - return parent::getContext(); - } - public function getTransition(): Transition { return parent::getTransition(); diff --git a/Event/HasContextTrait.php b/Event/HasContextTrait.php new file mode 100644 index 0000000..4fc3d87 --- /dev/null +++ b/Event/HasContextTrait.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Workflow\Event; + +/** + * @author Fabien Potencier + * @author Grégoire Pineau + * @author Hugo Hamon + * + * @internal + */ +trait HasContextTrait +{ + private array $context = []; + + public function getContext(): array + { + return $this->context; + } +} diff --git a/Event/LeaveEvent.php b/Event/LeaveEvent.php index d3d48cb..78fd1b6 100644 --- a/Event/LeaveEvent.php +++ b/Event/LeaveEvent.php @@ -11,6 +11,21 @@ namespace Symfony\Component\Workflow\Event; +use Symfony\Component\Workflow\Marking; +use Symfony\Component\Workflow\Transition; +use Symfony\Component\Workflow\WorkflowInterface; + final class LeaveEvent extends Event { + use EventNameTrait { + getNameForPlace as public getName; + } + use HasContextTrait; + + public function __construct(object $subject, Marking $marking, ?Transition $transition = null, ?WorkflowInterface $workflow = null, array $context = []) + { + parent::__construct($subject, $marking, $transition, $workflow); + + $this->context = $context; + } } diff --git a/Event/TransitionEvent.php b/Event/TransitionEvent.php index 4710f90..a7a3dd0 100644 --- a/Event/TransitionEvent.php +++ b/Event/TransitionEvent.php @@ -11,8 +11,24 @@ namespace Symfony\Component\Workflow\Event; +use Symfony\Component\Workflow\Marking; +use Symfony\Component\Workflow\Transition; +use Symfony\Component\Workflow\WorkflowInterface; + final class TransitionEvent extends Event { + use EventNameTrait { + getNameForTransition as public getName; + } + use HasContextTrait; + + public function __construct(object $subject, Marking $marking, ?Transition $transition = null, ?WorkflowInterface $workflow = null, array $context = []) + { + parent::__construct($subject, $marking, $transition, $workflow); + + $this->context = $context; + } + public function setContext(array $context): void { $this->context = $context; diff --git a/EventListener/AuditTrailListener.php b/EventListener/AuditTrailListener.php index 8d82824..fe7ccdf 100644 --- a/EventListener/AuditTrailListener.php +++ b/EventListener/AuditTrailListener.php @@ -20,38 +20,27 @@ */ class AuditTrailListener implements EventSubscriberInterface { - private LoggerInterface $logger; - - public function __construct(LoggerInterface $logger) - { - $this->logger = $logger; + public function __construct( + private LoggerInterface $logger, + ) { } - /** - * @return void - */ - public function onLeave(Event $event) + 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())); } } - /** - * @return void - */ - public function onTransition(Event $event) + 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())); } - /** - * @return void - */ - public function onEnter(Event $event) + 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 82fe165..257f885 100644 --- a/EventListener/ExpressionLanguage.php +++ b/EventListener/ExpressionLanguage.php @@ -22,16 +22,13 @@ */ class ExpressionLanguage extends BaseExpressionLanguage { - /** - * @return void - */ - protected function registerFunctions() + 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 23e830c..deb148d 100644 --- a/EventListener/GuardExpression.php +++ b/EventListener/GuardExpression.php @@ -15,27 +15,18 @@ 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, + ) { } - /** - * @return Transition - */ - public function getTransition() + public function getTransition(): Transition { return $this->transition; } - /** - * @return string - */ - public function getExpression() + public function getExpression(): string { return $this->expression; } diff --git a/EventListener/GuardListener.php b/EventListener/GuardListener.php index 5f58837..23cdd7a 100644 --- a/EventListener/GuardListener.php +++ b/EventListener/GuardListener.php @@ -24,29 +24,18 @@ */ 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, + ) { } - /** - * @return void - */ - public function onTransition(GuardEvent $event, string $eventName) + public function onTransition(GuardEvent $event, string $eventName): void { if (!isset($this->configuration[$eventName])) { return; 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 890d8e2..e5c3846 100644 --- a/Exception/TransitionException.php +++ b/Exception/TransitionException.php @@ -19,25 +19,17 @@ */ 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; } - /** - * @return object - */ - public function getSubject() + public function getSubject(): object { return $this->subject; } 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 95a83f0..c3629a2 100644 --- a/Marking.php +++ b/Marking.php @@ -22,43 +22,68 @@ class Marking private ?array $context = null; /** - * @param int[] $representation Keys are the place name and values should be 1 + * @param int[] $representation Keys are the place name and values should be superior or equals to 1 */ public function __construct(array $representation = []) { foreach ($representation as $place => $nbToken) { - $this->mark($place); + $this->mark($place, $nbToken); } } /** - * @return void + * @param int $nbToken + * + * @psalm-param int<1, max> $nbToken */ - public function mark(string $place) + public function mark(string $place /* , int $nbToken = 1 */): void { - $this->places[$place] = 1; + $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)); + } + + $this->places[$place] ??= 0; + $this->places[$place] += $nbToken; } /** - * @return void + * @param int $nbToken + * + * @psalm-param int<1, max> $nbToken */ - public function unmark(string $place) + public function unmark(string $place /* , int $nbToken = 1 */): void { - unset($this->places[$place]); + $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)); + } + + if (!$this->has($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)); + } + + if (0 === $tokenCount) { + unset($this->places[$place]); + } else { + $this->places[$place] = $tokenCount; + } } - /** - * @return bool - */ - public function has(string $place) + public function has(string $place): bool { return isset($this->places[$place]); } - /** - * @return array - */ - public function getPlaces() + public function getPlaces(): array { return $this->places; } diff --git a/MarkingStore/MarkingStoreInterface.php b/MarkingStore/MarkingStoreInterface.php index 7547a7f..43b34f5 100644 --- a/MarkingStore/MarkingStoreInterface.php +++ b/MarkingStore/MarkingStoreInterface.php @@ -31,8 +31,6 @@ public function getMarking(object $subject): Marking; /** * Sets a Marking to a subject. - * - * @return void */ - public function setMarking(object $subject, Marking $marking, array $context = []); + public function setMarking(object $subject, Marking $marking, array $context = []): void; } 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/GetMetadataTrait.php b/Metadata/GetMetadataTrait.php index fd53ad8..83b57f7 100644 --- a/Metadata/GetMetadataTrait.php +++ b/Metadata/GetMetadataTrait.php @@ -18,10 +18,7 @@ */ trait GetMetadataTrait { - /** - * @return mixed - */ - public function getMetadata(string $key, string|Transition|null $subject = null) + public function getMetadata(string $key, string|Transition|null $subject = null): mixed { if (null === $subject) { return $this->getWorkflowMetadata()[$key] ?? null; 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/Metadata/MetadataStoreInterface.php b/Metadata/MetadataStoreInterface.php index c208b4d..e8f6b21 100644 --- a/Metadata/MetadataStoreInterface.php +++ b/Metadata/MetadataStoreInterface.php @@ -34,8 +34,6 @@ public function getTransitionMetadata(Transition $transition): array; * @param string|Transition|null $subject Use null to get workflow metadata * Use a string (the place name) to get place metadata * Use a Transition instance to get transition metadata - * - * @return mixed */ - public function getMetadata(string $key, string|Transition|null $subject = null); + public function getMetadata(string $key, string|Transition|null $subject = null): mixed; } diff --git a/Registry.php b/Registry.php index 8041e98..08017a3 100644 --- a/Registry.php +++ b/Registry.php @@ -22,10 +22,7 @@ class Registry { private array $workflows = []; - /** - * @return void - */ - public function addWorkflow(WorkflowInterface $workflow, WorkflowSupportStrategyInterface $supportStrategy) + public function addWorkflow(WorkflowInterface $workflow, WorkflowSupportStrategyInterface $supportStrategy): void { $this->workflows[] = [$workflow, $supportStrategy]; } @@ -52,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 5bfcee9..257ad66 100644 --- a/Tests/Debug/TraceableWorkflowTest.php +++ b/Tests/Debug/TraceableWorkflowTest.php @@ -21,9 +21,9 @@ class TraceableWorkflowTest extends TestCase { - private MockObject|Workflow $innerWorkflow; + private MockObject&Workflow $innerWorkflow; - private StopWatch $stopwatch; + private Stopwatch $stopwatch; private TraceableWorkflow $traceableWorkflow; diff --git a/Tests/DefinitionTest.php b/Tests/DefinitionTest.php index 9e9c783..4303dee 100644 --- a/Tests/DefinitionTest.php +++ b/Tests/DefinitionTest.php @@ -64,19 +64,17 @@ public function testAddTransition() public function testAddTransitionAndFromPlaceIsNotDefined() { - $this->expectException(LogicException::class); - $this->expectExceptionMessage('Place "c" referenced in transition "name" does not exist.'); $places = range('a', 'b'); - new Definition($places, [new Transition('name', 'c', $places[1])]); + $definition = new Definition($places, [new Transition('name', 'c', $places[1])]); + $this->assertContains('c', $definition->getPlaces()); } public function testAddTransitionAndToPlaceIsNotDefined() { - $this->expectException(LogicException::class); - $this->expectExceptionMessage('Place "c" referenced in transition "name" does not exist.'); $places = range('a', 'b'); - new Definition($places, [new Transition('name', $places[0], 'c')]); + $definition = new Definition($places, [new Transition('name', $places[0], 'c')]); + $this->assertContains('c', $definition->getPlaces()); } } 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/Event/EventNameTraitTest.php b/Tests/Event/EventNameTraitTest.php new file mode 100644 index 0000000..3c74523 --- /dev/null +++ b/Tests/Event/EventNameTraitTest.php @@ -0,0 +1,73 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Workflow\Tests\Event; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Workflow\Event\AnnounceEvent; +use Symfony\Component\Workflow\Event\CompletedEvent; +use Symfony\Component\Workflow\Event\EnteredEvent; +use Symfony\Component\Workflow\Event\EnterEvent; +use Symfony\Component\Workflow\Event\GuardEvent; +use Symfony\Component\Workflow\Event\LeaveEvent; +use Symfony\Component\Workflow\Event\TransitionEvent; + +class EventNameTraitTest extends TestCase +{ + /** + * @dataProvider getEvents + * + * @param class-string $class + */ + public function testEventNames(string $class, ?string $workflowName, ?string $transitionOrPlaceName, string $expected) + { + $name = $class::getName($workflowName, $transitionOrPlaceName); + $this->assertEquals($expected, $name); + } + + public static function getEvents(): iterable + { + yield [AnnounceEvent::class, null, null, 'workflow.announce']; + yield [AnnounceEvent::class, 'post', null, 'workflow.post.announce']; + yield [AnnounceEvent::class, 'post', 'publish', 'workflow.post.announce.publish']; + + yield [CompletedEvent::class, null, null, 'workflow.completed']; + yield [CompletedEvent::class, 'post', null, 'workflow.post.completed']; + yield [CompletedEvent::class, 'post', 'publish', 'workflow.post.completed.publish']; + + yield [EnteredEvent::class, null, null, 'workflow.entered']; + yield [EnteredEvent::class, 'post', null, 'workflow.post.entered']; + yield [EnteredEvent::class, 'post', 'published', 'workflow.post.entered.published']; + + yield [EnterEvent::class, null, null, 'workflow.enter']; + yield [EnterEvent::class, 'post', null, 'workflow.post.enter']; + yield [EnterEvent::class, 'post', 'published', 'workflow.post.enter.published']; + + yield [GuardEvent::class, null, null, 'workflow.guard']; + yield [GuardEvent::class, 'post', null, 'workflow.post.guard']; + yield [GuardEvent::class, 'post', 'publish', 'workflow.post.guard.publish']; + + yield [LeaveEvent::class, null, null, 'workflow.leave']; + yield [LeaveEvent::class, 'post', null, 'workflow.post.leave']; + yield [LeaveEvent::class, 'post', 'published', 'workflow.post.leave.published']; + + yield [TransitionEvent::class, null, null, 'workflow.transition']; + yield [TransitionEvent::class, 'post', null, 'workflow.post.transition']; + yield [TransitionEvent::class, 'post', 'publish', 'workflow.post.transition.publish']; + } + + public function testInvalidArgumentExceptionIsThrownIfWorkflowNameIsMissing() + { + $this->expectException(\InvalidArgumentException::class); + + EnterEvent::getName(null, 'place'); + } +} diff --git a/Tests/MarkingTest.php b/Tests/MarkingTest.php index 0a1c22b..86a306a 100644 --- a/Tests/MarkingTest.php +++ b/Tests/MarkingTest.php @@ -22,24 +22,70 @@ public function testMarking() $this->assertTrue($marking->has('a')); $this->assertFalse($marking->has('b')); - $this->assertSame(['a' => 1], $marking->getPlaces()); + $this->assertPlaces(['a' => 1], $marking); $marking->mark('b'); $this->assertTrue($marking->has('a')); $this->assertTrue($marking->has('b')); - $this->assertSame(['a' => 1, 'b' => 1], $marking->getPlaces()); + $this->assertPlaces(['a' => 1, 'b' => 1], $marking); $marking->unmark('a'); $this->assertFalse($marking->has('a')); $this->assertTrue($marking->has('b')); - $this->assertSame(['b' => 1], $marking->getPlaces()); + $this->assertPlaces(['b' => 1], $marking); $marking->unmark('b'); $this->assertFalse($marking->has('a')); $this->assertFalse($marking->has('b')); - $this->assertSame([], $marking->getPlaces()); + $this->assertPlaces([], $marking); + + $marking->mark('a'); + $this->assertPlaces(['a' => 1], $marking); + + $marking->mark('a'); + $this->assertPlaces(['a' => 2], $marking); + + $marking->unmark('a'); + $this->assertPlaces(['a' => 1], $marking); + + $marking->unmark('a'); + $this->assertPlaces([], $marking); + } + + public function testGuardNotMarked() + { + $marking = new Marking([]); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The place "a" is not marked.'); + $marking->unmark('a'); + } + + public function testUnmarkGuardResultTokenCountIsNotNegative() + { + $marking = new Marking(['a' => 1]); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The place "a" could not contain a negative token number: "1" (initial) - "2" (nbToken) = "-1".'); + $marking->unmark('a', 2); + } + + public function testUnmarkGuardNbTokenIsGreaterThanZero() + { + $marking = new Marking(['a' => 1]); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The number of tokens must be greater than 0, "0" given.'); + $marking->unmark('a', 0); + } + + private function assertPlaces(array $expected, Marking $marking) + { + $places = $marking->getPlaces(); + ksort($places); + $this->assertSame($expected, $places); } } diff --git a/Tests/RegistryTest.php b/Tests/RegistryTest.php index f9a8fe0..d3282a8 100644 --- a/Tests/RegistryTest.php +++ b/Tests/RegistryTest.php @@ -63,18 +63,14 @@ public function testGetWithMultipleMatch() { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Too many workflows (workflow2, workflow3) match this subject (Symfony\Component\Workflow\Tests\Subject2); set a different name on each and use the second (name) argument of this method.'); - $w1 = $this->registry->get(new Subject2()); - $this->assertInstanceOf(Workflow::class, $w1); - $this->assertSame('workflow1', $w1->getName()); + $this->registry->get(new Subject2()); } public function testGetWithNoMatch() { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Unable to find a workflow for class "stdClass".'); - $w1 = $this->registry->get(new \stdClass()); - $this->assertInstanceOf(Workflow::class, $w1); - $this->assertSame('workflow1', $w1->getName()); + $this->registry->get(new \stdClass()); } public function testAllWithOneMatchWithSuccess() 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 036ece7..50c3abd 100644 --- a/Tests/Validator/WorkflowValidatorTest.php +++ b/Tests/Validator/WorkflowValidatorTest.php @@ -24,8 +24,6 @@ class WorkflowValidatorTest extends TestCase public function testWorkflowWithInvalidNames() { - $this->expectException(InvalidDefinitionException::class); - $this->expectExceptionMessage('All transitions for a place must have an unique name. Multiple transitions named "t1" where found for place "a" in workflow "foo".'); $places = range('a', 'c'); $transitions = []; @@ -35,6 +33,9 @@ public function testWorkflowWithInvalidNames() $definition = new Definition($places, $transitions); + $this->expectException(InvalidDefinitionException::class); + $this->expectExceptionMessage('All transitions for a place must have an unique name. Multiple transitions named "t1" where found for place "a" in workflow "foo".'); + (new WorkflowValidator())->validate($definition, 'foo'); } @@ -54,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/WorkflowBuilderTrait.php b/Tests/WorkflowBuilderTrait.php index 07a589e..86478bb 100644 --- a/Tests/WorkflowBuilderTrait.php +++ b/Tests/WorkflowBuilderTrait.php @@ -158,4 +158,43 @@ private static function createComplexStateMachineDefinition(): Definition // | d | -------------+ // +-----+ } + + private static function createWorkflowWithSameNameBackTransition(): Definition + { + $places = range('a', 'c'); + + $transitions = []; + $transitions[] = new Transition('a_to_bc', 'a', ['b', 'c']); + $transitions[] = new Transition('back1', 'b', 'a'); + $transitions[] = new Transition('back1', 'c', 'b'); + $transitions[] = new Transition('back2', 'c', 'b'); + $transitions[] = new Transition('back2', 'b', 'a'); + $transitions[] = new Transition('c_to_cb', 'c', ['b', 'c']); + + return new Definition($places, $transitions); + + // The graph looks like: + // +-----------------------------------------------------------------+ + // | | + // | | + // | +---------------------------------------------+ | + // v | v | + // +---+ +---------+ +-------+ +---------+ +---+ +-------+ + // | a | --> | a_to_bc | --> | | --> | back2 | --> | | --> | back2 | + // +---+ +---------+ | | +---------+ | | +-------+ + // ^ | | | | + // | | c | <-----+ | b | + // | | | | | | + // | | | +---------+ | | +-------+ + // | | | --> | c_to_cb | --> | | --> | back1 | + // | +-------+ +---------+ +---+ +-------+ + // | | ^ | + // | | | | + // | v | | + // | +-------+ | | + // | | back1 | ----------------------+ | + // | +-------+ | + // | | + // +-----------------------------------------------------------------+ + } } diff --git a/Tests/WorkflowTest.php b/Tests/WorkflowTest.php index 8e112df..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()); @@ -319,28 +320,32 @@ public function testApplyWithSameNameTransition() $marking = $workflow->apply($subject, 'a_to_bc'); - $this->assertFalse($marking->has('a')); - $this->assertTrue($marking->has('b')); - $this->assertTrue($marking->has('c')); + $this->assertPlaces([ + 'b' => 1, + 'c' => 1, + ], $marking); $marking = $workflow->apply($subject, 'to_a'); - $this->assertTrue($marking->has('a')); - $this->assertFalse($marking->has('b')); - $this->assertFalse($marking->has('c')); + // Two tokens in "a" + $this->assertPlaces([ + 'a' => 2, + ], $marking); $workflow->apply($subject, 'a_to_bc'); $marking = $workflow->apply($subject, 'b_to_c'); - $this->assertFalse($marking->has('a')); - $this->assertFalse($marking->has('b')); - $this->assertTrue($marking->has('c')); + $this->assertPlaces([ + 'a' => 1, + 'c' => 2, + ], $marking); $marking = $workflow->apply($subject, 'to_a'); - $this->assertTrue($marking->has('a')); - $this->assertFalse($marking->has('b')); - $this->assertFalse($marking->has('c')); + $this->assertPlaces([ + 'a' => 2, + 'c' => 1, + ], $marking); } public function testApplyWithSameNameTransition2() @@ -685,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'))]); @@ -730,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); @@ -776,6 +819,63 @@ public function testGetEnabledTransitionsWithSameNameTransition() $this->assertSame('to_a', $transitions[1]->getName()); $this->assertSame('to_a', $transitions[2]->getName()); } + + /** + * @@testWith ["back1"] + * ["back2"] + */ + public function testApplyWithSameNameBackTransition(string $transition) + { + $definition = $this->createWorkflowWithSameNameBackTransition(); + $workflow = new Workflow($definition, new MethodMarkingStore()); + + $subject = new Subject(); + + $marking = $workflow->apply($subject, 'a_to_bc'); + $this->assertPlaces([ + 'b' => 1, + 'c' => 1, + ], $marking); + + $marking = $workflow->apply($subject, $transition); + $this->assertPlaces([ + 'a' => 1, + 'b' => 1, + ], $marking); + + $marking = $workflow->apply($subject, $transition); + $this->assertPlaces([ + 'a' => 2, + ], $marking); + + $marking = $workflow->apply($subject, 'a_to_bc'); + $this->assertPlaces([ + 'a' => 1, + 'b' => 1, + 'c' => 1, + ], $marking); + + $marking = $workflow->apply($subject, 'c_to_cb'); + $this->assertPlaces([ + 'a' => 1, + 'b' => 2, + 'c' => 1, + ], $marking); + + $marking = $workflow->apply($subject, 'c_to_cb'); + $this->assertPlaces([ + 'a' => 1, + 'b' => 3, + 'c' => 1, + ], $marking); + } + + private function assertPlaces(array $expected, Marking $marking) + { + $places = $marking->getPlaces(); + ksort($places); + $this->assertSame($expected, $places); + } } class EventDispatcherMock implements \Symfony\Contracts\EventDispatcher\EventDispatcherInterface 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/DefinitionValidatorInterface.php b/Validator/DefinitionValidatorInterface.php index c9717b7..7944a05 100644 --- a/Validator/DefinitionValidatorInterface.php +++ b/Validator/DefinitionValidatorInterface.php @@ -21,9 +21,7 @@ interface DefinitionValidatorInterface { /** - * @return void - * * @throws InvalidDefinitionException on invalid definition */ - public function validate(Definition $definition, string $name); + public function validate(Definition $definition, string $name): void; } diff --git a/Validator/StateMachineValidator.php b/Validator/StateMachineValidator.php index 20afc8d..626a20e 100644 --- a/Validator/StateMachineValidator.php +++ b/Validator/StateMachineValidator.php @@ -19,28 +19,25 @@ */ class StateMachineValidator implements DefinitionValidatorInterface { - /** - * @return void - */ - public function validate(Definition $definition, string $name) + public function validate(Definition $definition, string $name): void { $transitionFromNames = []; 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; @@ -48,7 +45,7 @@ public function validate(Definition $definition, string $name) $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 c13c281..f4eb292 100644 --- a/Validator/WorkflowValidator.php +++ b/Validator/WorkflowValidator.php @@ -20,24 +20,19 @@ */ class WorkflowValidator implements DefinitionValidatorInterface { - private bool $singlePlace; - - public function __construct(bool $singlePlace = false) - { - $this->singlePlace = $singlePlace; + public function __construct( + private bool $singlePlace = false, + ) { } - /** - * @return void - */ - public function validate(Definition $definition, string $name) + public function validate(Definition $definition, string $name): void { // Make sure all transitions for one place has unique name. $places = array_fill_keys($definition->getPlaces(), []); foreach ($definition->getTransitions() as $transition) { foreach ($transition->getFroms() as $from) { - if (\in_array($transition->getName(), $places[$from])) { - 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)); + 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)); } $places[$from][] = $transition->getName(); } @@ -49,13 +44,13 @@ public function validate(Definition $definition, string $name) 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 1bad55e..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 Guard Event). - * Passing an array with WorkflowEvents will allow only those events to be dispatched plus - * the Guard Event. - * - * @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 17aa7e0..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; @@ -19,6 +20,8 @@ * Describes a workflow instance. * * @author Amrouche Hamza + * + * @method Transition|null getEnabledTransition(object $subject, string $name) */ interface WorkflowInterface { @@ -36,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 2c277fc..3e2c50a 100644 --- a/composer.json +++ b/composer.json @@ -20,22 +20,23 @@ } ], "require": { - "php": ">=8.1", - "symfony/deprecation-contracts": "^2.5|^3" + "php": ">=8.2", + "symfony/deprecation-contracts": "2.5|^3" }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", "symfony/error-handler": "^6.4|^7.0", - "symfony/event-dispatcher": "^5.4|^6.0|^7.0", - "symfony/expression-language": "^5.4|^6.0|^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": "^5.4|^6.0|^7.0", - "symfony/stopwatch": "^5.4|^6.0|^7.0", - "symfony/validator": "^5.4|^6.0|^7.0" + "symfony/security-core": "^6.4|^7.0", + "symfony/stopwatch": "^6.4|^7.0", + "symfony/validator": "^6.4|^7.0" }, "conflict": { - "symfony/event-dispatcher": "<5.4" + "symfony/event-dispatcher": "<6.4" }, "autoload": { "psr-4": { "Symfony\\Component\\Workflow\\": "" }, pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy