Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
77.78% covered (warning)
77.78%
14 / 18
CRAP
97.45% covered (success)
97.45%
153 / 157
Workflow
0.00% covered (danger)
0.00%
0 / 1
77.78% covered (warning)
77.78%
14 / 18
63
97.45% covered (success)
97.45%
153 / 157
 __construct
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
5 / 5
 getMarking
100.00% covered (success)
100.00%
1 / 1
8
100.00% covered (success)
100.00%
18 / 18
 can
100.00% covered (success)
100.00%
1 / 1
4
100.00% covered (success)
100.00%
9 / 9
 buildTransitionBlockerList
100.00% covered (success)
100.00%
1 / 1
6
100.00% covered (success)
100.00%
14 / 14
 apply
0.00% covered (danger)
0.00%
0 / 1
10
96.77% covered (success)
96.77%
30 / 31
 getEnabledTransitions
100.00% covered (success)
100.00%
1 / 1
3
100.00% covered (success)
100.00%
7 / 7
 getName
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 getDefinition
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 getMarkingStore
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 getMetadataStore
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 buildTransitionBlockerListForTransition
100.00% covered (success)
100.00%
1 / 1
5
100.00% covered (success)
100.00%
10 / 10
 guardTransition
0.00% covered (danger)
0.00%
0 / 1
2.01
85.71% covered (warning)
85.71%
6 / 7
 leave
100.00% covered (success)
100.00%
1 / 1
4
100.00% covered (success)
100.00%
10 / 10
 transition
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
8 / 8
 enter
100.00% covered (success)
100.00%
1 / 1
4
100.00% covered (success)
100.00%
10 / 10
 entered
100.00% covered (success)
100.00%
1 / 1
4
100.00% covered (success)
100.00%
9 / 9
 completed
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
7 / 7
 announce
