Completed
Pull Request — master (#1)
by Fabrice
09:39 queued 01:54
created

NodalFlow::getProgressMod()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
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\FlowAbstract;
14
use fab2s\NodalFlow\Flows\FlowMap;
15
use fab2s\NodalFlow\Flows\FlowStatus;
16
use fab2s\NodalFlow\Flows\InterrupterInterface;
17
use fab2s\NodalFlow\Nodes\BranchNodeInterface;
18
use fab2s\NodalFlow\Nodes\ExecNodeInterface;
19
use fab2s\NodalFlow\Nodes\NodeInterface;
20
use fab2s\NodalFlow\Nodes\TraversableNodeInterface;
21
22
/**
23
 * Class NodalFlow
24
 */
25
class NodalFlow extends FlowAbstract
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
     * @var array
37
     */
38
    protected $flowIncrements = [];
39
40
    /**
41
     * The current Node index
42
     *
43
     * @var int
44
     */
45
    protected $nodeIdx = 0;
46
47
    /**
48
     * The last index value
49
     *
50
     * @var int
51
     */
52
    protected $lastIdx = 0;
53
54
    /**
55
     * The number of Node in this Flow
56
     *
57
     * @var int
58
     */
59
    protected $nodeCount = 0;
60
61
    /**
62
     * The number of iteration within this Flow
63
     *
64
     * @var int
65
     */
66
    protected $numIterate = 0;
67
68
    /**
69
     * The current registered Callback class if any
70
     *
71
     * @var CallbackInterface|null
72
     */
73
    protected $callBack;
74
75
    /**
76
     * Continue flag
77
     *
78
     * @var bool
79
     */
80
    protected $continue = false;
81
82
    /**
83
     * Break Flag
84
     *
85
     * @var bool
86
     */
87
    protected $break = false;
88
89
    /**
90
     * Instantiate a Flow
91
     */
92
    public function __construct()
93
    {
94
        $this->flowMap = new FlowMap($this, $this->flowIncrements);
95
    }
96
97
    /**
98
     * Adds a Node to the flow
99
     *
100
     * @param NodeInterface $node
101
     *
102
     * @throws NodalFlowException
103
     *
104
     * @return $this
105
     */
106
    public function add(NodeInterface $node)
107
    {
108
        if ($node instanceof BranchNodeInterface) {
109
            // this node is a branch, set it's parent
110
            $node->getPayload()->setParent($this);
111
        }
112
113
        $node->setCarrier($this);
114
115
        $this->flowMap->register($node, $this->nodeIdx);
116
        $this->nodes[$this->nodeIdx] = $node;
117
118
        ++$this->nodeIdx;
119
120
        return $this;
121
    }
122
123
    /**
124
     * Adds a Payload Node to the Flow
125
     *
126
     * @param callable $payload
127
     * @param mixed    $isAReturningVal
128
     * @param mixed    $isATraversable
129
     *
130
     * @return $this
131
     */
132
    public function addPayload(callable $payload, $isAReturningVal, $isATraversable = false)
133
    {
134
        $node = PayloadNodeFactory::create($payload, $isAReturningVal, $isATraversable);
135
136
        $this->add($node);
137
138
        return $this;
139
    }
140
141
    /**
142
     * Register callback class
143
     *
144
     * @param CallbackInterface $callBack
145
     *
146
     * @return $this
147
     */
148
    public function setCallBack(CallbackInterface $callBack)
149
    {
150
        $this->callBack = $callBack;
151
152
        return $this;
153
    }
154
155
    /**
156
     * Execute the flow
157
     *
158
     * @param null|mixed $param The eventual init argument to the first node
159
     *                          or, in case of a branch, the last relevant
160
     *                          argument from upstream Flow
161
     *
162
     * @throws NodalFlowException
163
     *
164
     * @return mixed the last result of the
165
     *               last returning value node
166
     */
167
    public function exec($param = null)
168
    {
169
        try {
170
            $result = $this->rewind()
171
                    ->flowStart()
172
                    ->recurse($param);
173
174
            // set flowStatus to make sure that we have the proper
175
            // value in flowEnd even when overridden without (or when
176
            // improperly) calling parent
177
            if ($this->flowStatus->isRunning()) {
178
                $this->flowStatus = new FlowStatus(FlowStatus::FLOW_CLEAN);
179
            }
180
181
            $this->flowEnd();
182
183
            return $result;
184
        } catch (\Exception $e) {
185
            $this->flowStatus = new FlowStatus(FlowStatus::FLOW_EXCEPTION);
186
            $this->flowEnd();
187
            if ($e instanceof NodalFlowException) {
188
                throw $e;
189
            }
190
191
            throw new NodalFlowException('Flow execution failed', 0, $e, [
192
                'nodeMap' => $this->getNodeMap(),
193
            ]);
194
        }
195
    }
196
197
    /**
198
     * Rewinds the Flow
199
     *
200
     * @return $this
201
     */
202
    public function rewind()
203
    {
204
        $this->nodeCount       = count($this->nodes);
205
        $this->lastIdx         = $this->nodeCount - 1;
206
        $this->break           = false;
207
        $this->continue        = false;
208
        $this->interruptNodeId = null;
209
210
        return $this;
211
    }
212
213
    /**
214
     * Break the flow's execution, conceptually similar to breaking
215
     * a regular loop
216
     *
217
     * @param InterrupterInterface|null $flowInterrupt
218
     *
219
     * @return $this
220
     */
221
    public function breakFlow(InterrupterInterface $flowInterrupt = null)
222
    {
223
        return $this->interruptFlow(InterrupterInterface::TYPE_BREAK, $flowInterrupt);
224
    }
225
226
    /**
227
     * Continue the flow's execution, conceptually similar to continuing
228
     * a regular loop
229
     *
230
     * @param InterrupterInterface|null $flowInterrupt
231
     *
232
     * @return $this
233
     */
234
    public function continueFlow(InterrupterInterface $flowInterrupt = null)
235
    {
236
        return $this->interruptFlow(InterrupterInterface::TYPE_CONTINUE, $flowInterrupt);
237
    }
238
239
    /**
240
     * Triggered just before the flow starts
241
     *
242
     * @return $this
243
     */
244
    protected function flowStart()
245
    {
246
        $this->flowMap->incrementFlow('num_exec')->flowStart();
247
        $this->triggerCallback(static::FLOW_START);
248
249
        // flow is started
250
        $this->flowStatus = new FlowStatus(FlowStatus::FLOW_RUNNING);
251
252
        return $this;
253
    }
254
255
    /**
256
     * Triggered right after the flow stops
257
     *
258
     * @return $this
259
     */
260
    protected function flowEnd()
261
    {
262
        $this->flowMap->flowEnd();
263
264
        $this->triggerCallback($this->flowStatus->isException() ? static::FLOW_FAIL : static::FLOW_SUCCESS);
265
266
        return $this;
267
    }
268
269
    /**
270
     * Recurse over nodes which may as well be Flows and
271
     * Traversable ...
272
     * Welcome to the abysses of recursion or iter-recursion ^^
273
     *
274
     * `recurse` perform kind of an hybrid recursion as the
275
     * Flow is effectively iterating and recurring over its
276
     * Nodes, which may as well be seen as over itself
277
     *
278
     * Iterating tends to limit the amount of recursion levels:
279
     * recursion is only triggered when executing a Traversable
280
     * Node's downstream Nodes while every consecutive exec
281
     * Nodes are executed within a while loop.
282
     * And recursion keeps the size of the recursion context
283
     * to a minimum as pretty much everything is done by the
284
     * iterating instance
285
     *
286
     * @param mixed $param
287
     * @param int   $nodeIdx
288
     *
289
     * @return mixed the last value returned by the last
290
     *               returning value Node in the flow
291
     */
292
    protected function recurse($param = null, $nodeIdx = 0)
293
    {
294
        while ($nodeIdx <= $this->lastIdx) {
295
            $node      = $this->nodes[$nodeIdx];
296
            $nodeStats = &$this->flowMap->getNodeStat($node->getId());
297
298
            $returnVal = $node->isReturningVal();
299
300
            if ($node->isTraversable()) {
301
                /** @var TraversableNodeInterface $node */
302
                foreach ($node->getTraversable($param) as $value) {
303
                    if ($returnVal) {
304
                        // pass current $value as next param
305
                        $param = $value;
306
                    }
307
308
                    ++$nodeStats['num_iterate'];
309
                    ++$this->numIterate;
310
                    if (!($this->numIterate % $this->progressMod)) {
311
                        $this->triggerCallback(static::FLOW_PROGRESS, $node);
312
                    }
313
314
                    $param = $this->recurse($param, $nodeIdx + 1);
315
                    if ($this->continue) {
316
                        if ($this->continue = $this->interruptNode($node)) {
317
                            // since we want to bubble the continue upstream
318
                            // we break here waiting for next $param if any
319
                            ++$nodeStats['num_break'];
320
                            break;
321
                        }
322
323
                        // we drop one iteration
324
                        ++$nodeStats['num_continue'];
325
                        continue;
326
                    }
327
328
                    if ($this->break) {
329
                        // we drop all subsequent iterations
330
                        ++$nodeStats['num_break'];
331
                        $this->break = $this->interruptNode($node);
332
                        break;
333
                    }
334
                }
335
336
                // we reached the end of this Traversable and executed all its downstream Nodes
337
                ++$nodeStats['num_exec'];
338
339
                return $param;
340
            }
341
342
            /** @var ExecNodeInterface $node */
343
            $value = $node->exec($param);
344
            ++$nodeStats['num_exec'];
345
346
            if ($this->continue) {
347
                ++$nodeStats['num_continue'];
348
                // a continue does not need to bubble up unless
349
                // it specifically targets a node in this flow
350
                // or targets an upstream flow
351
                $this->continue = $this->interruptNode($node);
352
353
                return $param;
354
            }
355
356
            if ($this->break) {
357
                ++$nodeStats['num_break'];
358
                // a break always need to bubble up to the first upstream Traversable if any
359
                return $param;
360
            }
361
362
            if ($returnVal) {
363
                // pass current $value as next param
364
                $param = $value;
365
            }
366
367
            ++$nodeIdx;
368
        }
369
370
        // we reached the end of this recursion
371
        return $param;
372
    }
373
374
    /**
375
     * KISS helper to trigger Callback slots
376
     *
377
     * @param string             $which
378
     * @param null|NodeInterface $node
379
     *
380
     * @return $this
381
     */
382
    protected function triggerCallback($which, NodeInterface $node = null)
383
    {
384
        if (null !== $this->callBack) {
385
            $this->callBack->$which($this, $node);
386
        }
387
388
        return $this;
389
    }
390
}
391