Completed
Push — master ( cfa95b...fcac32 )
by Fabrice
01:49
created

NodalFlow::duration()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 20
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 20
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 12
nc 3
nop 1

1 Method

Rating   Name   Duplication   Size   Complexity  
A NodalFlow::getId() 0 4 1
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
            // set flowStatus to make sure that we have the proper
289
            // value in flowEnd even when overridden without (or when
290
            // improperly) calling parent
291
            if ($this->flowStatus->isRunning()) {
292
                $this->flowStatus = new FlowStatus(FlowStatus::FLOW_CLEAN);
293
            }
294
295
            $this->flowEnd();
296
297
            return $result;
298
        } catch (\Exception $e) {
299
            $this->flowStatus = new FlowStatus(FlowStatus::FLOW_EXCEPTION);
300
            $this->flowEnd();
301
            if ($e instanceof NodalFlowException) {
302
                throw $e;
303
            }
304
305
            throw new NodalFlowException('Flow execution failed', 0, $e, [
306
                'nodeMap' => $this->getNodeMap(),
307
            ]);
308
        }
309
    }
310
311
    /**
312
     * Get the stats array with latest Node stats
313
     *
314
     * @return array
315
     */
316
    public function getStats()
317
    {
318
        return $this->flowMap->getStats();
319
    }
320
321
    /**
322
     * Return the Flow id as set during instantiation
323
     *
324
     * @return string
325
     */
326
    public function getId()
327
    {
328
        return $this->id;
329
    }
330
331
    /**
332
     * getId() alias for backward compatibility
333
     *
334
     * @deprecated
335
     *
336
     * @return string
337
     */
338
    public function getFlowId()
339
    {
340
        return $this->getId();
341
    }
342
343
    /**
344
     * Get the Node array
345
     *
346
     * @return NodeInterface[]
347
     */
348
    public function getNodes()
349
    {
350
        return $this->nodes;
351
    }
352
353
    /**
354
     * Get/Generate Node Map
355
     *
356
     * @return array
357
     */
358
    public function getNodeMap()
359
    {
360
        return $this->flowMap->getNodeMap();
361
    }
362
363
    /**
364
     * Rewinds the Flow
365
     *
366
     * @return $this
367
     */
368
    public function rewind()
369
    {
370
        $this->nodeCount       = count($this->nodes);
371
        $this->lastIdx         = $this->nodeCount - 1;
372
        $this->break           = false;
373
        $this->continue        = false;
374
        $this->interruptNodeId = null;
375
376
        return $this;
377
    }
378
379
    /**
380
     * Define the progress modulo, Progress Callback will be
381
     * triggered upon each iteration in the flow modulo $progressMod
382
     *
383
     * @param int $progressMod
384
     *
385
     * @return $this
386
     */
387
    public function setProgressMod($progressMod)
388
    {
389
        $this->progressMod = max(1, (int) $progressMod);
390
391
        return $this;
392
    }
393
394
    /**
395
     * Get current $progressMod
396
     *
397
     * @return int
398
     */
399
    public function getProgressMod()
400
    {
401
        return $this->progressMod;
402
    }
403
404
    /**
405
     * The Flow status can either indicate be:
406
     *      - clean (isClean()): everything went well
407
     *      - dirty (isDirty()): one Node broke the flow
408
     *      - exception (isException()): an exception was raised during the flow
409
     *
410
     * @return FlowStatusInterface
411
     */
412
    public function getFlowStatus()
413
    {
414
        return $this->flowStatus;
415
    }
416
417
    /**
418
     * Break the flow's execution, conceptually similar to breaking
419
     * a regular loop
420
     *
421
     * @param InterrupterInterface|null $flowInterrupt
422
     *
423
     * @return $this
424
     */
425
    public function breakFlow(InterrupterInterface $flowInterrupt = null)
426
    {
427
        return $this->interruptFlow(InterrupterInterface::TYPE_BREAK, $flowInterrupt);
428
    }
429
430
    /**
431
     * Continue the flow's execution, conceptually similar to continuing
432
     * a regular loop
433
     *
434
     * @param InterrupterInterface|null $flowInterrupt
435
     *
436
     * @return $this
437
     */
438
    public function continueFlow(InterrupterInterface $flowInterrupt = null)
439
    {
440
        return $this->interruptFlow(InterrupterInterface::TYPE_CONTINUE, $flowInterrupt);
441
    }
442
443
    /**
444
     * @param string                    $interruptType
445
     * @param InterrupterInterface|null $flowInterrupt
446
     *
447
     * @throws NodalFlowException
448
     *
449
     * @return $this
450
     */
451
    public function interruptFlow($interruptType, InterrupterInterface $flowInterrupt = null)
452
    {
453
        switch ($interruptType) {
454
            case InterrupterInterface::TYPE_CONTINUE:
455
                $this->continue = true;
456
                $this->flowMap->incrementFlow('num_continue');
457
                break;
458
            case InterrupterInterface::TYPE_BREAK:
459
                $this->flowStatus = new FlowStatus(FlowStatus::FLOW_DIRTY);
460
                $this->break      = true;
461
                $this->flowMap->incrementFlow('num_break');
462
                break;
463
            default:
464
                throw new NodalFlowException('FlowInterrupt Type missing');
465
        }
466
467
        if ($flowInterrupt) {
468
            $flowInterrupt->setType($interruptType)->propagate($this);
469
        }
470
471
        return $this;
472
    }
