Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
Total | |
0.00% |
0 / 1 |
|
77.78% |
14 / 18 |
CRAP | |
97.45% |
153 / 157 |
Workflow | |
0.00% |
0 / 1 |
|
77.78% |
14 / 18 |
63 | |
97.45% |
153 / 157 |
__construct | |
100.00% |
1 / 1 |
2 | |
100.00% |
5 / 5 |
|||
getMarking | |
100.00% |
1 / 1 |
8 | |
100.00% |
18 / 18 |
|||
can | |
100.00% |
1 / 1 |
4 | |
100.00% |
9 / 9 |
|||
buildTransitionBlockerList | |
100.00% |
1 / 1 |
6 | |
100.00% |
14 / 14 |
|||
apply | |
0.00% |
0 / 1 |
10 | |
96.77% |
30 / 31 |
|||
getEnabledTransitions | |
100.00% |
1 / 1 |
3 | |
100.00% |
7 / 7 |
|||
getName | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
getDefinition | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 1 |
|||
getMarkingStore | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 1 |
|||
getMetadataStore | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
buildTransitionBlockerListForTransition | |
100.00% |
1 / 1 |
5 | |
100.00% |
10 / 10 |
|||
guardTransition | |
0.00% |
0 / 1 |
2.01 | |
85.71% |
6 / 7 |
|||
leave | |
100.00% |
1 / 1 |
4 | |
100.00% |
10 / 10 |
|||
transition | |
100.00% |
1 / 1 |
2 | |
100.00% |
8 / 8 |
|||
enter | |
100.00% |
1 / 1 |
4 | |
100.00% |
10 / 10 |
|||
entered | |
100.00% |
1 / 1 |
4 | |
100.00% |
9 / 9 |
|||
completed | |
100.00% |
1 / 1 |
2 | |
100.00% |
7 / 7 |
|||
announce | |
100.00% |
1 / 1 |
3 | |
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 | |
12 | namespace Symfony\Component\Workflow; |
13 | |
14 | use Symfony\Component\Workflow\Event\AnnounceEvent; |
15 | use Symfony\Component\Workflow\Event\CompletedEvent; |
16 | use Symfony\Component\Workflow\Event\EnteredEvent; |
17 | use Symfony\Component\Workflow\Event\EnterEvent; |
18 | use Symfony\Component\Workflow\Event\GuardEvent; |
19 | use Symfony\Component\Workflow\Event\LeaveEvent; |
20 | use Symfony\Component\Workflow\Event\TransitionEvent; |
21 | use Symfony\Component\Workflow\Exception\LogicException; |
22 | use Symfony\Component\Workflow\Exception\NotEnabledTransitionException; |
23 | use Symfony\Component\Workflow\Exception\UndefinedTransitionException; |
24 | use Symfony\Component\Workflow\MarkingStore\MarkingStoreInterface; |
25 | use Symfony\Component\Workflow\MarkingStore\MethodMarkingStore; |
26 | use Symfony\Component\Workflow\Metadata\MetadataStoreInterface; |
27 | use 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 | */ |
34 | class 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 | } |