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/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 cee5504..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 @@ -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 bf8e888..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; } @@ -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/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 652e886..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'], diff --git a/Dumper/MermaidDumper.php b/Dumper/MermaidDumper.php index 69220ee..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 ); diff --git a/Dumper/PlantUmlDumper.php b/Dumper/PlantUmlDumper.php index e2f5859..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'; } 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 6f50382..fe7ccdf 100644 --- a/EventListener/AuditTrailListener.php +++ b/EventListener/AuditTrailListener.php @@ -20,35 +20,24 @@ */ 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())); } } - /** - * @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())); } - /** - * @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())); diff --git a/EventListener/ExpressionLanguage.php b/EventListener/ExpressionLanguage.php index 7848fb3..257f885 100644 --- a/EventListener/ExpressionLanguage.php +++ b/EventListener/ExpressionLanguage.php @@ -22,10 +22,7 @@ */ class ExpressionLanguage extends BaseExpressionLanguage { - /** - * @return void - */ - protected function registerFunctions() + protected function registerFunctions(): void { parent::registerFunctions(); 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 543478c..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/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 50d6169..a2844b7 100644 --- a/MarkingStore/MethodMarkingStore.php +++ b/MarkingStore/MethodMarkingStore.php @@ -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, }; 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 96be3cc..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]; } 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/Debug/TraceableWorkflowTest.php b/Tests/Debug/TraceableWorkflowTest.php index 3d8e699..257ad66 100644 --- a/Tests/Debug/TraceableWorkflowTest.php +++ b/Tests/Debug/TraceableWorkflowTest.php @@ -21,7 +21,7 @@ class TraceableWorkflowTest extends TestCase { - private MockObject|Workflow $innerWorkflow; + private MockObject&Workflow $innerWorkflow; private Stopwatch $stopwatch; diff --git a/Tests/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/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/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 543398a..48e2209 100644 --- a/Tests/WorkflowTest.php +++ b/Tests/WorkflowTest.php @@ -287,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()); @@ -320,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() @@ -769,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); @@ -815,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 65fd665..626a20e 100644 --- a/Validator/StateMachineValidator.php +++ b/Validator/StateMachineValidator.php @@ -19,10 +19,7 @@ */ 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) { diff --git a/Validator/WorkflowValidator.php b/Validator/WorkflowValidator.php index e3bee6d..f4eb292 100644 --- a/Validator/WorkflowValidator.php +++ b/Validator/WorkflowValidator.php @@ -20,23 +20,18 @@ */ 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])) { + 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(); diff --git a/Workflow.php b/Workflow.php index dee280b..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 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\\": "" },