Completed
Push — master ( ab60f2...c901a1 )
by Ricardo
07:00
created

Workflow   F

Complexity

Total Complexity 167

Size/Duplication

Total Lines 864
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 16

Importance

Changes 0
Metric Value
wmc 167
lcom 1
cbo 16
dl 0
loc 864
rs 1.736
c 0
b 0
f 0

37 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 26 5
A createInstance() 0 9 1
A getEntryId() 0 4 1
A isActionAvailable() 0 11 5
B start() 0 47 7
A getEntryState() 0 4 1
A changeEntryState() 0 6 1
A completeEntry() 0 4 1
A getCurrentSteps() 0 4 1
A getStepMeta() 0 9 3
A moveToHistory() 0 15 2
D createNewCurrentStep() 0 39 14
F transitionWorkflow() 0 104 28
A isJoinCompleted() 0 4 1
A doAction() 0 19 4
A getJoinDescriptor() 0 11 3
A getSplitDescriptor() 0 11 3
A getActionDescriptor() 0 13 4
A getStepDescriptor() 0 11 3
A saveWorkflowDefinition() 0 6 2
A removeWorkflowDefinition() 0 4 1
A getAvailableActions() 0 15 3
C getAvailableActionsFromStep() 0 42 14
A getAvailableResult() 0 22 5
B passesConditions() 0 24 11
A passesCondition() 0 4 1
A executeFunctions() 0 15 6
A executeFunction() 0 17 5
A getWorkflowNames() 0 4 1
A genTmpVars() 0 11 2
A getPropertySet() 0 4 2
A setPropertySet() 0 5 2
A removePropertySet() 0 4 2
A getStates() 0 10 5
B getScreens() 0 21 11
A getStepNum() 0 5 3
A fakeNewCurrentStep() 0 10 3

How to fix   Complexity   

Complex Class

Complex classes like Workflow often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Workflow, and based on these observations, apply Extract Interface, too.

