Completed
Push — master ( ddc20e...61f12d )
by Fabrice
03:08
created

NodalFlow::addPayload()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 8
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 4
nc 1
nop 3
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\FlowStatus;
15
use fab2s\NodalFlow\Flows\FlowStatusInterface;
16
use fab2s\NodalFlow\Nodes\AggregateNodeInterface;
17
use fab2s\NodalFlow\Nodes\BranchNode;
18
use fab2s\NodalFlow\Nodes\BranchNodeInterface;
19
use fab2s\NodalFlow\Nodes\NodeInterface;
20
21
/**
22
 * Class NodalFlow
23
 */
24
class NodalFlow implements FlowInterface
25
{
26
    /**
27
     * Flow steps triggering callbacks
28
     */
29
    const FLOW_START    = 'start';
30
    const FLOW_PROGRESS = 'progress';
31
    const FLOW_SUCCESS  = 'success';
32
    const FLOW_FAIL     = 'fail';
33
34
    /**
35
     * The parent Flow, only set when branched
36
     *
37
     * @var FlowInterface
38
     */
39
    public $parent;
40
41
    /**
42
     * This Flow id
43
     *
44
     * @var string
45
     */
46
    protected $id;
47
48
    /**
49
     * The underlying node structure
50
     *
51
     * @var NodeInterface[]
52
     */
53
    protected $nodes = [];
54
55
    /**
56
     * The current Node index
57
     *
58
     * @var int
59
     */
60
    protected $nodeIdx = 0;
61
62
    /**
63
     * The last index value
64
     *
65
     * @var int
66
     */
67
    protected $lastIdx = 0;
68
69
    /**
70
     * The number of Node in this Flow
71
     *
72
     * @var int
73
     */
74
    protected $nodeCount = 0;
75
76
    /**
77
     * The number of iteration within this Flow
78
     *
79
     * @var int
80
     */
81
    protected $numIterate = 0;
82
83
    /**
84
     * The number of break within this Flow
85
     *
86
     * @var int
87
     */
88
    protected $numBreak = 0;
89
90
    /**
91
     * The number of continue within this Flow
92
     *
93
     * @var int
94
     */
95
    protected $numContinue = 0;
96
97
    /**
98
     * The current registered Callback class if any
99
     *
100
     * @var CallbackInterface|null
101
     */
102
    protected $callBack;
103
104
    /**
105
     * Progress modulo to apply
106
     * Set to x if you want to trigger
107
     * progress every x iterations in flow
108
     *
109
     * @var int
110
     */
111
    protected $progressMod = 1024;
112
113
    /**
114
     * The default Node Map values
115
     *
116
     * @var array
117
     */
118
    protected $nodeMapDefault = [
119
        'class'        => null,
120
        'branchId'     => null,
121
        'hash'         => null,
122
        'index'        => 0,
123
        'num_exec'     => 0,
124
        'num_iterate'  => 0,
125
        'num_break'    => 0,
126
        'num_continue' => 0,
127
    ];
128
129
    /**
130
     * The default Node stats values
131
     *
132
     * @var array
133
     */
134
    protected $nodeStatsDefault = [
135
        'num_exec'     => 0,
136
        'num_iterate'  => 0,
137
        'num_break'    => 0,
138
        'num_continue' => 0,
139
    ];
140
141
    /**
142
     * Node stats values
143
     *
144
     * @var array
145
     */
146
    protected $nodeStats = [];
147
148
    /**
149
     * The object map, used to enforce object unicity within the Flow
150
     *
151
     * @var array
152
     */
153
    protected $objectMap = [];
154
155
    /**
156
     * The Node Map
157
     *
158
     * @var array
159
     */
160
    protected $nodeMap = [];
161
162
    /**
163
     * The Flow stats default values
164
     *
165
     * @var array
166
     */
167
    protected $statsDefault = [
168
        'start'    => null,
169
        'end'      => null,
170
        'duration' => null,
171
        'mib'      => null,
172
    ];
173
174
    /**
175
     * The Flow Stats
176
     *
177
     * @var array
178
     */
179
    protected $stats = [
180
        'invocations' => [],
181
    ];
182
183
    /**
184
     * Number of exec calls in this Flow
185
     *
186
     * @var int
187
     */
188
    protected $numExec = 0;
189
190
    /**
191
     * Continue flag
192
     *
193
     * @var bool
194
     */
195
    protected $continue = false;
196
197
    /**
198
     * Break Flag
199
     *
200
     * @var bool
201
     */
202
    protected $break = false;
203
204
    /**
205
     * Current Flow Status
206
     *
207
     * @var FlowStatusInterface
208
     */
209
    protected $flowStatus;
210
211
    /**
212
     * @var string
213
     */
214
    protected $interruptNodeId;
215
216
    /**
217
     * Current nonce
218
     *
219
     * @var int
220
     */
221
    private static $nonce = 0;
222
223
    /**
224
     * Instantiate a Flow
225
     */
226
    public function __construct()
227
    {
228
        $this->id = $this->uniqId();
229
        $this->stats += $this->statsDefault;
230
    }
231
232
    /**
233
     * Adds a Node to the flow
234
     *
235
     * @param NodeInterface $node
236
     *
237
     * @throws NodalFlowException
238
     *
239
     * @return $this
240
     */
241
    public function add(NodeInterface $node)
242
    {
243
        $nodeHash = $node->getNodeHash();
244
245
        if (isset($this->nodeMap[$nodeHash])) {
246
            throw new NodalFlowException('Cannot reuse Node instances within a Flow', 1, null, [
247
                'duplicate_node' => get_class($node),
248
                'hash'           => $nodeHash,
249
            ]);
250
        }
251
252
        if ($node instanceof BranchNodeInterface) {
253
            // this node is a branch, set it's parent
254
            $node->getPayload()->setParent($this);
255
        }
256
257
        $node->setCarrier($this);
258
259
        $this->nodes[$this->nodeIdx] = $node;
260
        $this->nodeMap[$nodeHash]    = \array_replace($this->nodeMapDefault, [
261
            'class'           => \get_class($node),
262
            'branchId'        => $this->id,
263
            'hash'            => $nodeHash,
264
            'index'           => $this->nodeIdx,
265
            'isATraversable'  => $node->isTraversable(),
266
            'isAReturningVal' => $node->isReturningVal(),
267
            'isAFlow'         => $node->isFlow(),
268
        ]);
269
270
        // register references to nodeStats to increment faster
271
        // nodeStats can also be used as reverse lookup table
272
        $this->nodeStats[$this->nodeIdx] = &$this->nodeMap[$nodeHash];
273
274
        ++$this->nodeIdx;
275
276
        return $this;
277
    }
278
279
    /**
280
     * Adds a Payload Node to the Flow
281
     *
282
     * @param callable $payload
283
     * @param mixed    $isAReturningVal
284
     * @param mixed    $isATraversable
285
     *
286
     * @return $this
287
     */
288
    public function addPayload(callable $payload, $isAReturningVal, $isATraversable = false)
289
    {
290
        $node = PayloadNodeFactory::create($payload, $isAReturningVal, $isATraversable);
291
292
        $this->add($node);
293
294
        return $this;
295
    }
296
297
    /**
298
     * Register callback class
299
     *
300
     * @param CallbackInterface $callBack
301
     *
302
     * @return $this
303
     */
304
    public function setCallBack(CallbackInterface $callBack)
305
    {
306
        $this->callBack = $callBack;
307
308
        return $this;
309
    }
310
311
    /**
312
     * Used to set the eventual Node Target of an Interrupt signal
313
     * set to :
314
     * - A node hash to target
315
     * - true to interrupt every upstream nodes
316
     *     in this Flow
317
     * - false to only interrupt up to the first
318
     *     upstream Traversable in this Flow
319
     *
320
     * @param string|bool $interruptNodeId
321
     *
322
     * @return $this
323
     */
324
    public function setInterruptNodeId($interruptNodeId)
325
    {
326
        $this->interruptNodeId = $interruptNodeId;
0 ignored issues
show
Documentation Bug introduced by
It seems like $interruptNodeId can also be of type boolean. However, the property $interruptNodeId is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
327
328
        return $this;
329
    }
330
331
    /**
332
     * Set parent Flow, happens only when branched
333
     *
334
     * @param FlowInterface $flow
335
     *
336
     * @return $this
337
     */
338
    public function setParent(FlowInterface $flow)
339
    {
340
        $this->parent = $flow;
341
342
        return $this;
343
    }
344
345
    /**
346
     * Get eventual parent Flow
347
     *
348
     * @return FlowInterface
349
     */
350
    public function getParent()
351
    {
352
        return $this->parent;
353
    }
354
355
    /**
356
     * Tells if this flow has a parent
357
     *
358
     * @return bool
359
     */
360
    public function hasParent()
361
    {
362
        return !empty($this->parent);
363
    }
364
365
    /**
366
     * Generates a truly unique id for the Flow context
367
     *
368
     * @return string
369
     */
370
    public function uniqId()
371
    {
372
        // while we're at it, drop any doubt about
373
        // colliding from here
374
        return \sha1(uniqid() . $this->getNonce());
375
    }
376
377
    /**
378
     * Execute the flow
379
     *
380
     * @param null|mixed $param The eventual init argument to the first node
381
     *                          or, in case of a branch, the last relevant
382
     *                          argument from upstream Flow
383
     *
384
     * @throws NodalFlowException
385
     *
386
     * @return mixed the last result of the
387
     *               last returning value node
388
     */
389
    public function exec($param = null)
390
    {
391
        try {
392
            $result = $this->rewind()
393
                    ->flowStart()
394
                    ->recurse($param);
395
            // set flowStatus to make sure that we have the proper
396
            // value in flowEnd even when overridden without (or when
397
            // improperly) calling parent
398
            if ($this->flowStatus->isRunning()) {
399
                $this->flowStatus = new FlowStatus(FlowStatus::FLOW_CLEAN);
400
            }
401
402
            $this->flowEnd();
403
404
            return $result;
405
        } catch (\Exception $e) {
406
            $this->flowStatus = new FlowStatus(FlowStatus::FLOW_EXCEPTION);
407
            $this->flowEnd();
408
            if ($e instanceof NodalFlowException) {
409
                throw $e;
410
            }
411
412
            throw new NodalFlowException('Flow execution failed', 0, $e, [
413
                'nodeMap' => $this->getNodeMap(),
414
            ]);
415
        }
416
    }
417
418
    /**
419
     * Computes a human readable duration string from floating seconds
420
     *
421
     * @param float $seconds
422
     *
423
     * @return array
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use array<string,integer|string>.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
424
     */
425
    public function duration($seconds)
426
    {
427
        $result = [
428
            'hour'     => (int) \floor($seconds / 3600),
429
            'min'      => (int) \floor(($seconds / 60) % 60),
430
            'sec'      => $seconds % 60,
431
            'ms'       => (int) \round(\fmod($seconds, 1) * 1000),
432
        ];
433
434
        $durationStr = '';
435
        foreach ($result as $unit => $value) {
436
            if (!empty($value)) {
437
                $durationStr .= $value . "$unit ";
438
            }
439
        }
440
441
        $result['durationStr'] = \trim($durationStr);
442
443
        return $result;
444
    }
445
446
    /**
447
     * Resets Nodes stats, can be used prior to Flow's re-exec
448
     *
449
     * @return $this
450
     */
451
    public function resetNodeStats()
452
    {
453
        foreach ($this->nodeStats as &$nodeStat) {
454
            $nodeStat = \array_replace($nodeStat, $this->nodeStatsDefault);
455
        }
456
457
        return $this;
458
    }
459
460
    /**
461
     * Get the stats array with latest Node stats
462
     *
463
     * @return array
464
     */
465
    public function getStats()
466
    {
467
        foreach ($this->nodes as $node) {
468
            if (\is_a($node, BranchNode::class)) {
469
                $this->stats['branches'][$node->getPayload()->getFlowId()] = $node->getPayload()->getStats();
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 getPayload() does only exist in the following implementations of said interface: fab2s\NodalFlow\Nodes\BranchNode, fab2s\NodalFlow\Nodes\CallableNode, fab2s\NodalFlow\Nodes\ClosureNode, fab2s\NodalFlow\Nodes\PayloadNodeAbstract.

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...
470
            }
471
        }
472
473
        return $this->stats;
474
    }
475
476
    /**
477
     * Return the Flow id as set during instantiation
478
     *
479
     * @return string
480
     */
481
    public function getId()
482
    {
483
        return $this->id;
484
    }
485
486
    /**
487
     * getId() alias for backward compatibility
488
     *
489
     * @deprecated
490
     *
491
     * @return string
492
     */
493
    public function getFlowId()
494
    {
495
        return $this->getId();
496
    }
497
498
    /**
499
     * Get the Node array
500
     *
501
     * @return NodeInterface[]
502
     */
503
    public function getNodes()
504
    {
505
        return $this->nodes;
506
    }
507
508
    /**
509
     * Generate Node Map
510
     *
511
     * @return array
512
     */
513
    public function getNodeMap()
514
    {
515
        foreach ($this->nodes as $node) {
516
            if (\is_a($node, BranchNode::class)) {
517
                $this->nodeMap[$node->getNodeHash()]['nodes'] = $node->getPayload()->getNodeMap();
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 getPayload() does only exist in the following implementations of said interface: fab2s\NodalFlow\Nodes\BranchNode, fab2s\NodalFlow\Nodes\CallableNode, fab2s\NodalFlow\Nodes\ClosureNode, fab2s\NodalFlow\Nodes\PayloadNodeAbstract.

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...
518
                continue;
519
            }
520
521
            if ($node instanceof AggregateNodeInterface) {
522
                foreach ($node->getNodeCollection() as $aggregatedNode) {
523
                    $this->nodeMap[$node->getNodeHash()]['nodes'][$aggregatedNode->getNodeHash()] = [
524
                        'class' => \get_class($aggregatedNode),
525
                        'hash'  => $aggregatedNode->getNodeHash(),
526
                    ];
527
                }
528
                continue;
529
            }
530
        }
531
532
        return $this->nodeMap;
533
    }
534
535
    /**
536
     * Get the Node stats
537
     *
538
     * @return array
539
     */
540
    public function getNodeStats()
541
    {
542
        foreach ($this->nodes as $nodeIdx => $node) {
543
            if ($node instanceof BranchNodeInterface) {
544
                $payload = $node->getPayload();
545
                // The plan would be to extract the stat logic from here rather
546
                // than adding method to the interface
547
                $this->nodeStats[$nodeIdx]['nodes'] = is_callable([$payload, 'getNodeStats']) ? $payload->getNodeStats() : 'N/A';
0 ignored issues
show
Bug introduced by
The method getNodeStats() does not exist on fab2s\NodalFlow\Flows\FlowInterface. Did you maybe mean getNodes()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
548
            }
549
        }
550
551
        return $this->nodeStats;
552
    }
553
554
    /**
555
     * Rewinds the Flow
556
     *
557
     * @return $this
558
     */
559
    public function rewind()
560
    {
561
        $this->nodeCount       = count($this->nodes);
562
        $this->lastIdx         = $this->nodeCount - 1;
563
        $this->nodeIdx         = 0;
564
        $this->break           = false;
565
        $this->continue        = false;
566
        $this->interruptNodeId = null;
567
568
        return $this;
569
    }
570
571
    /**
572
     * Define the progress modulo, Progress Callback will be
573
     * triggered upon each iteration in the flow modulo $progressMod
574
     *
575
     * @param int $progressMod
576
     *
577
     * @return $this
578
     */
579
    public function setProgressMod($progressMod)
580
    {
581
        $this->progressMod = max(1, (int) $progressMod);
582
583
        return $this;
584
    }
585
586
    /**
587
     * Get current $progressMod
588
     *
589
     * @return int
590
     */
591
    public function getProgressMod()
592
    {
593
        return $this->progressMod;
594
    }
595
596
    /**
597
     * The Flow status can either indicate be:
598
     *      - clean (isClean()): everything went well
599
     *      - dirty (isDirty()): one Node broke the flow
600
     *      - exception (isException()): an exception was raised during the flow
601
     *
602
     * @return FlowStatusInterface
603
     */
604
    public function getFlowStatus()
605
    {
606
        return $this->flowStatus;
607
    }
608
609
    /**
610
     * Break the flow's execution, conceptually similar to breaking
611
     * a regular loop
612
     *
613
     * @param InterrupterInterface|null $flowInterrupt
614
     *
615
     * @return $this
616
     */
617
    public function breakFlow(InterrupterInterface $flowInterrupt = null)
618
    {
619
        return $this->interruptFlow(InterrupterInterface::TYPE_BREAK, $flowInterrupt);
620
    }
621
622
    /**
623
     * Continue the flow's execution, conceptually similar to continuing
624
     * a regular loop
625
     *
626
     * @param InterrupterInterface|null $flowInterrupt
627
     *
628
     * @return $this
629
     */
630
    public function continueFlow(InterrupterInterface $flowInterrupt = null)
631
    {
632
        return $this->interruptFlow(InterrupterInterface::TYPE_CONTINUE, $flowInterrupt);
633
    }
634
635
    /**
636
     * @param string                    $interruptType
637
     * @param InterrupterInterface|null $flowInterrupt
638
     *
639
     * @throws NodalFlowException
640
     *
641
     * @return $this
642
     */
643
    public function interruptFlow($interruptType, InterrupterInterface $flowInterrupt = null)
644
    {
645
        switch ($interruptType) {
646
            case InterrupterInterface::TYPE_CONTINUE:
647
                $this->continue = true;
648
                ++$this->numContinue;
649
                break;
650
            case InterrupterInterface::TYPE_BREAK:
651
                $this->flowStatus = new FlowStatus(FlowStatus::FLOW_DIRTY);
652
                $this->break      = true;
653
                ++$this->numBreak;
654
                break;
655
            default:
656
                throw new NodalFlowException('FlowInterrupt Type missing');
657
        }
658
659
        if ($flowInterrupt) {
660
            $flowInterrupt->setType($interruptType)->propagate($this);
661
        }
662
663
        return $this;
664
    }
665
666
    /**
667
     * @param NodeInterface|null $node
668
     *
669
     * @return bool
670
     */
671
    protected function interruptNode(NodeInterface $node = null)
672
    {
673
        // if we have an interruptNodeId, bubble up until we match a node
674
        // else stop propagation
675
        return $this->interruptNodeId ? $this->interruptNodeId !== $node->getNodeHash() : false;
0 ignored issues
show
Bug introduced by
It seems like $node is not always an object, but can also be of type null. Maybe add an additional type check?

If a variable is not always an object, we recommend to add an additional type check to ensure your method call is safe:

function someFunction(A $objectMaybe = null)
{
    if ($objectMaybe instanceof A) {
        $objectMaybe->doSomething();
    }
}
Loading history...
676
    }
677
678
    /**
679
     * Triggered just before the flow starts
680
     *
681
     * @return $this
682
     */
683
    protected function flowStart()
684
    {
685
        ++$this->numExec;
686
        $this->stats['start'] = \microtime(true);
687
        $this->triggerCallback(static::FLOW_START);
688
689
        if (!$this->hasParent()) {
690
            $this->stats['invocations'][$this->numExec]['start'] = $this->stats['start'];
691
        }
692
693
        // flow is started
694
        $this->flowStatus = new FlowStatus(FlowStatus::FLOW_RUNNING);
695
696
        return $this;
697
    }
698
699
    /**
700
     * Triggered right after the flow stops
701
     *
702
     * @return $this
703
     */
704
    protected function flowEnd()
705
    {
706
        $this->stats['end']          = \microtime(true);
707
        $this->stats['mib']          = \memory_get_peak_usage(true) / 1048576;
708
        $this->stats['duration']     = $this->stats['end'] - $this->stats['start'];
709
        $this->stats['num_break']    = $this->numBreak;
710
        $this->stats['num_continue'] = $this->numContinue;
711
712
        if (!$this->hasParent()) {
713
            $this->stats['invocations'][$this->numExec]['end']      = $this->stats['end'];
714
            $this->stats['invocations'][$this->numExec]['duration'] = $this->stats['duration'];
715
            $this->stats['invocations'][$this->numExec]['mib']      = $this->stats['mib'];
716
        }
717
718
        $this->triggerCallback($this->flowStatus->isException() ? static::FLOW_FAIL : static::FLOW_SUCCESS);
719
720
        return $this;
721
    }
722
723
    /**
724
     * Return a simple nonce, fully valid within any flow
725
     *
726
     * @return int
727
     */
728
    protected function getNonce()
729
    {
730
        return self::$nonce++;
731
    }
732
733
    /**
734
     * Recurse over nodes which may as well be Flows and
735
     * Traversable ...
736
     * Welcome to the abysses of recursion or iter-recursion ^^
737
     *
738
     * `recurse` perform kind of an hybrid recursion as the
739
     * Flow is effectively iterating and recurring over its
740
     * Nodes, which may as well be seen as over itself
741
     *
742
     * Iterating tends to limit the amount of recursion levels:
743
     * recursion is only triggered when executing a Traversable
744
     * Node's downstream Nodes while every consecutive exec
745
     * Nodes are executed within a while loop.
746
     * And recursion keeps the size of the recursion context
747
     * to a minimum as pretty much everything is done by the
748
     * iterating instance
749
     *
750
     * @param mixed $param
751
     * @param int   $nodeIdx
752
     *
753
     * @return mixed the last value returned by the last
754
     *               returning value Node in the flow
755
     */
756
    protected function recurse($param = null, $nodeIdx = 0)
757
    {
758
        while ($nodeIdx <= $this->lastIdx) {
759
            $node      = $this->nodes[$nodeIdx];
760
            $nodeStat  = &$this->nodeStats[$nodeIdx];
761
            $returnVal = $node->isReturningVal();
762
763
            if ($node->isTraversable()) {
764
                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...
765
                    if ($returnVal) {
766
                        // pass current $value as next param
767
                        $param = $value;
768
                    }
769
770
                    ++$nodeStat['num_iterate'];
771
                    ++$this->numIterate;
772
                    if (!($this->numIterate % $this->progressMod)) {
773
                        $this->triggerCallback(static::FLOW_PROGRESS, $node);
774
                    }
775
776
                    $param = $this->recurse($param, $nodeIdx + 1);
777
                    if ($this->continue) {
778
                        if ($this->continue = $this->interruptNode($node)) {
779
                            // since we want to bubble the continue upstream
780
                            // we break here waiting for next $param if any
781
                            ++$nodeStat['num_break'];
782
                            break;
783
                        }
784
785
                        // we drop one iteration
786
                        ++$nodeStat['num_continue'];
787
                        continue;
788
                    }
789
790
                    if ($this->break) {
791
                        // we drop all subsequent iterations
792
                        ++$nodeStat['num_break'];
793
                        $this->break = $this->interruptNode($node);
794
                        break;
795
                    }
796
                }
797
798
                // we reached the end of this Traversable and executed all its downstream Nodes
799
                ++$nodeStat['num_exec'];
800
801
                return $param;
802
            }
803
804
            $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...
805
            ++$nodeStat['num_exec'];
806
807
            if ($this->continue) {
808
                ++$nodeStat['num_continue'];
809
                // a continue does not need to bubble up unless
810
                // it specifically targets a node in this flow
811
                // or targets an upstream flow
812
                $this->continue = $this->interruptNode($node);
813
814
                return $param;
815
            }
816
817
            if ($this->break) {
818
                ++$nodeStat['num_break'];
819
                // a break always need to bubble up to the first upstream Traversable if any
820
                return $param;
821
            }
822
823
            if ($returnVal) {
824
                // pass current $value as next param
825
                $param = $value;
826
            }
827
828
            ++$nodeIdx;
829
        }
830
831
        // we reached the end of this recursion
832
        return $param;
833
    }
834
835
    /**
836
     * KISS helper to trigger Callback slots
837
     *
838
     * @param string             $which
839
     * @param null|NodeInterface $node
840
     *
841
     * @return $this
842
     */
843
    protected function triggerCallback($which, NodeInterface $node = null)
844
    {
845
        if (null !== $this->callBack) {
846
            $this->callBack->$which($this, $node);
847
        }
848
849
        return $this;
850
    }
851
}
852