100.00% covered (success)
100.00%
1 / 1
3
100.00% covered (success)
100.00%
8 / 8
1<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\Workflow;
13
14use Symfony\Component\Workflow\Event\AnnounceEvent;
15use Symfony\Component\Workflow\Event\CompletedEvent;
16use Symfony\Component\Workflow\Event\EnteredEvent;
17use Symfony\Component\Workflow\Event\EnterEvent;
18use Symfony\Component\Workflow\Event\GuardEvent;
19use Symfony\Component\Workflow\Event\LeaveEvent;
20use Symfony\Component\Workflow\Event\TransitionEvent;
21use Symfony\Component\Workflow\Exception\LogicException;
22use Symfony\Component\Workflow\Exception\NotEnabledTransitionException;
23use Symfony\Component\Workflow\Exception\UndefinedTransitionException;
24use Symfony\Component\Workflow\MarkingStore\MarkingStoreInterface;
25use Symfony\Component\Workflow\MarkingStore\MethodMarkingStore;
26use Symfony\Component\Workflow\Metadata\MetadataStoreInterface;
27use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
28
29/**
30 * @author Fabien Potencier <fabien@symfony.com>
31 * @author GrĂ©goire Pineau <lyrixx@lyrixx.info>
32 * @author Tobias Nyholm <tobias.nyholm@gmail.com>
33 */
34class Workflow implements WorkflowInterface
35{
36    public const DISABLE_ANNOUNCE_EVENT = 'workflow_disable_announce_event';
37
38    private $definition;
39    private $markingStore;
40    private $dispatcher;
41    private $name;
42
43    public function __construct(Definition $definition, MarkingStoreInterface $markingStore = null, EventDispatcherInterface $dispatcher = null, string $name = 'unnamed')
44    {
45        $this->definition = $definition;
46        $this->markingStore = $markingStore ?: new MethodMarkingStore();
47        $this->dispatcher = $dispatcher;
48        $this->name = $name;
49    }
50
51    /**
52     * {@inheritdoc}
53     */
54    public function getMarking(object $subject)
55    {
56        $marking = $this->markingStore->getMarking($subject);
57
58        if (!$marking instanceof Marking) {
59            throw new LogicException(sprintf('The value returned by the MarkingStore is not an instance of "%s" for workflow "%s".', Marking::class, $this->name));
60        }
61
62        // check if the subject is already in the workflow
63        if (!$marking->getPlaces()) {
64            if (!$this->definition->getInitialPlaces()) {
65                throw new LogicException(sprintf('The Marking is empty and there is no initial place for workflow "%s".', $this->name));
66            }
67            foreach ($this->definition->getInitialPlaces() as $place) {
68                $marking->mark($place);
69            }
70
71            // update the subject with the new marking
72            $this->markingStore->setMarking($subject, $marking);
73
74            $this->entered($subject, null, $marking);
75        }
76
77        // check that the subject has a known place
78        $places = $this->definition->getPlaces();
79        foreach ($marking->getPlaces() as $placeName => $nbToken) {
80            if (!isset($places[$placeName])) {
81                $message = sprintf('Place "%s" is not valid for workflow "%s".', $placeName, $this->name);
82                if (!$places) {
83                    $message .= ' It seems you forgot to add places to the current workflow.';
84                }
85
86                throw new LogicException($message);
87            }
88        }
89
90        return $marking;
91    }
92
93    /**
94     * {@inheritdoc}
95     */
96    public function can(object $subject, string $transitionName)
97    {
98        $transitions = $this->definition->getTransitions();
99        $marking = $this->getMarking($subject);
100
101        foreach ($transitions as $transition) {
102            if ($transition->getName() !== $transitionName) {
103                continue;
104            }
105
106            $transitionBlockerList = $this->buildTransitionBlockerListForTransition($subject, $marking, $transition);
107
108            if ($transitionBlockerList->isEmpty()) {
109                return true;
110            }
111        }
112
113        return false;
114    }
115
116    /**
117     * {@inheritdoc}
118     */
119    public function buildTransitionBlockerList(object $subject, string $transitionName): TransitionBlockerList
120    {
121        $transitions = $this->definition->getTransitions();
122        $marking = $this->getMarking($subject);
123        $transitionBlockerList = null;
124
125        foreach ($transitions as $transition) {
126            if ($transition->getName() !== $transitionName) {
127                continue;
128            }
129
130            $transitionBlockerList = $this->buildTransitionBlockerListForTransition($subject, $marking, $transition);
131
132            if ($transitionBlockerList->isEmpty()) {
133                return $transitionBlockerList;
134            }
135
136            // We prefer to return transitions blocker by something else than
137            // marking. Because it means the marking was OK. Transitions are
138            // deterministic: it's not possible to have many transitions enabled
139            // at the same time that match the same marking with the same name
140            if (!$transitionBlockerList->has(TransitionBlocker::BLOCKED_BY_MARKING)) {
141                return $transitionBlockerList;
142            }
143        }
144
145        if (!$transitionBlockerList) {
146            throw new UndefinedTransitionException($subject, $transitionName, $this);
147        }
148
149        return $transitionBlockerList;
150    }
151
152    /**
153     * {@inheritdoc}
154     */
155    public function apply(object $subject, string $transitionName, array $context = [])
156    {
157        $marking = $this->getMarking($subject);
158
159        $transitionExist = false;
160        $approvedTransitions = [];
161        $bestTransitionBlockerList = null;
162
163        foreach ($this->definition->getTransitions() as $transition) {
164            if ($transition->getName() !== $transitionName) {
165                continue;
166            }
167
168            $transitionExist = true;
169
170            $tmpTransitionBlockerList = $this->buildTransitionBlockerListForTransition($subject, $marking, $transition);
171
172            if ($tmpTransitionBlockerList->isEmpty()) {
173                $approvedTransitions[] = $transition;
174                continue;
175            }
176
177            if (!$bestTransitionBlockerList) {
178                $bestTransitionBlockerList = $tmpTransitionBlockerList;
179                continue;
180            }
181
182            // We prefer to return transitions blocker by something else than
183            // marking. Because it means the marking was OK. Transitions are
184            // deterministic: it's not possible to have many transitions enabled
185            // at the same time that match the same marking with the same name
186            if (!$tmpTransitionBlockerList->has(TransitionBlocker::BLOCKED_BY_MARKING)) {
187                $bestTransitionBlockerList = $tmpTransitionBlockerList;
188            }
189        }
190
191        if (!$transitionExist) {
192            throw new UndefinedTransitionException($subject, $transitionName, $this, $context);
193        }
194
195        if (!$approvedTransitions) {
196            throw new NotEnabledTransitionException($subject, $transitionName, $this, $bestTransitionBlockerList, $context);
197        }
198
199        foreach ($approvedTransitions as $transition) {
200            $this->leave($subject, $transition, $marking);
201
202            $context = $this->transition($subject, $transition, $marking, $context);
203
204            $this->enter($subject, $transition, $marking);
205
206            $this->markingStore->setMarking($subject, $marking, $context);
207
208            $this->entered($subject, $transition, $marking);
209
210            $this->completed($subject, $transition, $marking);
211
212            if (!($context[self::DISABLE_ANNOUNCE_EVENT] ?? false)) {
213                $this->announce($subject, $transition, $marking);
214            }
215        }
216
217        return $marking;
218    }
219
220    /**
221     * {@inheritdoc}
222     */
223    public function getEnabledTransitions(object $subject)
224    {
225        $enabledTransitions = [];
226        $marking = $this->getMarking($subject);
227
228        foreach ($this->definition->getTransitions() as $transition) {
229            $transitionBlockerList = $this->buildTransitionBlockerListForTransition($subject, $marking, $transition);
230            if ($transitionBlockerList->isEmpty()) {
231                $enabledTransitions[] = $transition;
232            }
233        }
234
235        return $enabledTransitions;
236    }
237
238    /**
239     * {@inheritdoc}
240     */
241    public function getName()
242    {
243        return $this->name;
244    }
245
246    /**
247     * {@inheritdoc}
248     */
249    public function getDefinition()
250    {
251        return $this->definition;
252    }
253
254    /**
255     * {@inheritdoc}
256     */
257    public function getMarkingStore()
258    {
259        return $this->markingStore;
260    }
261
262    /**
263     * {@inheritdoc}
264     */
265    public function getMetadataStore(): MetadataStoreInterface
266    {
267        return $this->definition->getMetadataStore();
268    }
269
270    private function buildTransitionBlockerListForTransition(object $subject, Marking $marking, Transition $transition): TransitionBlockerList
271    {
272        foreach ($transition->getFroms() as $place) {
273            if (!$marking->has($place)) {
274                return new TransitionBlockerList([
275                    TransitionBlocker::createBlockedByMarking($marking),
276                ]);
277            }
278        }
279
280        if (null === $this->dispatcher) {
281            return new TransitionBlockerList();
282        }
283
284        $event = $this->guardTransition($subject, $marking, $transition);
285
286        if ($event->isBlocked()) {
287            return $event->getTransitionBlockerList();
288        }
289
290        return new TransitionBlockerList();
291    }
292
293    private function guardTransition(object $subject, Marking $marking, Transition $transition): ?GuardEvent
294    {
295        if (null === $this->dispatcher) {
296            return null;
297        }
298
299        $event = new GuardEvent($subject, $marking, $transition, $this);
300
301        $this->dispatcher->dispatch($event, WorkflowEvents::GUARD);
302        $this->dispatcher->dispatch($event, sprintf('workflow.%s.guard', $this->name));
303        $this->dispatcher->dispatch($event, sprintf('workflow.%s.guard.%s', $this->name, $transition->getName()));
304
305        return $event;
306    }
307
308    private function leave(object $subject, Transition $transition, Marking $marking): void
309    {
310        $places = $transition->getFroms();
311
312        if (null !== $this->dispatcher) {
313            $event = new LeaveEvent($subject, $marking, $transition, $this);
314
315            $this->dispatcher->dispatch($event, WorkflowEvents::LEAVE);
316            $this->dispatcher->dispatch($event, sprintf('workflow.%s.leave', $this->name));
317
318            foreach ($places as $place) {
319                $this->dispatcher->dispatch($event, sprintf('workflow.%s.leave.%s', $this->name, $place));
320            }
321        }
322
323        foreach ($places as $place) {
324            $marking->unmark($place);
325        }
326    }
327
328    private function transition(object $subject, Transition $transition, Marking $marking, array $context): array
329    {
330        if (null === $this->dispatcher) {
331            return $context;
332        }
333
334        $event = new TransitionEvent($subject, $marking, $transition, $this);
335        $event->setContext($context);
336
337        $this->dispatcher->dispatch($event, WorkflowEvents::TRANSITION);
338        $this->dispatcher->dispatch($event, sprintf('workflow.%s.transition', $this->name));
339        $this->dispatcher->dispatch($event, sprintf('workflow.%s.transition.%s', $this->name, $transition->getName()));
340
341        return $event->getContext();
342    }
343
344    private function enter(object $subject, Transition $transition, Marking $marking): void
345    {
346        $places = $transition->getTos();
347
348        if (null !== $this->dispatcher) {
349            $event = new EnterEvent($subject, $marking, $transition, $this);
350
351            $this->dispatcher->dispatch($event, WorkflowEvents::ENTER);
352            $this->dispatcher->dispatch($event, sprintf('workflow.%s.enter', $this->name));
353
354            foreach ($places as $place) {
355                $this->dispatcher->dispatch($event, sprintf('workflow.%s.enter.%s', $this->name, $place));
356            }
357        }
358
359        foreach ($places as $place) {
360            $marking->mark($place);
361        }
362    }
363
364    private function entered(object $subject, ?Transition $transition, Marking $marking): void
365    {
366        if (null === $this->dispatcher) {
367            return;
368        }
369
370        $event = new EnteredEvent($subject, $marking, $transition, $this);
371
372        $this->dispatcher->dispatch($event, WorkflowEvents::ENTERED);
373        $this->dispatcher->dispatch($event, sprintf('workflow.%s.entered', $this->name));
374
375        if ($transition) {
376            foreach ($transition->getTos() as $place) {
377                $this->dispatcher->dispatch($event, sprintf('workflow.%s.entered.%s', $this->name, $place));
378            }
379        }
380    }
381
382    private function completed(object $subject, Transition $transition, Marking $marking): void
383    {
384        if (null === $this->dispatcher) {
385            return;
386        }
387
388        $event = new CompletedEvent($subject, $marking, $transition, $this);
389
390        $this->dispatcher->dispatch($event, WorkflowEvents::COMPLETED);
391        $this->dispatcher->dispatch($event, sprintf('workflow.%s.completed', $this->name));
392        $this->dispatcher->dispatch($event, sprintf('workflow.%s.completed.%s', $this->name, $transition->getName()));
393    }
394
395    private function announce(object $subject, Transition $initialTransition, Marking $marking): void
396    {
397        if (null === $this->dispatcher) {
398            return;
399        }
400
401        $event = new AnnounceEvent($subject, $marking, $initialTransition, $this);
402
403        $this->dispatcher->dispatch($event, WorkflowEvents::ANNOUNCE);
404        $this->dispatcher->dispatch($event, sprintf('workflow.%s.announce', $this->name));
405
406        foreach ($this->getEnabledTransitions($subject) as $transition) {
407            $this->dispatcher->dispatch($event, sprintf('workflow.%s.announce.%s', $this->name, $transition->getName()));
408        }
409    }
410}