1
<?php
2
namespace Fabrica\Workflow;
3
4
use Fabrica\Workflow\Eloquent\Definition;
5
use Fabrica\Workflow\Eloquent\Entry;
6
use Fabrica\Workflow\Eloquent\CurrentStep;
7
use Fabrica\Workflow\Eloquent\HistoryStep;
8
9
use Fabrica\Workflow\StateNotActivatedException;
10
use Fabrica\Workflow\StepNotFoundException;
11
use Fabrica\Workflow\CurrentStepNotFoundException;
12
use Fabrica\Workflow\ActionNotFoundException;
13
use Fabrica\Workflow\ActionNotAvailableException;
14
use Fabrica\Workflow\ResultNotFoundException;
15
use Fabrica\Workflow\ResultNotAvailableException;
16
use Fabrica\Workflow\FunctionNotFoundException;
17
use Fabrica\Workflow\EntryNotFoundException;
18
use Fabrica\Workflow\ConfigNotFoundException;
19
use Fabrica\Workflow\SplitNotFoundException;
20
use Fabrica\Workflow\JoinNotFoundException;
21
22
class Workflow {
23
24
    /**
25
     * The workflow five states.
26
     *
27
     * @var int 
28
     */
29
    const OSWF_CREATED    = 1;
30
    const OSWF_ACTIVATED  = 2;
31
    const OSWF_SUSPENDED  = 3;
32
    const OSWF_COMPLETED  = 4;
33
    const OSWF_KILLED     = 5;
34
35
    /**
36
     * The workflow instance object.
37
     *
38
     * @var App\Workflow\Eloquent\Entry 
39
     */
40
    protected $entry;
41
42
    /**
43
     * The workflow config description.
44
     *
45
     * @var array
46
     */
47
    protected $wf_config;
48
49
    /**
50
     * workflow options 
51
     *
52
     * @var array
53
     */
54
    protected $options = [];
55
56
    /**
57
     * workflow constructor
58
     *
59
     * @param  string $entry_id
60
     * @return void
0 ignored issues
show
Comprehensibility Best Practice introduced by
Adding a @return annotation to constructors is generally not recommended as a constructor does not have a meaningful return value.

Adding a @return annotation to a constructor is not recommended, since a constructor does not have a meaningful return value.

Please refer to the PHP core documentation on constructors.

Loading history...
61
    */
62
    public function __construct($entry_id)
63
    {
64
        $entry = Entry::find($entry_id);
65
        if ($entry)
66
        {
67
            $this->entry = $entry;
68
            $definition = Definition::find($entry->definition_id);
69
            if (!$definition)
70
            {
71
                throw new ConfigNotFoundException();
72
            }
73
74
            if (isset($definition->contents) && $definition->contents)
75
            {
76
                $this->wf_config = $definition->contents;
77
            }
78
            else
79
            {
80
                throw new ConfigNotFoundException();
81
            }
82
        }
83
        else
84
        {
85
            throw new EntryNotFoundException();
86
        }
87
    }
88
89
    /**
90
     * create workflow.
91
     *
92
     * @param string $definition_id
93
     * @param string $caller
94
     * @return string
95
     */
96
    public static function createInstance($definition_id, $caller)
97
    {
98
        $entry = new Entry;
99
        $entry->definition_id = $definition_id;
100
        $entry->creator = $caller;
101
        $entry->state = self::OSWF_CREATED;
102
        $entry->save();
103
        return new Workflow($entry->id);
104
    }
105
106
    /**
107
     * get entry id.
108
     *
109
     * @return string
110
     */
111
    public function getEntryId()
112
    {
113
       return $this->entry->id; 
114
    }
115
116
    /**
117
     * check action is available
118
     *
119
     * @param array $action_descriptor
120
     * @return boolean
121
     */
122
    private function isActionAvailable($action_descriptor)
123
    {
124
        if (isset($action_descriptor['restrict_to']) && isset($action_descriptor['restrict_to']['conditions']) && $action_descriptor['restrict_to']['conditions'])
125
        {
126
            if (!$this->passesConditions($action_descriptor['restrict_to']['conditions']))
127
            {
128
                return false;
129
            }
130
        }
131
        return true;
132
    }
133
134
    /**
135
     * initialize workflow.
136
     *
137
     * @param array options 
138
     * @return void
139
     */
140
    public function start($options=[])
141
    {
142
        $this->options = array_merge($this->options, $options);
143
144
        if (!isset($this->wf_config['initial_action']) || !$this->wf_config['initial_action'])
145
        {
146
            throw new ActionNotFoundException();
147
        }
148
149
        //$available_action_flg = false;
150
        //foreach ($this->wf_config['initial_actions'] as $action_descriptor)
151
        //{
152
        //    if ($this->isActionAvailable($action_descriptor))
153
        //    {
154
        //        $available_action_flg = true;
155
        //        break;
156
        //    }
157
        //}
158
        //if (!$available_action_flg)
159
        //{
160
        //    throw new ActionNotAvailableException();
161
        //}
162
163
        $action_descriptor = $this->wf_config['initial_action'];
164
        if (!$this->isActionAvailable($action_descriptor))
165
        {
166
            throw new ActionNotAvailableException();
167
        }
168
169
        // confirm result whose condition is satified.
170
        if (!isset($action_descriptor['results']) || !$action_descriptor['results'])
171
        {
172
            throw new ResultNotFoundException();
173
        }
174
175
        $available_result_descriptor = $this->getAvailableResult($action_descriptor['results']);
176
        if (!$available_result_descriptor)
0 ignored issues
show
Bug Best Practice introduced by
The expression $available_result_descriptor of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
177
        {
178
            throw new ResultNotAvailableException();
179
        }
180
        // create new current step
181
        $this->createNewCurrentStep($available_result_descriptor, $action_descriptor['id'], '');
182
        // change workflow state to activited
183
        $this->changeEntryState(self::OSWF_ACTIVATED);
184
185
        return $this;
186
    }
187
188
    /**
189
     * get workflow state.
190
     *
191
     * @return string
192
     */
193
    public function getEntryState()
194
    {
195
        return $this->entry->state;
196
    }
197
198
    /**
199
     * change workflow state.
200
     *
201
     * @param string $new_state
202
     * @return void
203
     */
204
    public function changeEntryState($new_state)
205
    {
206
        $entry = Entry::find($this->entry->id);
207
        $entry->state = $new_state;
208
        $entry->save();
209
    }
210
211
    /**
212
     * complete workflow.
213
     *
214
     * @param string $entry_id
215
     * @return void
216
     */
217
    protected function completeEntry($entry_id)
218
    {
219
        return $this->changeEntryState($entry_id, self::OSWF_COMPLETED);
0 ignored issues
show
Unused Code introduced by
The call to Workflow::changeEntryState() has too many arguments starting with self::OSWF_COMPLETED.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
220
    }
221
222
    /**
223
     * get current steps for workflow.
224
     *
225
     * @return array
226
     */
227
    public function getCurrentSteps()
228
    {
229
        return Entry::find($this->entry->id)->currentSteps;
230
    }
231
232
    /**
233
     *  get step meta.
234
     *
235
     * @param string $step_id
236
     * @return array
237
     */
238
    public function getStepMeta($step_id, $name='')
239
    {
240
        $step_description = $this->getStepDescriptor($step_id);
241
        if ($name) 
242
        {
243
            return isset($step_description[$name]) ? $step_description[$name] : '';
244
        }
245
        return $step_description;
246
    }
247
248
    /**
249
     *  move workflow step to history
250
     *
251
     * @param App\Workflow\Eloquent\CurrentStep $current_step
252
     * @param int $action_id
253
     * @return string previous_id 
254
     */
255
    private function moveToHistory($current_step, $action_id)
256
    {
257
        // add to history records
258
        $history_step = new HistoryStep;
259
        $history_step->fill($current_step->toArray());
260
        $history_step->action_id = $action_id;
261
        $history_step->caller = isset($this->options['caller']) ? $this->options['caller'] : '';
262
        $history_step->finish_time = time();
263
        $history_step->save();
264
265
        // delete from current step
266
        $current_step->delete();
267
268
        return $history_step->id;
269
    }
270
271
    /**
272
     *  create new workflow step.
273
     *
274
     * @param array $result_descriptor
275
     * @param int $action_id
276
     * @param string $previous_id
277
     * @return void
278
     */
279
    private function createNewCurrentStep($result_descriptor, $action_id, $previous_id='')
280
    {
281
        $step_descriptor = [];
282
        if (isset($result_descriptor['step']) && $result_descriptor['step'])
283
        {
284
            $step_descriptor = $this->getStepDescriptor($result_descriptor['step']);
285
            if (!$step_descriptor)
0 ignored issues
show
Bug Best Practice introduced by
The expression $step_descriptor of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
286
            {
287
                throw new StepNotFoundException();
288
            }
289
        }
290
        if (!$step_descriptor)
0 ignored issues
show
Bug Best Practice introduced by
The expression $step_descriptor of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
291
        {
292
            return;
293
        }
294
        // order to use for workflow post-function
295
        if (isset($step_descriptor['state']) && $step_descriptor['state'])
296
        {
297
            $this->options['state'] = $step_descriptor['state'];
298
        }
299
300
        $new_current_step = new CurrentStep;
301
        $new_current_step->entry_id = $this->entry->id;
302
        $new_current_step->action_id = $action_id;
303
        $new_current_step->step_id = isset($result_descriptor['step']) ? intval($result_descriptor['step']) : 0;
304
        $new_current_step->previous_id = $previous_id;
305
        $new_current_step->status = isset($result_descriptor['status']) ? $result_descriptor['status'] : 'Finished';
306
        $new_current_step->start_time = time();
307
        $new_current_step->owners =  isset($this->options['owners']) ? $this->options['owners'] : '';
308
        $new_current_step->comments = isset($this->options['comments']) ? $this->options['comments'] : '';
309
        $new_current_step->caller = isset($this->options['caller']) ? $this->options['caller'] : '';
310
        $new_current_step->save();
311
312
        // trigger before step
313
        if (isset($step_descriptor['pre_functions']) && $step_descriptor['pre_functions'])
314
        {
315
            $this->executeFunctions($step_descriptor['pre_functions']);
316
        }
317
    }
318
319
    /**
320
     * transfer workflow step.
321
     *
322
     * @param array $current_steps
323
     * @param int $action;
0 ignored issues
show
Bug introduced by
There is no parameter named $action;. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
324
     * @return void
325
     */
326
    private function transitionWorkflow($current_steps, $action_id)
327
    {
328
        foreach ($current_steps as $current_step)
329
        {
330
            $step_descriptor = $this->getStepDescriptor($current_step->step_id);
331
            if (!$step_descriptor)
0 ignored issues
show
Bug Best Practice introduced by
The expression $step_descriptor of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
332
            {
333
                throw new StepNotFoundException();
334
            }
335
336
            $action_descriptor = $this->getActionDescriptor(isset($step_descriptor['actions']) ? $step_descriptor['actions'] : [], $action_id);
337
            if ($action_descriptor)
0 ignored issues
show
Bug Best Practice introduced by
The expression $action_descriptor of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
338
            {
339
                break;
340
            }
341
        }
342
        if (!$action_descriptor)
0 ignored issues
show
Bug introduced by
The variable $action_descriptor does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
Bug Best Practice introduced by
The expression $action_descriptor of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
343
        {
344
            throw new ActionNotFoundException(); 
345
        }
346
        if (!$this->isActionAvailable($action_descriptor))
347
        {
348
            throw new ActionNotAvailableException();
349
        }
350
351
        if (!isset($action_descriptor['results']) || !$action_descriptor['results'])
352
        {
353
            throw new ResultNotFoundException();
354
        }
355
        // confirm result whose condition is satified.
356
        $available_result_descriptor = $this->getAvailableResult($action_descriptor['results']);
357
        if (!$available_result_descriptor)
0 ignored issues
show
Bug Best Practice introduced by
The expression $available_result_descriptor of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
358
        {
359
            throw new ResultNotAvailableException();
360
        }
361
362
        // triggers after step
363
        if (isset($step_descriptor['post_functions']) && $step_descriptor['post_functions'])
364
        {
365
            $this->executeFunctions($step_descriptor['post_functions']);
366
        }
367
        // triggers before action
368
        if (isset($action_descriptor['pre_functions']) && $action_descriptor['pre_functions'])
369
        {
370
            $this->executeFunctions($action_descriptor['pre_functions']);
371
        }
372
        // triggers before result
373
        if (isset($available_result_descriptor['pre_functions']) && $available_result_descriptor['pre_functions'])
374
        {
375
            $this->executeFunctions($available_result_descriptor['pre_functions']);
376
        }
377
        // split workflow
378
        if (isset($available_result_descriptor['split']) && $available_result_descriptor['split'])
379
        {
380
            // get split result
381
            $split_descriptor = $this->getSplitDescriptor($available_result_descriptor['split']);
382
            if (!$split_descriptor)
0 ignored issues
show
Bug Best Practice introduced by
The expression $split_descriptor of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
383
            {
384
                throw new SplitNotFoundException();
385
            }
386
387
            // move current to history step
388
            $prevoius_id = $this->moveToHistory($current_step, $action_id);
0 ignored issues
show
Bug introduced by
The variable $current_step seems to be defined by a foreach iteration on line 328. Are you sure the iterator is never empty, otherwise this variable is not defined?

It seems like you are relying on a variable being defined by an iteration:

foreach ($a as $b) {
}

// $b is defined here only if $a has elements, for example if $a is array()
// then $b would not be defined here. To avoid that, we recommend to set a
// default value for $b.


// Better
$b = 0; // or whatever default makes sense in your context
foreach ($a as $b) {
}

// $b is now guaranteed to be defined here.
Loading history...
389
            foreach ($split_descriptor['list'] as $result_descriptor)
390
            {
391
                $this->createNewCurrentStep($result_descriptor, $action_id, $prevoius_id);
392
            }
393
        }
394
        else if (isset($available_result_descriptor['join']) && $available_result_descriptor['join'])
395
        {
396
            // fix me. join logic will be realized, suggest using the propertyset
397
            // get join result
398
            $join_descriptor = $this->getJoinDescriptor($available_result_descriptor['join']);
399
            if (!$join_descriptor)
0 ignored issues
show
Bug Best Practice introduced by
The expression $join_descriptor of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
400
            {
401
                throw new JoinNotFoundException();
402
            }
403
404
            // move current to history step
405
            $prevoius_id = $this->moveToHistory($current_step, $action_id);
406
            if ($this->isJoinCompleted())
407
            {
408
                // record other previous_ids by propertyset
409
                $this->createNewCurrentStep($join_descriptor, $action_id, $prevoius_id);
410
            }
411
        }
412
        else
413
        {
414
            // move current to history step
415
            $prevoius_id = $this->moveToHistory($current_step, $action_id);
416
            // create current step
417
            $this->createNewCurrentStep($available_result_descriptor, $action_id, $prevoius_id);
418
        }
419
        // triggers after result
420
        if (isset($available_result_descriptor['post_functions']) && $available_result_descriptor['post_functions'])
421
        {
422
            $this->executeFunctions($available_result_descriptor['post_functions']);
423
        }
424
        // triggers after action
425
        if (isset($action_descriptor['post_functions']) && $action_descriptor['post_functions'])
426
        {
427
            $this->executeFunctions($action_descriptor['post_functions']);
428
        }
429
    }
430
431
    /**
432
     * check if the join is completed 
433
     */
434
    private function isJoinCompleted()
435
    {
436
        return !CurrentStep::where('entry_id', $this->entry->id)->exists();
437
    }
438
439
    /**
440
     * execute action 
441
     *
442
     * @param string $action_id
443
     * @param array $options;
0 ignored issues
show
Documentation introduced by
There is no parameter named $options;. Did you maybe mean $options?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
444
     * @return string
445
     */
446
    public function doAction($action_id, $options=[])
447
    {
448
        $state = $this->getEntryState($this->entry->id);
0 ignored issues
show
Unused Code introduced by
The call to Workflow::getEntryState() has too many arguments starting with $this->entry->id.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
449
        if ($state != self::OSWF_CREATED && $state != self::OSWF_ACTIVATED)
450
        {
451
            throw new StateNotActivatedException();
452
        }
453
454
        $current_steps = $this->getCurrentSteps();
455
        if (!$current_steps)
0 ignored issues
show
Bug Best Practice introduced by
The expression $current_steps of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
456
        {
457
            throw new CurrentStepNotFoundException();
458
        }
459
460
        // set options
461
        $this->options = array_merge($this->options, $options);
462
        // complete workflow step transition
463
        $this->transitionWorkflow($current_steps, intval($action_id));
464
    }
465
466
    /**
467
     * get join descriptor from list.
468
     *
469
     * @param string $join_id
470
     * @return array 
471
     */
472
    private function getJoinDescriptor($join_id)
473
    {
474
        foreach ($this->wf_config['joins'] as $join)
475
        {
476
            if ($join['id'] == $join_id)
477
            {
478
                return $join;
479
            }
480
        }
481
        return [];
482
    }
483
484
    /**
485
     * get split descriptor from list.
486
     *
487
     * @param string $split_id
488
     * @return array 
489
     */
490
    private function getSplitDescriptor($split_id)
491
    {
492
        foreach ($this->wf_config['splits'] as $split)
493
        {
494
            if ($split['id'] == $split_id)
495
            {
496
                return $split;
497
            }
498
        }
499
        return [];
500
    }
501
502
    /**
503
     * get action descriptor from list.
504
     *
505
     * @param array $actions
506
     * @param string $action_id
507
     * @return array 
508
     */
509
    private function getActionDescriptor($actions, $action_id)
510
    {
511
        // get global config
512
        $actions = $actions ?: [];
513
        foreach ($actions as $action)
514
        {
515
            if ($action['id'] == $action_id)
516
            {
517
                return $action;
518
            }
519
        }
520
        return [];
521
    }
522
523
    /**
524
     *  get step configuration.
525
     *
526
     * @param array $steps
0 ignored issues
show
Bug introduced by
There is no parameter named $steps. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
527
     * @param string $step_id
528
     * @return array
529
     */
530
    private function getStepDescriptor($step_id)
531
    {
532
        foreach ($this->wf_config['steps'] as $step)
533
        {
534
            if ($step['id'] == $step_id)
535
            {
536
                return $step;
537
            }
538
        }
539
        return [];
540
    }
541
542
    /**
543
     * save workflow configuration info.
544
     *
545
     * @param array $info
546
     * @return void
547
     */
548
    public static function saveWorkflowDefinition($info)
549
    {
550
        $definition = $info['_id'] ? Definition::find($info['_id']) : new Definition;
551
        $definition->fill($info);
552
        $definition->save();
553
    }
554
555
    /**
556
     * remove configuration info.
557
     *
558
     * @param string $definition_id
559
     * @return void
560
     */
561
    public static function removeWorkflowDefinition($definition_id)
562
    {
563
        Definition::find($definition_id)->delete();
564
    }
565
566
    /**
567
     * get all available actions
568
     *
569
     * @param array $info
0 ignored issues
show
Bug introduced by
There is no parameter named $info. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
570
     * @param bool $dest_state added for kanban dnd, is not common param.
571
     * @return array
572
     */
573
    public function getAvailableActions($options=[], $dest_state = false)
574
    {
575
        // set options
576
        $this->options = array_merge($this->options, $options);
577
578
        $available_actions = [];
579
        // get current steps
580
        $current_steps = $this->getCurrentSteps();
581
        foreach ($current_steps as $current_step)
582
        {
583
            $actions = $this->getAvailableActionsFromStep($current_step->step_id, $dest_state);
584
            $actions && $available_actions += $actions;
0 ignored issues
show
Bug Best Practice introduced by
The expression $actions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
585
        }
586
        return $available_actions;
587
    }
588
589
    /**
590
     * get available actions for step
591
     *
592
     * @param string $step_id
593
     * @param bool $dest_state added for kanban dnd, is not common param.
594
     * @return array
595
     */
596
    private function getAvailableActionsFromStep($step_id, $dest_state = false)
597
    {
598
        $step_descriptor = $this->getStepDescriptor($step_id);
599
        if (!$step_descriptor)
0 ignored issues
show
Bug Best Practice introduced by
The expression $step_descriptor of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
600
        {
601
            throw new StepNotFoundException();
602
        }
603
        if (!isset($step_descriptor['actions']) || !$step_descriptor['actions'])
604
        {
605
            return [];
606
        }
607
        // global conditions for step
608
        if (!$this->isActionAvailable($step_descriptor))
609
        {
610
            return [];
611
        }
612
613
        $available_actions = [];
614
        foreach ($step_descriptor['actions'] as $action)
615
        {
616
            if ($this->isActionAvailable($action))
617
            {
618
                if ($dest_state)
619
                {
620
                    $state = '';
621
                    if (isset($action['results']) && is_array($action['results']) && count($action['results']) > 0 && isset($action['results'][0]['step']))
622
                    {
623
                        $dest_step_descriptor = $this->getStepDescriptor($action['results'][0]['step']);
624
                        $state = $dest_step_descriptor['state'];
625
                    }
626
                    
627
                    $available_actions[] = [ 'id' => $action['id'], 'name' => $action['name'], 'screen' => $action['screen'] ?: '', 'state' => $state ];
628
                }
629
                else
630
                {
631
                    $available_actions[] = [ 'id' => $action['id'], 'name' => $action['name'], 'screen' => $action['screen'] ?: '' ];
632
                }
633
            }
634
        }
635
636
        return $available_actions;
637
    }
638
639
    /**
640
     * get available result from result-list 
641
     *
642
     * @param array $results_descriptor
643
     * @return array
644
     */
645
    public function getAvailableResult($results_descriptor)
646
    {
647
        $available_result_descriptor = [];
648
649
        // confirm result whose condition is satified.
650
        foreach ($results_descriptor as $result_descriptor)
651
        {
652
            if (isset($result_descriptor['conditions']) && $result_descriptor['conditions'])
653
            {
654
                if ($this->passesConditions($result_descriptor['conditions']))
655
                {
656
                    $available_result_descriptor = $result_descriptor;
657
                    break;
658
                }
659
            }
660
            else
661
            {
662
                $available_result_descriptor = $result_descriptor;
663
            }
664
        }
665
        return $available_result_descriptor;
666
    }
667
668
    /**
669
     * check conditions is passed
670
     *
671
     * @param array $conditions
672
     * @return boolean
673
     */
674
    private function passesConditions($conditions)
675
    {
676
        if (!isset($conditions['list']) || !$conditions['list'])
677
        {
678
            return true;
679
        }
680
681
        $type = isset($conditions['type']) && isset($conditions['type']) ? $conditions['type'] : 'and';
682
        $result = $type == 'and' ? true : false;
683
684
        foreach ($conditions['list'] as $condition)
685
        {
686
            $tmp = $this->passesCondition($condition);
687
            if ($type == 'and' && !$tmp)
688
            {
689
                return false;
690
            }
691
            if ($type == 'or' && $tmp)
692
            {
693
                return true;
694
            }
695
        }
696
        return $result;
697
    }
698
699
    /**
700
     * check condition is passed
701
     *
702
     * @param array $condition
703
     * @return boolean
704
     */
705
    private function passesCondition($condition)
706
    {
707
        return $this->executeFunction($condition);
708
    }
709
710
    /**
711
     * execute functions
712
     *
713
     * @param array function
714
     * @return void
715
     */
716
    private function executeFunctions($functions)
717
    {
718
        if (!$functions || !is_array($functions))
719
        {
720
            return;
721
        }
722
723
        foreach ($functions as $function) 
724
        {
725
            if (is_array($function) && $function)
0 ignored issues
show
Bug Best Practice introduced by
The expression $function of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
726
            {
727
                $this->executeFunction($function);
728
            }
729
        }
730
    }
731
732
    /**
733
     * execute function
734
     *
735
     * @param array $function
736
     * @return mixed
737
     */
738
    private function executeFunction($function)
739
    {
740
        $method = explode('@', $function['name']);
741
        $class = $method[0];
742
        $action = isset($method[1]) && $method[1] ? $method[1] : 'handle';
743
744
        // check handle function exists
745
        if (!method_exists($class, $action))
746
        {
747
            throw new FunctionNotFoundException();
748
        }
749
        $args = isset($function['args']) ? $function['args'] : [];
750
        // generate temporary vars
751
        $tmp_vars = $this->genTmpVars($args);
752
        // call handle function
753
        return $class::$action($tmp_vars);
754
    }
755
756
    /**
757
     * get all workflows' name.
758
     *
759
     * @return array
760
     */
761
    public static function getWorkflowNames()
762
    {
763
        return Definition::all(['name']);
764
    }
765
766
    /**
767
     * generate temporary variable.
768
     *
769
     * @return array
770
     */
771
    private function genTmpVars($args=[])
772
    {
773
        $tmp_vars = [];
774
        foreach ($this->entry as $key => $val)
775
        {
776
            $tmp_vars[$key] = $val;
777
        }
778
        $tmp_vars = array_merge($tmp_vars, $this->options);
779
780
        return array_merge($tmp_vars, $args);
781
    }
782
783
    /**
784
     * get property set
785
     *
786
     * @return mixed 
787
     */
788
    public function getPropertySet($key)
789
    {
790
        return $key ? $this->entry->propertysets[$key] : $this->entry->propertysets;
791
    }
792
793
    /**
794
     * add property set 
795
     *
796
     * @return void 
797
     */
798
    public function setPropertySet($key, $val)
799
    {
800
        $this->entry->propertysets = array_merge($this->entry->propertysets ?: [], [ $key => $val ]);
801
        $this->entry->save();
802
    }
803
804
    /**
805
     * remove property set
806
     *
807
     * @return void 
808
     */
809
    public function removePropertySet($key)
810
    {
811
        $this->entry->unset($key ? ('propertysets.' . $key) : 'propertysets');
812
    }
813
814
    /**
815
     * get used states in the workflow
816
     *
817
     * @return array
818
     */
819
    public static function getStates($contents)
820
    {
821
        $state_ids = [];
822
        $steps = isset($contents['steps']) && $contents['steps'] ? $contents['steps'] : [];
823
        foreach ($steps as $step)
824
        {
825
            $state_ids[] = isset($step['state']) ? $step['state'] : '';
826
        }
827
        return $state_ids;
828
    }
829
830
    /**
831
     * get used screens in the workflow 
832
     *
833
     * @return array 
834
     */
835
    public static function getScreens($contents)
836
    {
837
        $screen_ids = [];
838
        $steps = isset($contents['steps']) && $contents['steps'] ? $contents['steps'] : [];
839
        foreach ($steps as $step)
840
        {
841
            if (!isset($step['actions']) || !$step['actions'])
842
            {
843
                continue;
844
            }
845
            foreach ($step['actions'] as $action)
846
            {
847
                if (!isset($action['screen']) || !$action['screen'])
848
                {
849
                    continue;
850
                }
851
                $action['screen'] !=  '-1' && !in_array($action['screen'], $screen_ids) && $screen_ids[] = $action['screen'];
852
            }
853
        }
854
        return $screen_ids;
855
    }
856
857
    /**
858
     * get step num 
859
     *
860
     * @return int 
861
     */
862
    public static function getStepNum($contents)
863
    {
864
        $steps = isset($contents['steps']) && $contents['steps'] ? $contents['steps'] : [];
865
        return count($steps);
866
    }
867
868
    /**
869
     * fake new workflow step.
870
     *
871
     * @param array $result_descriptor
872
     * @param array $caller
873
     * @return void
874
     */
875
    public function fakeNewCurrentStep($result_descriptor, $caller)
876
    {
877
        $new_current_step = new CurrentStep;
878
        $new_current_step->entry_id = $this->entry->id;
879
        $new_current_step->step_id = intval($result_descriptor['id']);
880
        $new_current_step->status = isset($result_descriptor['status']) ? $result_descriptor['status'] : '';
881
        $new_current_step->start_time = time();
882
        $new_current_step->caller = $caller ?: '';
883
        $new_current_step->save();
884
    }
885
}
886