Completed
Push — master ( c61f64...fb85af )
by Fabrice
01:51
created

NodalFlow::replace()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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