Issues (3877)

Zed/Oms/Business/OrderStateMachine/Builder.php (8 issues)

1
<?php
2
3
/**
4
 * Copyright © 2016-present Spryker Systems GmbH. All rights reserved.
5
 * Use of this software requires acceptance of the Evaluation License Agreement. See LICENSE file.
6
 */
7
8
namespace Spryker\Zed\Oms\Business\OrderStateMachine;
9
10
use LogicException;
11
use SimpleXMLElement;
12
use Spryker\Zed\Oms\Business\Exception\StatemachineException;
13
use Spryker\Zed\Oms\Business\Process\EventInterface;
14
use Spryker\Zed\Oms\Business\Process\ProcessInterface;
15
use Spryker\Zed\Oms\Business\Process\StateInterface;
16
use Spryker\Zed\Oms\Business\Process\TransitionInterface;
17
use Spryker\Zed\Oms\Business\Reader\ProcessCacheReaderInterface;
18
use Spryker\Zed\Oms\Business\Writer\ProcessCacheWriterInterface;
19
use Symfony\Component\Finder\Finder as SymfonyFinder;
20
21
class Builder implements BuilderInterface
22
{
23
    /**
24
     * @var \SimpleXMLElement
25
     */
26
    protected $rootElement;
27
28
    /**
29
     * @var array<\Spryker\Zed\Oms\Business\Process\ProcessInterface>
30
     */
31
    protected static $processBuffer = [];
32
33
    /**
34
     * @var \Spryker\Zed\Oms\Business\Process\EventInterface
35
     */
36
    protected $event;
37
38
    /**
39
     * @var \Spryker\Zed\Oms\Business\Process\StateInterface
40
     */
41
    protected $state;
42
43
    /**
44
     * @var \Spryker\Zed\Oms\Business\Process\TransitionInterface
45
     */
46
    protected $transition;
47
48
    /**
49
     * @var \Spryker\Zed\Oms\Business\Process\ProcessInterface
50
     */
51
    protected $process;
52
53
    /**
54
     * @var array|string
55
     */
56
    protected $processDefinitionLocation;
57
58
    /**
59
     * @var string
60
     */
61
    protected $subProcessPrefixDelimiter;
62
63
    /**
64
     * @var \Spryker\Zed\Oms\Business\Reader\ProcessCacheReaderInterface
65
     */
66
    protected ProcessCacheReaderInterface $processCacheReader;
67
68
    /**
69
     * @var \Spryker\Zed\Oms\Business\Writer\ProcessCacheWriterInterface
70
     */
71
    protected ProcessCacheWriterInterface $processCacheWriter;
72
73
    /**
74
     * @param \Spryker\Zed\Oms\Business\Process\EventInterface $event
75
     * @param \Spryker\Zed\Oms\Business\Process\StateInterface $state
76
     * @param \Spryker\Zed\Oms\Business\Process\TransitionInterface $transition
77
     * @param \Spryker\Zed\Oms\Business\Process\ProcessInterface $process
78
     * @param array|string $processDefinitionLocation
79
     * @param \Spryker\Zed\Oms\Business\Reader\ProcessCacheReaderInterface $processCacheReader
80
     * @param \Spryker\Zed\Oms\Business\Writer\ProcessCacheWriterInterface $processCacheWriter
81
     * @param string $subProcessPrefixDelimiter
82
     */
83
    public function __construct(
84
        EventInterface $event,
85
        StateInterface $state,
86
        TransitionInterface $transition,
87
        ProcessInterface $process,
88
        $processDefinitionLocation,
89
        ProcessCacheReaderInterface $processCacheReader,
90
        ProcessCacheWriterInterface $processCacheWriter,
91
        $subProcessPrefixDelimiter = ' - '
92
    ) {
93
        $this->event = $event;
94
        $this->state = $state;
95
        $this->transition = $transition;
96
        $this->process = $process;
97
        $this->processCacheReader = $processCacheReader;
98
        $this->processCacheWriter = $processCacheWriter;
99
        $this->subProcessPrefixDelimiter = $subProcessPrefixDelimiter;
100
101
        $this->setProcessDefinitionLocation($processDefinitionLocation);
102
    }
103
104
    /**
105
     * @param string $processName
106
     *
107
     * @return \Spryker\Zed\Oms\Business\Process\ProcessInterface
108
     */
109
    public function createProcess($processName)
110
    {
111
        if (isset(static::$processBuffer[$processName])) {
112
            return static::$processBuffer[$processName];
113
        }
114
115
        if ($this->processCacheReader->hasProcess($processName)) {
116
            static::$processBuffer[$processName] = $this->processCacheReader->getProcess($processName);
117
118
            return static::$processBuffer[$processName];
119
        }
120
121
        $mainProcess = $this->createMainProcess($processName);
122
123
        static::$processBuffer[$processName] = $mainProcess;
124
125
        $this->processCacheWriter->cacheProcess($mainProcess, $processName);
126
127
        return static::$processBuffer[$processName];
128
    }
129
130
    /**
131
     * @param string $processName
132
     *
133
     * @return \Spryker\Zed\Oms\Business\Process\ProcessInterface
134
     */
135
    protected function createMainProcess(string $processName): ProcessInterface
136
    {
137
        $this->rootElement = $this->loadXmlFromProcessName($processName);
138
139
        $this->mergeSubProcessFiles();
140
141
        /** @var array<\Spryker\Zed\Oms\Business\Process\ProcessInterface> $processMap */
142
        $processMap = [];
143
144
        [$processMap, $mainProcess] = $this->createSubProcess($processMap);
145
146
        $stateToProcessMap = $this->createStates($processMap);
147
148
        $this->createSubProcesses($processMap);
149
150
        $eventMap = $this->createEvents();
151
152
        $this->createTransitions($stateToProcessMap, $processMap, $eventMap);
153
154
        return $mainProcess->warmupCache();
155
    }
156
157
    /**
158
     * @return void
159
     */
160
    protected function mergeSubProcessFiles()
161
    {
162
        foreach ($this->rootElement->children() as $xmlProcess) {
163
            $processFile = $this->getAttributeString($xmlProcess, 'file');
0 ignored issues
show
It seems like $xmlProcess can also be of type null; however, parameter $xmlElement of Spryker\Zed\Oms\Business...r::getAttributeString() does only seem to accept SimpleXMLElement, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

163
            $processFile = $this->getAttributeString(/** @scrutinizer ignore-type */ $xmlProcess, 'file');
Loading history...
164
            $processName = $this->getAttributeString($xmlProcess, 'name');
165
            $processPrefix = $this->getAttributeString($xmlProcess, 'prefix');
166
167
            if ($processFile) {
168
                $xmlSubProcess = $this->loadXmlFromFileName(str_replace(' ', '_', $processFile));
169
170
                if ($processName) {
171
                    $xmlSubProcess->children()->process[0]['name'] = $processName;
172
                }
173
174
                $this->recursiveMerge($xmlSubProcess, $this->rootElement, $processPrefix);
175
            }
176
        }
177
    }
178
179
    /**
180
     * @param \SimpleXMLElement $fromXmlElement
181
     * @param \SimpleXMLElement $intoXmlNode
182
     * @param string|null $prefix
183
     *
184
     * @return void
185
     */
186
    protected function recursiveMerge($fromXmlElement, $intoXmlNode, $prefix = null)
187
    {
188
        /** @var array<\SimpleXMLElement> $xmlElements */
189
        $xmlElements = $fromXmlElement->children();
190
        if (!$xmlElements) {
191
            return;
192
        }
193
194
        foreach ($xmlElements as $xmlElement) {
195
            $xmlElement = $this->prefixSubProcessElementValue($xmlElement, $prefix);
196
            $xmlElement = $this->prefixSubProcessElementAttributes($xmlElement, $prefix);
197
198
            $child = $intoXmlNode->addChild($xmlElement->getName(), $xmlElement);
199
            $attributes = $xmlElement->attributes();
200
            foreach ($attributes as $k => $v) {
201
                $child->addAttribute($k, $v);
202
            }
203
204
            $this->recursiveMerge($xmlElement, $child, $prefix);
205
        }
206
    }
207
208
    /**
209
     * @param \SimpleXMLElement $xmlElement
210
     * @param string|null $prefix
211
     *
212
     * @return \SimpleXMLElement
213
     */
214
    protected function prefixSubProcessElementValue(SimpleXMLElement $xmlElement, $prefix = null)
215
    {
216
        if ($prefix === null) {
217
            return $xmlElement;
218
        }
219
220
        $namespaceDependentElementNames = ['source', 'target', 'event'];
221
222
        if (in_array($xmlElement->getName(), $namespaceDependentElementNames)) {
223
            $xmlElement[0] = $prefix . $this->subProcessPrefixDelimiter . $xmlElement[0];
224
        }
225
226
        return $xmlElement;
227
    }
228
229
    /**
230
     * @param \SimpleXMLElement $xmlElement
231
     * @param string|null $prefix
232
     *
233
     * @return \SimpleXMLElement
234
     */
235
    protected function prefixSubProcessElementAttributes(SimpleXMLElement $xmlElement, $prefix = null)
236
    {
237
        if ($prefix === null) {
238
            return $xmlElement;
239
        }
240
241
        $namespaceDependentElementNames = ['state', 'event'];
242
243
        if (in_array($xmlElement->getName(), $namespaceDependentElementNames)) {
244
            $xmlElement->attributes()['name'] = $prefix . $this->subProcessPrefixDelimiter . $xmlElement->attributes()['name'];
245
        }
246
247
        return $xmlElement;
248
    }
249
250
    /**
251
     * @param string $fileName
252
     *
253
     * @return \SimpleXMLElement
254
     */
255
    protected function loadXmlFromFileName($fileName)
256
    {
257
        $definitionFile = $this->locateProcessDefinition($fileName);
258
259
        return $this->loadXml($definitionFile->getContents());
260
    }
261
262
    /**
263
     * @param string $fileName
264
     *
265
     * @return \Symfony\Component\Finder\SplFileInfo
266
     */
267
    private function locateProcessDefinition($fileName)
268
    {
269
        $finder = $this->buildFinder($fileName);
270
271
        /** @phpstan-var \Symfony\Component\Finder\SplFileInfo */
272
        return current(iterator_to_array($finder->getIterator()));
273
    }
274
275
    /**
276
     * @param string $processName
277
     *
278
     * @return \SimpleXMLElement
279
     */
280
    protected function loadXmlFromProcessName($processName)
281
    {
282
        return $this->loadXmlFromFileName($processName . '.xml');
283
    }
284
285
    /**
286
     * @param string $xml
287
     *
288
     * @return \SimpleXMLElement
289
     */
290
    protected function loadXml($xml)
291
    {
292
        return new SimpleXMLElement($xml);
293
    }
294
295
    /**
296
     * @return array
297
     */
298
    protected function createEvents()
299
    {
300
        $eventMap = [];
301
302
        foreach ($this->rootElement as $xmlProcess) {
303
            if (!isset($xmlProcess->events)) {
304
                continue;
305
            }
306
307
            $xmlEvents = $xmlProcess->events->children();
308
            foreach ($xmlEvents as $xmlEvent) {
309
                $event = clone $this->event;
310
                $eventId = $this->getAttributeString($xmlEvent, 'name');
0 ignored issues
show
It seems like $xmlEvent can also be of type null; however, parameter $xmlElement of Spryker\Zed\Oms\Business...r::getAttributeString() does only seem to accept SimpleXMLElement, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

310
                $eventId = $this->getAttributeString(/** @scrutinizer ignore-type */ $xmlEvent, 'name');
Loading history...
311
                $event->setCommand($this->getAttributeString($xmlEvent, 'command'));
312
                $event->setManual($this->getAttributeBoolean($xmlEvent, 'manual'));
0 ignored issues
show
It seems like $xmlEvent can also be of type null; however, parameter $xmlElement of Spryker\Zed\Oms\Business...::getAttributeBoolean() does only seem to accept SimpleXMLElement, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

312
                $event->setManual($this->getAttributeBoolean(/** @scrutinizer ignore-type */ $xmlEvent, 'manual'));
Loading history...
313
                $event->setOnEnter($this->getAttributeBoolean($xmlEvent, 'onEnter'));
314
                $event->setTimeout($this->getAttributeString($xmlEvent, 'timeout'));
315
                $event->setTimeoutProcessor($this->getAttributeString($xmlEvent, 'timeoutProcessor'));
316
                if ($eventId === null) {
317
                    continue;
318
                }
319
320
                $event->setName($eventId);
321
                $eventMap[$event->getName()] = $event;
322
            }
323
        }
324
325
        return $eventMap;
326
    }
327
328
    /**
329
     * @param array<\Spryker\Zed\Oms\Business\Process\ProcessInterface> $processMap
330
     *
331
     * @return array
332
     */
333
    protected function createSubProcess(array $processMap)
334
    {
335
        $mainProcess = null;
336
        $xmlProcesses = $this->rootElement->children();
337
338
        /** @var \SimpleXMLElement $xmlProcess */
339
        foreach ($xmlProcesses as $xmlProcess) {
340
            $process = clone $this->process;
341
            $processName = $this->getAttributeString($xmlProcess, 'name');
0 ignored issues
show
It seems like $xmlProcess can also be of type null; however, parameter $xmlElement of Spryker\Zed\Oms\Business...r::getAttributeString() does only seem to accept SimpleXMLElement, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

341
            $processName = $this->getAttributeString(/** @scrutinizer ignore-type */ $xmlProcess, 'name');
Loading history...
342
            $process->setName($processName);
343
            $processMap[$processName] = $process;
344
            $process->setIsMain($this->getAttributeBoolean($xmlProcess, 'main'));
345
346
            $process->setFile($this->getAttributeString($xmlProcess, 'file'));
347
348
            if ($process->getIsMain()) {
349
                $mainProcess = $process;
350
            }
351
        }
352
353
        return [$processMap, $mainProcess];
354
    }
355
356
    /**
357
     * @param array<\Spryker\Zed\Oms\Business\Process\ProcessInterface> $processMap
358
     *
359
     * @return void
360
     */
361
    protected function createSubProcesses(array $processMap)
362
    {
363
        foreach ($this->rootElement as $xmlProcess) {
364
            $processName = $this->getAttributeString($xmlProcess, 'name');
0 ignored issues
show
It seems like $xmlProcess can also be of type null; however, parameter $xmlElement of Spryker\Zed\Oms\Business...r::getAttributeString() does only seem to accept SimpleXMLElement, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

364
            $processName = $this->getAttributeString(/** @scrutinizer ignore-type */ $xmlProcess, 'name');
Loading history...
365
366
            $process = $processMap[$processName];
367
368
            if ($xmlProcess->subprocesses) {
369
                $xmlSubProcesses = $xmlProcess->subprocesses->children();
370
371
                foreach ($xmlSubProcesses as $xmlSubProcess) {
372
                    $subProcessName = (string)$xmlSubProcess;
373
                    $subProcess = $processMap[$subProcessName];
374
                    $process->addSubProcess($subProcess);
375
                }
376
            }
377
        }
378
    }
379
380
    /**
381
     * @param array<\Spryker\Zed\Oms\Business\Process\ProcessInterface> $processMap
382
     *
383
     * @return array<\Spryker\Zed\Oms\Business\Process\ProcessInterface>
384
     */
385
    protected function createStates(array $processMap)
386
    {
387
        $stateToProcessMap = [];
388
389
        $xmlProcesses = $this->rootElement->children();
390
        foreach ($xmlProcesses as $xmlProcess) {
391
            $processName = $this->getAttributeString($xmlProcess, 'name');
0 ignored issues
show
It seems like $xmlProcess can also be of type null; however, parameter $xmlElement of Spryker\Zed\Oms\Business...r::getAttributeString() does only seem to accept SimpleXMLElement, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

391
            $processName = $this->getAttributeString(/** @scrutinizer ignore-type */ $xmlProcess, 'name');
Loading history...
392
            $process = $processMap[$processName];
393
394
            if ($xmlProcess->states) {
395
                $xmlStates = $xmlProcess->states->children();
396
                /** @var \SimpleXMLElement $xmlState */
397
                foreach ($xmlStates as $xmlState) {
398
                    $state = clone $this->state;
399
                    $state->setName($this->getAttributeString($xmlState, 'name'));
400
                    $state->setDisplay($this->getAttributeString($xmlState, 'display'));
401
                    $state->setReserved($this->getAttributeBoolean($xmlState, 'reserved'));
402
                    $state->setProcess($process);
403
404
                    /** @var array $stateFlag */
405
                    $stateFlag = $xmlState->flag;
406
                    if ($stateFlag) {
407
                        $flags = $xmlState->children();
408
                        foreach ($flags->flag as $flag) {
409
                            $state->addFlag((string)$flag);
410
                        }
411
                    }
412
413
                    $process->addState($state);
414
                    $stateToProcessMap[$state->getName()] = $process;
415
                }
416
            }
417
        }
418
419
        return $stateToProcessMap;
420
    }
421
422
    /**
423
     * @param array<\Spryker\Zed\Oms\Business\Process\ProcessInterface> $stateToProcessMap
424
     * @param array<\Spryker\Zed\Oms\Business\Process\ProcessInterface> $processMap
425
     * @param array<\Spryker\Zed\Oms\Business\Process\EventInterface> $eventMap
426
     *
427
     * @throws \LogicException
428
     *
429
     * @return void
430
     */
431
    protected function createTransitions(array $stateToProcessMap, array $processMap, array $eventMap)
432
    {
433
        foreach ($this->rootElement as $xmlProcess) {
434
            if ($xmlProcess->transitions) {
435
                $xmlTransitions = $xmlProcess->transitions->children();
436
437
                $processName = $this->getAttributeString($xmlProcess, 'name');
0 ignored issues
show
It seems like $xmlProcess can also be of type null; however, parameter $xmlElement of Spryker\Zed\Oms\Business...r::getAttributeString() does only seem to accept SimpleXMLElement, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

437
                $processName = $this->getAttributeString(/** @scrutinizer ignore-type */ $xmlProcess, 'name');
Loading history...
438
439
                foreach ($xmlTransitions as $xmlTransition) {
440
                    $transition = clone $this->transition;
441
442
                    $transition->setCondition($this->getAttributeString($xmlTransition, 'condition'));
443
444
                    $transition->setHappy($this->getAttributeBoolean($xmlTransition, 'happy'));
0 ignored issues
show
It seems like $xmlTransition can also be of type null; however, parameter $xmlElement of Spryker\Zed\Oms\Business...::getAttributeBoolean() does only seem to accept SimpleXMLElement, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

444
                    $transition->setHappy($this->getAttributeBoolean(/** @scrutinizer ignore-type */ $xmlTransition, 'happy'));
Loading history...
445
446
                    $sourceName = (string)$xmlTransition->source;
447
448
                    if (!isset($stateToProcessMap[$sourceName])) {
449
                        throw new LogicException(sprintf('Source: %s does not exist.', $sourceName));
450
                    }
451
452
                    $sourceProcess = $stateToProcessMap[$sourceName];
453
                    $sourceState = $sourceProcess->getState($sourceName);
454
                    $transition->setSource($sourceState);
455
                    $sourceState->addOutgoingTransition($transition);
456
457
                    $targetName = (string)$xmlTransition->target;
458
459
                    if (!isset($stateToProcessMap[$targetName])) {
460
                        throw new LogicException('Target: "' . $targetName . '" does not exist from source: "' . $sourceName . '"');
461
                    }
462
                    $targetProcess = $stateToProcessMap[$targetName];
463
                    $targetState = $targetProcess->getState($targetName);
464
                    $transition->setTarget($targetState);
465
                    $targetState->addIncomingTransition($transition);
466
467
                    if (isset($xmlTransition->event)) {
468
                        $eventId = (string)$xmlTransition->event;
469
470
                        if (!isset($eventMap[$eventId])) {
471
                            throw new LogicException('Event: "' . $eventId . '" does not exist from source: "' . $sourceName . '"');
472
                        }
473
474
                        $event = $eventMap[$eventId];
475
                        $event->addTransition($transition);
476
                        $transition->setEvent($event);
477
                    }
478
479
                    $processMap[$processName]->addTransition($transition);
480
                }
481
            }
482
        }
483
    }
484
485
    /**
486
     * @param \SimpleXMLElement $xmlElement
487
     * @param string $attributeName
488
     *
489
     * @return string|null
490
     */
491
    protected function getAttributeString(SimpleXMLElement $xmlElement, $attributeName)
492
    {
493
        $string = (string)$xmlElement->attributes()[$attributeName];
494
        $string = ($string === '') ? null : $string;
495
496
        return $string;
497
    }
498
499
    /**
500
     * @param \SimpleXMLElement $xmlElement
501
     * @param string $attributeName
502
     *
503
     * @return bool
504
     */
505
    protected function getAttributeBoolean(SimpleXMLElement $xmlElement, $attributeName)
506
    {
507
        return (string)$xmlElement->attributes()[$attributeName] === 'true';
508
    }
509
510
    /**
511
     * @param array|string|null $processDefinitionLocation
512
     *
513
     * @return void
514
     */
515
    private function setProcessDefinitionLocation($processDefinitionLocation)
516
    {
517
        $this->processDefinitionLocation = $processDefinitionLocation;
518
    }
519
520
    /**
521
     * @param string $fileName
522
     *
523
     * @return \Symfony\Component\Finder\Finder
524
     */
525
    protected function buildFinder($fileName)
526
    {
527
        $finder = $this->getFinder();
528
        $finder->in($this->processDefinitionLocation);
529
        if (strpos($fileName, '/') !== false) {
530
            $finder->path($this->createSubProcessPathPattern($fileName));
531
            $finder->name(basename($fileName));
532
        } else {
533
            $finder->name($fileName);
534
        }
535
536
        $this->validateFinder($finder, $fileName);
537
538
        return $finder;
539
    }
540
541
    /**
542
     * @return \Symfony\Component\Finder\Finder
543
     */
544
    protected function getFinder()
545
    {
546
        return new SymfonyFinder();
547
    }
548
549
    /**
550
     * @param \Symfony\Component\Finder\Finder $finder
551
     * @param string $fileName
552
     *
553
     * @throws \Spryker\Zed\Oms\Business\Exception\StatemachineException
554
     *
555
     * @return void
556
     */
557
    protected function validateFinder(SymfonyFinder $finder, $fileName)
558
    {
559
        if ($finder->count() > 1) {
560
            throw new StatemachineException(
561
                sprintf(
562
                    '"%s" found in more then one location. Could not determine which one to choose. Please check your process definition location',
563
                    $fileName,
564
                ),
565
            );
566
        }
567
568
        if ($finder->count() === 0) {
569
            throw new StatemachineException(
570
                sprintf(
571
                    'Could not find "%s". Please check your process definition location',
572
                    $fileName,
573
                ),
574
            );
575
        }
576
    }
577
578
    /**
579
     * @param string $fileName
580
     *
581
     * @return string
582
     */
583
    protected function createSubProcessPathPattern($fileName)
584
    {
585
        return '/\b' . preg_quote(dirname($fileName), '/') . '\b/';
586
    }
587
}
588