Passed
Pull Request — master (#1)
by Fabrice
04:59
created

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