Completed
Push — master ( ce7d20...cfa95b )
by Fabrice
01:54
created

NodalFlow::uniqId()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 6
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
1
<?php
2
3
/*
4
 * This file is part of NodalFlow.
5
 *     (c) Fabrice de Stefanis / https://github.com/fab2s/NodalFlow
6
 * This source file is licensed under the MIT license which you will
7
 * find in the LICENSE file or at https://opensource.org/licenses/MIT
8
 */
9
10
namespace fab2s\NodalFlow;
11
12
use fab2s\NodalFlow\Callbacks\CallbackInterface;
13
use fab2s\NodalFlow\Flows\FlowInterface;
14
use fab2s\NodalFlow\Flows\FlowMap;
15
use fab2s\NodalFlow\Flows\FlowMapInterface;
16
use fab2s\NodalFlow\Flows\FlowStatus;
17
use fab2s\NodalFlow\Flows\FlowStatusInterface;
18
use fab2s\NodalFlow\Flows\InterrupterInterface;
19
use fab2s\NodalFlow\Nodes\BranchNodeInterface;
20
use fab2s\NodalFlow\Nodes\NodeInterface;
21
22
/**
23
 * Class NodalFlow
24
 */
25
class NodalFlow implements FlowInterface
26
{
27
    /**
28
     * Flow steps triggering callbacks
29
     */
30
    const FLOW_START    = 'start';
31
    const FLOW_PROGRESS = 'progress';
32
    const FLOW_SUCCESS  = 'success';
33
    const FLOW_FAIL     = 'fail';
34
35
    /**
36
     * The parent Flow, only set when branched
37
     *
38
     * @var FlowInterface
39
     */
40
    public $parent;
41
42
    /**
43
     * This Flow id
44
     *
45
     * @var string
46
     */
47
    protected $id;
48
49
    /**
50
     * The underlying node structure
51
     *
52
     * @var NodeInterface[]
53
     */
54
    protected $nodes = [];
55
56
    /**
57
     * The current Node index
58
     *
59
     * @var int
60
     */
61
    protected $nodeIdx = 0;
62
63
    /**
64
     * The last index value
65
     *
66
     * @var int
67
     */
68
    protected $lastIdx = 0;
69
70
    /**
71
     * The number of Node in this Flow
72
     *
73
     * @var int
74
     */
75
    protected $nodeCount = 0;
76
77
    /**
78
     * The number of iteration within this Flow
79
     *
80
     * @var int
81
     */
82
    protected $numIterate = 0;
83
84
    /**
85
     * The current registered Callback class if any
86
     *
87
     * @var CallbackInterface|null
88
     */
89
    protected $callBack;
90
91
    /**
92
     * Progress modulo to apply
93
     * Set to x if you want to trigger
94
     * progress every x iterations in flow
95
     *
96
     * @var int
97
     */
98
    protected $progressMod = 1024;
99
100
    /**
101
     * Continue flag
102
     *
103
     * @var bool
104
     */
105
    protected $continue = false;
106
107
    /**
108
     * Break Flag
109
     *
110
     * @var bool
111
     */
112
    protected $break = false;
113
114
    /**
115
     * Current Flow Status
116
     *
117
     * @var FlowStatusInterface
118
     */
119
    protected $flowStatus;
120
121
    /**
122
     * @var FlowMapInterface
123
     */
124
    protected $flowMap;
125
126
    /**
127
     * @var string|bool
128
     */
129
    protected $interruptNodeId;
130
131
    /**
132
     * Current nonce
133
     *
134
     * @var int
135
     */
136
    private static $nonce = 0;
137
138
    /**
139
     * Instantiate a Flow
140
     */
141
    public function __construct()
142
    {
143
        $this->id      = $this->uniqId();
144
        $this->flowMap = new FlowMap($this);
145
    }
146
147
    /**
148
     * Adds a Node to the flow
149
     *
150
     * @param NodeInterface $node
151
     *
152
     * @throws NodalFlowException
153
     *
154
     * @return $this
155
     */
156
    public function add(NodeInterface $node)
157
    {
158
        if ($node instanceof BranchNodeInterface) {
159
            // this node is a branch, set it's parent
160
            $node->getPayload()->setParent($this);
161
        }
162
163
        $node->setCarrier($this);
164
165
        $this->flowMap->register($node, $this->nodeIdx);
166
        $this->nodes[$this->nodeIdx] = $node;
167
        ++$this->nodeIdx;
168
169
        return $this;
170
    }
171
172
    /**
173
     * Adds a Payload Node to the Flow
174
     *
175
     * @param callable $payload
176
     * @param mixed    $isAReturningVal
177
     * @param mixed    $isATraversable
178
     *
179
     * @return $this
180
     */
181
    public function addPayload(callable $payload, $isAReturningVal, $isATraversable = false)
182
    {
183
        $node = PayloadNodeFactory::create($payload, $isAReturningVal, $isATraversable);
184
185
        $this->add($node);
186
187
        return $this;
188
    }
189
190
    /**
191
     * Register callback class
192
     *
193
     * @param CallbackInterface $callBack
194
     *
195
     * @return $this
196
     */
197
    public function setCallBack(CallbackInterface $callBack)
198
    {
199
        $this->callBack = $callBack;
200
201
        return $this;
202
    }
203
204
    /**
205
     * Used to set the eventual Node Target of an Interrupt signal
206
     * set to :
207
     * - A node hash to target
208
     * - true to interrupt every upstream nodes
209
     *     in this Flow
210
     * - false to only interrupt up to the first
211
     *     upstream Traversable in this Flow
212
     *
213
     * @param string|bool $interruptNodeId
214
     *
215
     * @return $this
216
     */
217
    public function setInterruptNodeId($interruptNodeId)
218
    {
219
        $this->interruptNodeId = $interruptNodeId;
220
221
        return $this;
222
    }
223
224
    /**
225
     * Set parent Flow, happens only when branched
226
     *
227
     * @param FlowInterface $flow
228
     *
229
     * @return $this
230
     */
231
    public function setParent(FlowInterface $flow)
232
    {
233
        $this->parent = $flow;
234
235
        return $this;
236
    }
237
238
    /**
239
     * Get eventual parent Flow
240
     *
241
     * @return FlowInterface
242
     */
243
    public function getParent()
244
    {
245
        return $this->parent;
246
    }
247
248
    /**
249
     * Tells if this flow has a parent
250
     *
251
     * @return bool
252
     */
253
    public function hasParent()
254
    {
255
        return !empty($this->parent);
256
    }
257
258
    /**
259
     * Generates a truly unique id for the Flow context
260
     *
261
     * @return string
262
     */
263
    public function uniqId()
264
    {
265
        // while we're at it, drop any doubt about
266
        // colliding from here
267
        return \sha1(uniqid() . $this->getNonce());
268
    }
269
270
    /**
271
     * Execute the flow
272
     *
273
     * @param null|mixed $param The eventual init argument to the first node
274
     *                          or, in case of a branch, the last relevant
275
     *                          argument from upstream Flow
276
     *
277
     * @throws NodalFlowException
278
     *
279
     * @return mixed the last result of the
280
     *               last returning value node
281
     */
282
    public function exec($param = null)
283
    {
284
        try {
285
            $result = $this->rewind()
286
                    ->flowStart()
287
                    ->recurse($param);
288
289
            $this->flowMap->flowEnd();
290
            // set flowStatus to make sure that we have the proper
291
            // value in flowEnd even when overridden without (or when
292
            // improperly) calling parent
293
            if ($this->flowStatus->isRunning()) {
294
                $this->flowStatus = new FlowStatus(FlowStatus::FLOW_CLEAN);
295
            }
296
297
            $this->flowEnd();
298
299
            return $result;
300
        } catch (\Exception $e) {
301
            $this->flowStatus = new FlowStatus(FlowStatus::FLOW_EXCEPTION);
302
            $this->flowMap->flowEnd();
303
            $this->flowEnd();
304
            if ($e instanceof NodalFlowException) {
305
                throw $e;
306
            }
307
308
            throw new NodalFlowException('Flow execution failed', 0, $e, [
309
                'nodeMap' => $this->getNodeMap(),
310
            ]);
311
        }
312
    }
313
314
    /**
315
     * Computes a human readable duration string from floating seconds
316
     *
317
     * @param float $seconds
318
     *
319
     * @return array<string,integer|string>
320
     */
321
    public function duration($seconds)
322
    {
323
        $result = [
324
            'hour'     => (int) \floor($seconds / 3600),
325
            'min'      => (int) \floor(($seconds / 60) % 60),
326
            'sec'      => $seconds % 60,
327
            'ms'       => (int) \round(\fmod($seconds, 1) * 1000),
328
        ];
329
330
        $durationStr = '';
331
        foreach ($result as $unit => $value) {
332
            if (!empty($value)) {
333
                $durationStr .= $value . "$unit ";
334
            }
335
        }
336
337
        $result['durationStr'] = \trim($durationStr);
338
339
        return $result;
340
    }
341
342
    /**
343
     * Get the stats array with latest Node stats
344
     *
345
     * @return array
346
     */
347
    public function getStats()
348
    {
349
        return $this->flowMap->getStats();
350
    }
351
352
    /**
353
     * Return the Flow id as set during instantiation
354
     *
355
     * @return string
356
     */
357
    public function getId()
358
    {
359
        return $this->id;
360
    }
361
362
    /**
363
     * getId() alias for backward compatibility
364
     *
365
     * @deprecated
366
     *
367
     * @return string
368
     */
369
    public function getFlowId()
370
    {
371
        return $this->getId();
372
    }
373
374
    /**
375
     * Get the Node array
376
     *
377
     * @return NodeInterface[]
378
     */
379
    public function getNodes()
380
    {
381
        return $this->nodes;
382
    }
383
384
    /**
385
     * Get/Generate Node Map
386
     *
387
     * @return array
388
     */
389
    public function getNodeMap()
390
    {
391
        return $this->flowMap->getNodeMap();
392
    }
393
394
    /**
395
     * Rewinds the Flow
396
     *
397
     * @return $this
398
     */
399
    public function rewind()
400
    {
401
        $this->nodeCount       = count($this->nodes);
402
        $this->lastIdx         = $this->nodeCount - 1;
403
        $this->break           = false;
404
        $this->continue        = false;
405
        $this->interruptNodeId = null;
406
407
        return $this;
408
    }
409
410
    /**
411
     * Define the progress modulo, Progress Callback will be
412
     * triggered upon each iteration in the flow modulo $progressMod
413
     *
414
     * @param int $progressMod
415
     *
416
     * @return $this
417
     */
418
    public function setProgressMod($progressMod)
419
    {
420
        $this->progressMod = max(1, (int) $progressMod);
421
422
        return $this;
423
    }
424
425
    /**
426
     * Get current $progressMod
427
     *
428
     * @return int
429
     */
430
    public function getProgressMod()
431
    {
432
        return $this->progressMod;
433
    }
434
435
    /**
436
     * The Flow status can either indicate be:
437
     *      - clean (isClean()): everything went well
438
     *      - dirty (isDirty()): one Node broke the flow
439
     *      - exception (isException()): an exception was raised during the flow
440
     *
441
     * @return FlowStatusInterface
442
     */
443
    public function getFlowStatus()
444
    {
445
        return $this->flowStatus;
446
    }
447
448
    /**
449
     * Break the flow's execution, conceptually similar to breaking
450
     * a regular loop
451
     *
452
     * @param InterrupterInterface|null $flowInterrupt
453
     *
454
     * @return $this
455
     */
456
    public function breakFlow(InterrupterInterface $flowInterrupt = null)
457
    {
458
        return $this->interruptFlow(InterrupterInterface::TYPE_BREAK, $flowInterrupt);
459
    }
460
461
    /**
462
     * Continue the flow's execution, conceptually similar to continuing
463
     * a regular loop
464
     *
465
     * @param InterrupterInterface|null $flowInterrupt
466
     *
467
     * @return $this
468
     */
469
    public function continueFlow(InterrupterInterface $flowInterrupt = null)
470
    {
471
        return $this->interruptFlow(InterrupterInterface::TYPE_CONTINUE, $flowInterrupt);
472
    }
473
474
    /**
475
     * @param string                    $interruptType
476
     * @param InterrupterInterface|null $flowInterrupt
477
     *
478
     * @throws NodalFlowException
479
     *
480
     * @return $this
481
     */
482
    public function interruptFlow($interruptType, InterrupterInterface $flowInterrupt = null)
483
    {
484
        switch ($interruptType) {
485
            case InterrupterInterface::TYPE_CONTINUE:
486
                $this->continue = true;
487
                $this->flowMap->incrementFlow('num_continue');
488
                break;
489
            case InterrupterInterface::TYPE_BREAK:
490
                $this->flowStatus = new FlowStatus(FlowStatus::FLOW_DIRTY);
491
                $this->break      = true;
492
                $this->flowMap->incrementFlow('num_break');
493
                break;
494
            default:
495
                throw new NodalFlowException('FlowInterrupt Type missing');
496
        }
497
498
        if ($flowInterrupt) {
499
            $flowInterrupt->setType($interruptType)->propagate($this);
500
        }
501
502
        return $this;
503
    }
504
505
    /**
506
     * @param NodeInterface $node
507
     *
508
     * @return bool
509
     */
510
    protected function interruptNode(NodeInterface $node)
511
    {
512
        // if we have an interruptNodeId, bubble up until we match a node
513
        // else stop propagation
514
        return $this->interruptNodeId ? $this->interruptNodeId !== $node->getNodeHash() : false;
515
    }
516
517
    /**
518
     * Triggered just before the flow starts
519
     *
520
     * @return $this
521
     */
522
    protected function flowStart()
523
    {
524
        $this->flowMap->incrementFlow('num_exec')->flowStart();
525
        $this->triggerCallback(static::FLOW_START);
526
527
        // flow is started
528
        $this->flowStatus = new FlowStatus(FlowStatus::FLOW_RUNNING);
529
530
        return $this;
531
    }
532
533
    /**
534
     * Triggered right after the flow stops
535
     *
536
     * @return $this
537
     */
538
    protected function flowEnd()
539
    {
540
        $this->flowMap->flowEnd();
541
542
        $this->triggerCallback($this->flowStatus->isException() ? static::FLOW_FAIL : static::FLOW_SUCCESS);
543
544
        return $this;
545
    }
546
547
    /**
548
     * Return a simple nonce, fully valid within any flow
549
     *
550
     * @return int
551
     */
552
    protected function getNonce()
553
    {
554
        return self::$nonce++;
555
    }
556
557
    /**
558
     * Recurse over nodes which may as well be Flows and
559
     * Traversable ...
560
     * Welcome to the abysses of recursion or iter-recursion ^^
561
     *
562
     * `recurse` perform kind of an hybrid recursion as the
563
     * Flow is effectively iterating and recurring over its
564
     * Nodes, which may as well be seen as over itself
565
     *
566
     * Iterating tends to limit the amount of recursion levels:
567
     * recursion is only triggered when executing a Traversable
568
     * Node's downstream Nodes while every consecutive exec
569
     * Nodes are executed within a while loop.
570
     * And recursion keeps the size of the recursion context
571
     * to a minimum as pretty much everything is done by the
572
     * iterating instance
573
     *
574
     * @param mixed $param
575
     * @param int   $nodeIdx
576
     *
577
     * @return mixed the last value returned by the last
578
     *               returning value Node in the flow
579
     */
580
    protected function recurse($param = null, $nodeIdx = 0)
581
    {
582
        while ($nodeIdx <= $this->lastIdx) {
583
            $node      = $this->nodes[$nodeIdx];
584
            $nodeHash  = $node->getNodeHash();
585
            $returnVal = $node->isReturningVal();
586
587
            if ($node->isTraversable()) {
588
                foreach ($node->getTraversable($param) as $value) {
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface fab2s\NodalFlow\Nodes\NodeInterface as the method getTraversable() does only exist in the following implementations of said interface: fab2s\NodalFlow\Nodes\AggregateNode, fab2s\NodalFlow\Nodes\CallableNode, fab2s\NodalFlow\Nodes\ClosureNode.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
589
                    if ($returnVal) {
590
                        // pass current $value as next param
591
                        $param = $value;
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $param. This often makes code more readable.
Loading history...
592
                    }
593
594
                    $this->flowMap->increment($nodeHash, 'num_iterate')->incrementFlow('num_iterate');
595
                    ++$this->numIterate;
596
                    if (!($this->numIterate % $this->progressMod)) {
597
                        $this->triggerCallback(static::FLOW_PROGRESS, $node);
598
                    }
599
600
                    $param = $this->recurse($param, $nodeIdx + 1);
601 View Code Duplication
                    if ($this->continue) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
602
                        if ($this->continue = $this->interruptNode($node)) {
603
                            // since we want to bubble the continue upstream
604
                            // we break here waiting for next $param if any
605
                            $this->flowMap->increment($nodeHash, 'num_break');
606
                            break;
607
                        }
608
609
                        // we drop one iteration
610
                        $this->flowMap->increment($nodeHash, 'num_continue');
611
                        continue;
612
                    }
613
614
                    if ($this->break) {
615
                        // we drop all subsequent iterations
616
                        $this->flowMap->increment($nodeHash, 'num_break');
617
                        $this->break = $this->interruptNode($node);
618
                        break;
619
                    }
620
                }
621
622
                // we reached the end of this Traversable and executed all its downstream Nodes
623
                $this->flowMap->increment($nodeHash, 'num_exec');
624
625
                return $param;
626
            }
627
628
            $value = $node->exec($param);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface fab2s\NodalFlow\Nodes\NodeInterface as the method exec() does only exist in the following implementations of said interface: fab2s\NodalFlow\Nodes\BranchNode, fab2s\NodalFlow\Nodes\CallableInterruptNode, fab2s\NodalFlow\Nodes\CallableNode, fab2s\NodalFlow\Nodes\ClosureNode, fab2s\NodalFlow\Nodes\InterruptNodeAbstract.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
629
            $this->flowMap->increment($nodeHash, 'num_exec');
630
631 View Code Duplication
            if ($this->continue) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
632
                $this->flowMap->increment($nodeHash, 'num_continue');
633
                // a continue does not need to bubble up unless
634
                // it specifically targets a node in this flow
635
                // or targets an upstream flow
636
                $this->continue = $this->interruptNode($node);
637
638
                return $param;
639
            }
640
641
            if ($this->break) {
642
                $this->flowMap->increment($nodeHash, 'num_break');
643
                // a break always need to bubble up to the first upstream Traversable if any
644
                return $param;
645
            }
646
647
            if ($returnVal) {
648
                // pass current $value as next param
649
                $param = $value;
650
            }
651
652
            ++$nodeIdx;
653
        }
654
655
        // we reached the end of this recursion
656
        return $param;
657
    }
658
659
    /**
660
     * KISS helper to trigger Callback slots
661
     *
662
     * @param string             $which
663
     * @param null|NodeInterface $node
664
     *
665
     * @return $this
666
     */
667
    protected function triggerCallback($which, NodeInterface $node = null)
668
    {
669
        if (null !== $this->callBack) {
670
            $this->callBack->$which($this, $node);
671
        }
672
673
        return $this;
674
    }
675
}
676