473
474
    /**
475
     * @param NodeInterface $node
476
     *
477
     * @return bool
478
     */
479
    protected function interruptNode(NodeInterface $node)
480
    {
481
        // if we have an interruptNodeId, bubble up until we match a node
482
        // else stop propagation
483
        return $this->interruptNodeId ? $this->interruptNodeId !== $node->getNodeHash() : false;
484
    }
485
486
    /**
487
     * Triggered just before the flow starts
488
     *
489
     * @return $this
490
     */
491
    protected function flowStart()
492
    {
493
        $this->flowMap->incrementFlow('num_exec')->flowStart();
494
        $this->triggerCallback(static::FLOW_START);
495
496
        // flow is started
497
        $this->flowStatus = new FlowStatus(FlowStatus::FLOW_RUNNING);
498
499
        return $this;
500
    }
501
502
    /**
503
     * Triggered right after the flow stops
504
     *
505
     * @return $this
506
     */
507
    protected function flowEnd()
508
    {
509
        $this->flowMap->flowEnd();
510
511
        $this->triggerCallback($this->flowStatus->isException() ? static::FLOW_FAIL : static::FLOW_SUCCESS);
512
513
        return $this;
514
    }
515
516
    /**
517
     * Return a simple nonce, fully valid within any flow
518
     *
519
     * @return int
520
     */
521
    protected function getNonce()
522
    {
523
        return self::$nonce++;
524
    }
525
526
    /**
527
     * Recurse over nodes which may as well be Flows and
528
     * Traversable ...
529
     * Welcome to the abysses of recursion or iter-recursion ^^
530
     *
531
     * `recurse` perform kind of an hybrid recursion as the
532
     * Flow is effectively iterating and recurring over its
533
     * Nodes, which may as well be seen as over itself
534
     *
535
     * Iterating tends to limit the amount of recursion levels:
536
     * recursion is only triggered when executing a Traversable
537
     * Node's downstream Nodes while every consecutive exec
538
     * Nodes are executed within a while loop.
539
     * And recursion keeps the size of the recursion context
540
     * to a minimum as pretty much everything is done by the
541
     * iterating instance
542
     *
543
     * @param mixed $param
544
     * @param int   $nodeIdx
545
     *
546
     * @return mixed the last value returned by the last
547
     *               returning value Node in the flow
548
     */
549
    protected function recurse($param = null, $nodeIdx = 0)
550
    {
551
        while ($nodeIdx <= $this->lastIdx) {
552
            $node      = $this->nodes[$nodeIdx];
553
            $nodeHash  = $node->getNodeHash();
554
            $returnVal = $node->isReturningVal();
555
556
            if ($node->isTraversable()) {
557
                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...
558
                    if ($returnVal) {
559
                        // pass current $value as next param
560
                        $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...
561
                    }
562
563
                    $this->flowMap->increment($nodeHash, 'num_iterate')->incrementFlow('num_iterate');
564
                    ++$this->numIterate;
565
                    if (!($this->numIterate % $this->progressMod)) {
566
                        $this->triggerCallback(static::FLOW_PROGRESS, $node);
567
                    }
568
569
                    $param = $this->recurse($param, $nodeIdx + 1);
570 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...
571
                        if ($this->continue = $this->interruptNode($node)) {
572
                            // since we want to bubble the continue upstream
573
                            // we break here waiting for next $param if any
574
                            $this->flowMap->increment($nodeHash, 'num_break');
575
                            break;
576
                        }
577
578
                        // we drop one iteration
579
                        $this->flowMap->increment($nodeHash, 'num_continue');
580
                        continue;
581
                    }
582
583
                    if ($this->break) {
584
                        // we drop all subsequent iterations
585
                        $this->flowMap->increment($nodeHash, 'num_break');
586
                        $this->break = $this->interruptNode($node);
587
                        break;
588
                    }
589
                }
590
591
                // we reached the end of this Traversable and executed all its downstream Nodes
592
                $this->flowMap->increment($nodeHash, 'num_exec');
593
594
                return $param;
595
            }
596
597
            $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...
598
            $this->flowMap->increment($nodeHash, 'num_exec');
599
600 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...
601
                $this->flowMap->increment($nodeHash, 'num_continue');
602
                // a continue does not need to bubble up unless
603
                // it specifically targets a node in this flow
604
                // or targets an upstream flow
605
                $this->continue = $this->interruptNode($node);
606
607
                return $param;
608
            }
609
610
            if ($this->break) {
611
                $this->flowMap->increment($nodeHash, 'num_break');
612
                // a break always need to bubble up to the first upstream Traversable if any
613
                return $param;
614
            }
615
616
            if ($returnVal) {
617
                // pass current $value as next param
618
                $param = $value;
619
            }
620
621
            ++$nodeIdx;
622
        }
623
624
        // we reached the end of this recursion
625
        return $param;
626
    }
627
628
    /**
629
     * KISS helper to trigger Callback slots
630
     *
631
     * @param string             $which
632
     * @param null|NodeInterface $node
633
     *
634
     * @return $this
635
     */
636
    protected function triggerCallback($which, NodeInterface $node = null)
637
    {
638
        if (null !== $this->callBack) {
639
            $this->callBack->$which($this, $node);
640
        }
641
642
        return $this;
643
    }
644
}
645