Completed
Pull Request — master (#1)
by Fabrice
04:13
created

FlowMap::initDefaults()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 18
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 18
c 0
b 0
f 0
rs 9.4285
cc 2
eloc 11
nc 2
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\Flows;
11
12
use fab2s\NodalFlow\NodalFlowException;
13
use fab2s\NodalFlow\Nodes\AggregateNodeInterface;
14
use fab2s\NodalFlow\Nodes\BranchNodeInterface;
15
use fab2s\NodalFlow\Nodes\NodeInterface;
16
17
/**
18
 * class FlowMap
19
 * Do not implement Serializable interface on purpose
20
 *
21
 * @SEE https://externals.io/message/98834#98834
22
 */
23
class FlowMap implements FlowMapInterface
24
{
25
    /**
26
     * Flow map
27
     *
28
     * @var array
29
     */
30
    protected $nodeMap = [];
31
32
    /**
33
     * @var NodeInterface[]
34
     */
35
    protected $reverseMap = [];
36
37
    /**
38
     * The default Node Map values
39
     *
40
     * @var array
41
     */
42
    protected $nodeMapDefault = [
43
        'class'           => null,
44
        'flowId'          => null,
45
        'hash'            => null,
46
        'index'           => null,
47
        'isATraversable'  => null,
48
        'isAReturningVal' => null,
49
        'isAFlow'         => null,
50
        'num_exec'        => 0,
51
        'num_iterate'     => 0,
52
        'num_break'       => 0,
53
        'num_continue'    => 0,
54
    ];
55
56
    /**
57
     * The default Node stats values
58
     *
59
     * @var array
60
     */
61
    protected $nodeIncrements = [
62
        'num_exec'     => 0,
63
        'num_iterate'  => 0,
64
        'num_break'    => 0,
65
        'num_continue' => 0,
66
    ];
67
68
    /**
69
     * The Flow map default values
70
     *
71
     * @var array
72
     */
73
    protected $flowMapDefault = [
74
        'class'    => null,
75
        'id'       => null,
76
        'start'    => null,
77
        'end'      => null,
78
        'elapsed'  => null,
79
        'duration' => null,
80
        'mib'      => null,
81
    ];
82
83
    /**
84
     * @var array
85
     */
86
    protected $incrementTotals = [];
87
88
    /**
89
     * @var array
90
     */
91
    protected $flowIncrements;
92
93
    /**
94
     * @var array
95
     */
96
    protected $flowStats;
97
98
    /**
99
     * @var array
100
     */
101
    protected static $registry = [];
102
103
    /**
104
     * @var FlowInterface
105
     */
106
    protected $flow;
107
108
    /**
109
     * @var string
110
     */
111
    protected $flowId;
112
113
    /**
114
     * Instantiate a Flow Status
115
     *
116
     * @param FlowInterface $flow
117
     * @param array         $flowIncrements
118
     */
119
    public function __construct(FlowInterface $flow, array $flowIncrements = [])
120
    {
121
        $this->flow             = $flow;
122
        $this->flowId           = $this->flow->getId();
123
        $this->initDefaults()->setRefs()->setFlowIncrement($flowIncrements);
124
    }
125
126
    /**
127
     * The goal is to offer a cheap global state that supports
128
     * sending a parameter to whatever node in any live Flows.
129
     * We also need some kind of specific setup for each Flows
130
     * and Nodes (custom increments etc).
131
     *
132
     * The idea is to share a registry among all instances
133
     * without :
134
     *  - singleton: would only be useful to store the global state
135
     *      not the specific setup
136
     *  - breaking references: we want to be able to increment fast
137
     *      thus multiple entries at once by reference
138
     *  - DI: it would be a bit too much of code for the purpose and
139
     *      would come with the same limitation as the singleton
140
     *  - Complex and redundant merge strategies: registering a Node
141
     *      would then require to look up for ascendant and descendant
142
     *      Flows each time.
143
     *  - Breaking serialization: Playing with static and references
144
     *      require some attention. The matter is dealt with transparently
145
     *      as the static registry acts like a global cache feed with each
146
     *      instance data upon un-serialization.
147
     *
148
     * By using a static, we make sure all instances share the same
149
     * registry at all time in the simplest way.
150
     * Each FlowMap instance keeps dynamic reference to the portion
151
     * of the registry that belongs to the Flow holding the instance.
152
     * Upon Serialization, every FlowMap instance will thus only store
153
     * a portion of the global state, that is relevant to its carrying
154
     * Flow.
155
     *
156
     * Upon un-serialization, the global state is restored bit by bit
157
     * as each FlowMap gets un-serialized.
158
     * This leverage one edge case, that is, if we un-serialize two
159
     * time the same Flow (could only be members of different Flows).
160
     * This is not very likely as Flow id are immutable and unique,
161
     * but it could occur.
162
     * This situation is currently dealt with by throwing an exception,
163
     * as it would introduce the need to deal with distinct instances
164
     * with the same Id. And generating new ids is not simple either
165
     * as their whole point is to stay the same for others to know
166
     * who's who.
167
     * I find it a small trade of though, as un-serializing twice
168
     * the same string seems buggy and reusing the same Flow without
169
     * cloning is not such a good idea either (nor required).
170
     *
171
     * The use of a static registry also bring a somehow exotic feature :
172
     * The ability to target any Node in any Flow is not limited to the
173
     * flow within the same root Flow. You can actually target any Flow
174
     * in the current process. Using reference implementation would limit
175
     * the sharing to the root FLow members.
176
     * I don't know if this can be actually useful, but I don't think
177
     * it's such a big deal either.
178
     *
179
     * If you don't feel like doing this at home, I completely
180
     * understand, I'd be very happy to hear about a better and
181
     * more efficient way
182
     */
183
    public function __wakeup()
184
    {
185
        if (isset(static::$registry[$this->flowId])) {
186
            throw new NodalFlowException('Un-serializing a Flow when it is already instantiated is not supported', 1, null, [
187
                'class' => get_class($this->flow),
188
                'id'    => $this->flowId,
189
            ]);
190
        }
191
192
        $this->setRefs();
193
    }
194
195
    /**
196
     * @return array
197
     */
198
    public function getRegistry()
199
    {
200
        return static::$registry;
201
    }
202
203
    /**
204
     * @param NodeInterface $node
205
     * @param int           $index
206
     *
207
     * @throws NodalFlowException
208
     */
209
    public function register(NodeInterface $node, $index)
210
    {
211
        $this->enforceUniqueness($node);
212
        $nodeId                 = $node->getId();
213
        $this->nodeMap[$nodeId] = array_replace($this->nodeMapDefault, [
214
            'class'           => get_class($node),
215
            'flowId'          => $this->flowId,
216
            'hash'            => $nodeId,
217
            'index'           => $index,
218
            'isATraversable'  => $node->isTraversable(),
219
            'isAReturningVal' => $node->isReturningVal(),
220
            'isAFlow'         => $node->isFlow(),
221
        ]);
222
223
        $this->setNodeIncrement($node);
224
225
        if (isset($this->reverseMap[$index])) {
226
            // replacing a node, maintain nodeMap accordingly
227
            unset($this->nodeMap[$this->reverseMap[$index]->getId()]);
228
        }
229
230
        $this->reverseMap[$index] = $node;
231
    }
232
233
    /**
234
     * Triggered right before the flow starts
235
     *
236
     * @return $this
237
     */
238
    public function flowStart()
239
    {
240
        $this->flowStats['start'] = microtime(true);
241
242
        return $this;
243
    }
244
245
    /**
246
     * Triggered right after the flow stops
247
     *
248
     * @return $this
249
     */
250
    public function flowEnd()
251
    {
252
        $this->flowStats['end']     = microtime(true);
253
        $this->flowStats['mib']     = memory_get_peak_usage(true) / 1048576;
254
        $this->flowStats['elapsed'] = $this->flowStats['end'] - $this->flowStats['start'];
255
256
        $this->flowStats = array_replace($this->flowStats, $this->duration($this->flowStats['elapsed']));
257
258
        return $this;
259
    }
260
261
    /**
262
     * Let's be fast at incrementing while we are at it
263
     *
264
     * @param string $nodeHash
265
     *
266
     * @return array
267
     */
268
    public function &getNodeStat($nodeHash)
269
    {
270
        return $this->nodeMap[$nodeHash];
271
    }
272
273
    /**
274
     * Get/Generate Node Map
275
     *
276
     * @throws NodalFlowException
277
     *
278
     * @return array
279
     */
280
    public function getNodeMap()
281
    {
282
        foreach ($this->flow->getNodes() as $node) {
283
            $nodeId = $node->getId();
284
            if ($node instanceof BranchNodeInterface) {
285
                $this->nodeMap[$nodeId]['nodes'] = $node->getPayload()->getNodeMap();
286
                continue;
287
            }
288
289
            if ($node instanceof AggregateNodeInterface) {
290
                foreach ($node->getNodeCollection() as $aggregatedNode) {
291
                    $this->nodeMap[$nodeId]['nodes'][$aggregatedNode->getId()] = array_replace($this->nodeMapDefault, [
292
                        'class'  => get_class($aggregatedNode),
293
                        'flowId' => $this->flowId,
294
                        'hash'   => $aggregatedNode->getId(),
295
                    ]);
296
                }
297
                continue;
298
            }
299
        }
300
301
        return $this->nodeMap;
302
    }
303
304
    /**
305
     * Get the latest Node stats
306
     *
307
     * @return array
308
     */
309
    public function getStats()
310
    {
311
        foreach ($this->flow->getNodes() as $node) {
312
            $nodeMap = $this->nodeMap[$node->getId()];
313
            foreach ($this->incrementTotals as $srcKey => $totalKey) {
314
                if (isset($nodeMap[$srcKey])) {
315
                    $this->flowStats[$totalKey] += $nodeMap[$srcKey];
316
                }
317
            }
318
319
            if ($node instanceof BranchNodeInterface) {
320
                $childFlowId                               = $node->getPayload()->getId();
321
                $this->flowStats['branches'][$childFlowId] = $nodeMap = $node->getPayload()->getStats();
0 ignored issues
show
Unused Code introduced by
$nodeMap is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
322
                foreach ($this->incrementTotals as $srcKey => $totalKey) {
323
                    if (isset($this->flowStats['branches'][$childFlowId][$totalKey])) {
324
                        $this->flowStats[$totalKey] += $this->flowStats['branches'][$childFlowId][$totalKey];
325
                    }
326
                }
327
            }
328
        }
329
330
        return $this->flowStats;
331
    }
332
333
    /**
334
     * @param string $nodeHash
335
     * @param string $key
336
     *
337
     * @return $this
338
     */
339
    public function incrementNode($nodeHash, $key)
340
    {
341
        ++$this->nodeMap[$nodeHash][$key];
342
343
        return $this;
344
    }
345
346
    /**
347
     * @param string $key
348
     *
349
     * @return $this
350
     */
351
    public function incrementFlow($key)
352
    {
353
        ++$this->flowStats[$key];
354
355
        return $this;
356
    }
357
358
    /**
359
     * Resets Nodes stats, can be used prior to Flow's re-exec
360
     *
361
     * @return $this
362
     */
363
    public function resetNodeStats()
364
    {
365
        foreach ($this->nodeMap as &$nodeStat) {
366
            foreach ($this->nodeIncrements as $key => $value) {
367
                if (isset($nodeStat[$key])) {
368
                    $nodeStat[$key] = $value;
369
                }
370
            }
371
        }
372
373
        return $this;
374
    }
375
376
    /**
377
     * Computes a human readable duration string from floating seconds
378
     *
379
     * @param float $seconds
380
     *
381
     * @return array<string,integer|string>
382
     */
383
    public function duration($seconds)
384
    {
385
        $result = [
386
            'hour'     => (int) floor($seconds / 3600),
387
            'min'      => (int) floor(($seconds / 60) % 60),
388
            'sec'      => $seconds % 60,
389
            'ms'       => (int) round(\fmod($seconds, 1) * 1000),
390
        ];
391
392
        $duration = '';
393
        foreach ($result as $unit => $value) {
394
            if (!empty($value) || $unit === 'ms') {
395
                $duration .= $value . "$unit ";
396
            }
397
        }
398
399
        $result['duration'] = trim($duration);
400
401
        return $result;
402
    }
403
404
    /**
405
     * @return $this
406
     */
407
    protected function setRefs()
408
    {
409
        static::$registry[$this->flowId]['flowStats'] = &$this->flowStats;
410
        static::$registry[$this->flowId]['nodeStats'] = &$this->nodeMap;
411
        static::$registry[$this->flowId]['flow']      = $this->flow;
412
        static::$registry[$this->flowId]['nodes']     = &$this->reverseMap;
413
414
        return $this;
415
    }
416
417
    /**
418
     * @return $this
419
     */
420
    protected function initDefaults()
421
    {
422
        $this->flowIncrements = $this->nodeIncrements;
423
        foreach ($this->flowIncrements as $key => $ignore) {
424
            $totalKey                        = $key . '_total';
425
            $this->incrementTotals[$key]     = $totalKey;
426
            $this->flowIncrements[$totalKey] = 0;
427
        }
428
429
        $this->flowMapDefault = array_replace($this->flowMapDefault, $this->flowIncrements, [
430
            'class' => get_class($this->flow),
431
            'id'    => $this->flowId,
432
        ]);
433
434
        $this->flowStats = $this->flowMapDefault;
435
436
        return $this;
437
    }
438
439
    /**
440
     * Set additional increment keys, use :
441
     *      'keyName' => int
442
     * to add keyName as increment, starting at int
443
     * or :
444
     *      'keyName' => 'existingIncrement'
445
     * to assign keyName as a reference to existingIncrement
446
     *
447
     * @param array $flowIncrements
448
     *
449
     * @throws NodalFlowException
450
     *
451
     * @return $this
452
     */
453
    protected function setFlowIncrement(array $flowIncrements)
454
    {
455
        foreach ($flowIncrements as $incrementKey => $target) {
456
            if (is_string($target)) {
457
                if (!isset($this->flowStats[$target])) {
458
                    throw new NodalFlowException('Cannot set reference on unset target');
459
                }
460
461
                if (substr($incrementKey, -6) === '_total') {
462
                    $this->incrementTotals[$incrementKey] = $target;
463
                    $this->flowStats[$incrementKey]       = 0;
464
                    continue;
465
                }
466
467
                $this->flowStats[$incrementKey] = &$this->flowStats[$target];
468
                continue;
469
            }
470
471
            $this->flowIncrements[$incrementKey] = $target;
472
            $this->flowStats[$incrementKey]      = $target;
473
        }
474
475
        return $this;
476
    }
477
478
    /**
479
     * @param NodeInterface $node
480
     *
481
     * @throws NodalFlowException
482
     *
483
     * @return $this
484
     */
485
    protected function setNodeIncrement(NodeInterface $node)
486
    {
487
        $nodeId = $node->getId();
488
        foreach ($node->getNodeIncrements() as $incrementKey => $target) {
489
            if (is_string($target)) {
490
                if (!isset($this->nodeIncrements[$target])) {
491
                    throw new NodalFlowException('Tried to set an increment alias to an un-registered increment', 1, null, [
492
                        'aliasKey'  => $incrementKey,
493
                        'targetKey' => $target,
494
                    ]);
495
                }
496
497
                $this->nodeMap[$nodeId][$incrementKey] = &$this->nodeMap[$nodeId][$target];
498
                continue;
499
            }
500
501
            $this->nodeIncrements[$incrementKey]   = $target;
502
            $this->nodeMap[$nodeId][$incrementKey] = $target;
503
        }
504
505
        return $this;
506
    }
507
508
    /**
509
     * @param NodeInterface $node
510
     *
511
     * @throws NodalFlowException
512
     *
513
     * @return $this
514
     */
515
    protected function enforceUniqueness(NodeInterface $node)
516
    {
517
        if (isset($this->nodeMap[$node->getId()])) {
518
            throw new NodalFlowException('Cannot reuse Node instances within a Flow', 1, null, [
519
                'duplicate_node' => get_class($node),
520
                'hash'           => $node->getId(),
521
            ]);
522
        }
523
524
        return $this;
525
    }
526
}
527