GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Completed
Push — develop ( 50cef7...02ca20 )
by Rolf
01:54
created

State   B

Complexity

Total Complexity 45

Size/Duplication

Total Lines 521
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 5

Test Coverage

Coverage 100%

Importance

Changes 17
Bugs 0 Features 2
Metric Value
wmc 45
c 17
b 0
f 2
lcom 2
cbo 5
dl 0
loc 521
ccs 113
cts 113
cp 1
rs 8.3673

31 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 10 1
A getEntryCallable() 0 4 1
A setEntryCallable() 0 5 1
A getExitCallable() 0 5 1
A setExitCallable() 0 5 1
A isInitial() 0 4 1
A isNormal() 0 4 1
A isFinal() 0 4 1
A isRegex() 0 5 3
A isNormalRegex() 0 4 1
A isNegatedRegex() 0 4 1
A getType() 0 6 1
A setType() 0 10 2
B addTransition() 0 14 5
A getTransitions() 0 5 1
A getName() 0 4 1
A setName() 0 5 1
A __toString() 0 4 1
A hasTransition() 0 11 3
A entryAction() 0 6 1
A callCallable() 0 6 3
A exitAction() 0 6 1
A execute() 0 10 2
A getCommand() 0 4 1
A getTransitionsTriggeredByEvent() 0 10 3
A getEntryCommandName() 0 4 1
A getExitCommandName() 0 4 1
A setExitCommandName() 0 5 1
A setEntryCommandName() 0 5 1
A setDescription() 0 5 1
A getDescription() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like State 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 State, and based on these observations, apply Extract Interface, too.

1
<?php
2
namespace izzum\statemachine;
3
use izzum\command\ICommand;
4
use izzum\command\NullCommand;
5
use izzum\statemachine\Exception;
6
use izzum\command\Composite;
7
use izzum\statemachine\utils\Utils;
8
9
/**
10
 * This class holds the finite state data:
11
 * - the name of the state
12
 * - the type of the state (initial/normal/final)
13
 * - what outgoing transitions this state has (bidirectional association
14
 * initiated by a Transition)
15
 * - class names for the entry and exit commands (if any)
16
 * - callables for entry and exit logic (if any)
17
 *
18
 * A State instance can (and should) be shared by multiple Transition
19
 * objects when it is the same State for their origin/from State.
20
 * 
21
 * A State can be a regex state (or negated regex). 
22
 * A regex state can be used in a transition and when added to a
23
 * statemachine the regular expression will be matched on all currently
24
 * known states on that statemachine and new Transitions will be added
25
 * to the statemachine that match the from/to state regexes. This is very 
26
 * useful to build a lot of transitions very quickly.
27
 * 
28
 * to build a full mesh of transitions (all states to all states):
29
 * $a = new State('a');
30
 * $b = new State('b');
31
 * $c = new State('c');
32
 * $machine->addState($a);
33
 * $machine->addState($b);
34
 * $machine->addState($c);
35
 * $state_regex_all = new State('regex:|.*|');
36
 * $machine->addTransition(new Transition($state_regex_all, $state_regex_all));
37
 *
38
 * @author Rolf Vreijdenberger
39
 * @link https://php.net/manual/en/language.types.callable.php
40
 * @link https://en.wikipedia.org/wiki/Command_pattern    
41
 * @link https://php.net/manual/en/function.preg-match.php
42
 * @link http://regexr.com/ for trying out regular expressions    
43
 */
44
class State {
45
    
46
    /**
47
     * state name if it is unknown (not configured)
48
     * @var string
49
     */
50
    const STATE_UNKNOWN = 'unknown';
51
    
52
    /**
53
     * default name for the first/only initial state (but you can specify whatever you want for your initial state)
54
     * @var string
55
     */
56
    const STATE_NEW = 'new';
57
    
58
    /**
59
     * default name for a normal final state
60
     * @var string
61
     */
62
    const STATE_DONE = 'done';
63
    
64
    /**
65
     * default exit/entry command
66
     * @var string
67
     */
68
    const COMMAND_NULL = '\izzum\command\NullCommand';
69
    
70
    /**
71
     * default exit/entry command for constructor
72
     * @var string
73
     */
74
    const COMMAND_EMPTY = '';
75
    const CALLABLE_NULL = null;
76
    const REGEX_PREFIX = 'regex:';
77
    const REGEX_PREFIX_NEGATED = 'not-regex:';
78
    
79
    /**
80
     * the state types:
81
     * - 'initial':     a statemachine has exactly 1 initial type, this is always the only
82
     *                  entrance into the statemachine.
83
     * - 'normal':      a statemachine can have 0-n normal types.
84
     * - 'done':        a statemachine should have at least 1 final type where it has no
85
     *                  further transitions.
86
     * - 'regex':       a statemachine configuration could have regex states, which serve a purpose to create transitions
87
     *                  from or to multiple other states
88
     * 
89
     * @var string
90
     */
91
    const TYPE_INITIAL = 'initial', TYPE_NORMAL = 'normal', TYPE_FINAL = 'final', TYPE_REGEX = 'regex';
92
    
93
    /**
94
     * The state type:
95
     * - State::TYPE_INITIAL
96
     * - State::TYPE_NORMAL
97
     * - State::TYPE_FINAL
98
     * - State::TYPE_REGEX
99
     * @var string
100
     */
101
    protected $type;
102
    
103
    /**
104
     * an array of transitions that are outgoing for this state.
105
     * These will be set by Transition objects (they provide the association)
106
     *
107
     * this is not a hashmap, so the order of Transitions *might* be important.
108
     * whenever a State is asked for it's transitions, the first transition
109
     * might
110
     * be tried first. this might have performance and configuration benefits
111
     *
112
     * @var Transition[]
113
     */
114
    protected $transitions;
115
    
116
    /**
117
     * The name of the state
118
     * 
119
     * @var string
120
     */
121
    protected $name;
122
    
123
    /**
124
     * fully qualified command name for the command to be executed
125
     * when entering a state as part of a transition.
126
     * This can actually be a ',' seperated string of multiple commands that
127
     * will be executed as a composite.
128
     * 
129
     * @var string
130
     */
131
    protected $command_entry_name;
132
    
133
    /**
134
     * fully qualified command name for the command to be executed
135
     * when exiting a state as part of a transition.
136
     * This can actually be a ',' seperated string of multiple commands that
137
     * will be executed as a composite.
138
     * 
139
     * @var string
140
     */
141
    protected $command_exit_name;
142
    
143
    /**
144
     *  the entry callable method
145
     * @var callable
146
     */
147
    protected $callable_entry;
148
    
149
    /**
150
     *  the exit callable method
151
     * @var callable
152
     */
153
    protected $callable_exit;
154
    
155
    /**
156
     * a description for the state
157
     * 
158
     * @var string
159
     */
160
    protected $description;
161
162
    /**
163
     *
164
     * @param string $name
165
     *            the name of the state (can also be a regex in format: [not-]regex:/<regex-specification-here>/)
166
     * @param string $type
167
     *            the type of the state (on of self::TYPE_<*>)
168
     * @param $command_entry_name optional:
169
     *            a command to be executed when a transition enters this state
170
     *            One or more fully qualified command (sub)class name(s) to
171
     *            execute when entering this state.
172
     *            This can actually be a ',' seperated string of multiple
173
     *            commands that will be executed as a composite.
174
     * @param $command_exit_name optional:
175
     *            a command to be executed when a transition leaves this state
176
     *            One or more fully qualified command (sub)class name(s) to
177
     *            execute when exiting this state.
178
     *            This can actually be a ',' seperated string of multiple
179
     *            commands that will be executed as a composite.
180
     * @param callable $callable_entry
181
     *            optional: a php callable to call. eg: "function(){echo 'closure called';};"
182
     * @param callable $callable_exit
183
     *            optional: a php callable to call. eg: "izzum\MyClass::myStaticMethod"
184
     */
185 74
    public function __construct($name, $type = self::TYPE_NORMAL, $command_entry_name = self::COMMAND_EMPTY, $command_exit_name = self::COMMAND_EMPTY, $callable_entry = self::CALLABLE_NULL, $callable_exit = self::CALLABLE_NULL)
186
    {
187 74
        $this->setName($name);
188 74
        $this->setType($type);
189 74
        $this->setEntryCommandName($command_entry_name);
190 74
        $this->setExitCommandName($command_exit_name);
191 74
        $this->setEntryCallable($callable_entry);
0 ignored issues
show
Bug introduced by
It seems like $callable_entry defined by parameter $callable_entry on line 185 can also be of type null; however, izzum\statemachine\State::setEntryCallable() does only seem to accept callable, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
192 74
        $this->setExitCallable($callable_exit);
0 ignored issues
show
Bug introduced by
It seems like $callable_exit defined by parameter $callable_exit on line 185 can also be of type null; however, izzum\statemachine\State::setExitCallable() does only seem to accept callable, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
193 74
        $this->transitions = array();
194 74
    }
195
196
    /**
197
     * get the entry callable, the callable to be called when entering this state
198
     * @return callable 
199
     */
200 21
    public function getEntryCallable()
201
    {
202 21
        return $this->callable_entry;
203
    }
204
205
    /**
206
     * set the entry callable, the callable to be called when entering this state
207
     * @param callable $callable
208
     */
209 74
    public function setEntryCallable($callable)
210
    {
211 74
        $this->callable_entry = $callable;
212 74
        return $this;
213
    }
214
215
    /**
216
     * get the exit callable, the callable to be called when exiting this state
217
     * @return callable
218
     */
219 21
    public function getExitCallable()
220
    {
221 21
        return $this->callable_exit;
222
        
223
    }
224
225
    /**
226
     * set the exit callable, the callable to be called when exiting this state
227
     * @param callable $callable
228
     */
229 74
    public function setExitCallable($callable)
230
    {
231 74
        $this->callable_exit = $callable;
232 74
        return $this;
233
    }
234
235
    /**
236
     * is it an initial state
237
     * 
238
     * @return boolean
239
     */
240 25
    public function isInitial()
241
    {
242 25
        return $this->type === self::TYPE_INITIAL;
243
    }
244
245
    /**
246
     * is it a normal state
247
     * 
248
     * @return boolean
249
     */
250 5
    public function isNormal()
251
    {
252 5
        return $this->type === self::TYPE_NORMAL;
253
    }
254
255
    /**
256
     * is it a final state
257
     * 
258
     * @return boolean
259
     */
260 64
    public function isFinal()
261
    {
262 64
        return $this->type === self::TYPE_FINAL;
263
    }
264
265
    /**
266
     * is this state a regex type of state?
267
     * formats:
268
     *      "regex:<regular-expression-here>"
269
     *      "not-regex:<regular-expression-here>"
270
     *
271
     * @return boolean
272
     * @link https://php.net/manual/en/function.preg-match.php
273
     * @link http://regexr.com/ for trying out regular expressions
274
     */
275 74
    public function isRegex()
276
    {
277
        //check the type (and check the state name for regex matches)
278 74
        return $this->type === self::TYPE_REGEX || $this->isNormalRegex() || $this->isNegatedRegex();
279
    }
280
281
    /**
282
     * is this state a normal regex type of state?
283
     * "regex:<regular-expression-here>"
284
     *
285
     * @return boolean
286
     */
287 74
    public function isNormalRegex()
288
    {
289 74
        return strpos($this->getName(), self::REGEX_PREFIX) === 0;
290
    }
291
292
    /**
293
     * is this state a negated regex type of state?
294
     * "not-regex:<regular-expression-here>"
295
     *
296
     * @return boolean
297
     */
298 74
    public function isNegatedRegex()
299
    {
300 74
        return strpos($this, self::REGEX_PREFIX_NEGATED) === 0;
301
    }
302
303
    /**
304
     * get the state type
305
     * 
306
     * @return string
307
     */
308 2
    public function getType()
309
    {
310
        
311 2
        $this->isRegex();
312 2
        return $this->type;
313
    }
314
315
    /**
316
     * set the state type
317
     *
318
     * @param string $type
319
     */
320 74
    protected function setType($type)
321
    {
322
        //if a client mistakenly creates a regex State (a name of [not-]<regex:>), but with a non-regex type, 
323
        //we will set it to a regex state.
324 74
        if($this->isRegex()) {
325 23
            $type = self::TYPE_REGEX;
326 23
        }
327 74
        $this->type = trim($type);
328 74
        return $this;
329
    }
330
331
    /**
332
     * add an outgoing transition from this state.
333
     *
334
     * TRICKY: this method should be package visibility only,
335
     * so don't use directly. it is used to set the bidirectional association
336
     * for State and Transition from a Transition instance on the state the transition will be allowed to 
337
     * run from ('state from').
338
     *
339
     * @param Transition $transition            
340
     * @return boolan yes in case the transition was not on the State already or in case of an invalid transition
341
     */
342 63
    public function addTransition(Transition $transition)
343
    {
344 63
        $output = false;
345
        // check all existing transitions.
346 63
        if (!$this->hasTransition($transition->getName()) 
347 63
                && $transition->getStateFrom()->getName() == $this->getName() 
348 63
                && !$this->isFinal()
349 63
                 && !$this->isRegex()) {
350 63
            $output = true;
351 63
            $this->transitions [] = $transition;
352 63
        }
353
        
354 63
        return $output;
355
    }
356
357
    /**
358
     * get all outgoing transitions
359
     * 
360
     * @return Transition[] an array of transitions
361
     */
362 20
    public function getTransitions()
363
    {
364
        // a subclass might return an ordered/prioritized array
365 20
        return $this->transitions;
366
    }
367
368
    /**
369
     * gets the name of this state
370
     */
371 74
    public function getName()
372
    {
373 74
        return $this->name;
374
    }
375
376
    /**
377
     * sets the name of this state
378
     * @param string $name
379
     */
380 74
    protected function setName($name)
381
    {
382 74
        $this->name = trim($name);
383 74
        return $this;
384
    }
385
386
    /**
387
     *
388
     * @return string
389
     */
390 74
    public function __toString()
391
    {
392 74
        return $this->getName();
393
    }
394
395
    /**
396
     * Do we have a transition from this state with a certain name?
397
     * 
398
     * @param string $transition_name            
399
     * @return boolean
400
     */
401 63
    public function hasTransition($transition_name)
402
    {
403 63
        $has = false;
404 63
        foreach ($this->transitions as $transition) {
405 48
            if ($transition_name === $transition->getName()) {
406 46
                $has = true;
407 46
                break;
408
            }
409 63
        }
410 63
        return $has;
411
    }
412
413
    /**
414
     * An action executed every time a state is entered.
415
     * An entry action will not be executed for an 'initial' state.
416
     *
417
     * @param Context $context            
418
     * @throws Exception
419
     */
420 21
    public function entryAction(Context $context)
421
    {
422 21
        $command = $this->getCommand($this->getEntryCommandName(), $context);
423 21
        $this->execute($command);
424 21
        $this->callCallable($this->getEntryCallable(), $context);
425 21
    }
426
427
    /**
428
     * calls a $callable if it exists, with the arguments $context->getEntity()
429
     * @param callable $callable
430
     * @param Context $context
431
     */
432 21
    protected function callCallable($callable, Context $context)
433
    {
434 21
        if ($callable != self::CALLABLE_NULL && is_callable($callable)) {
435 8
            call_user_func($callable, $context->getEntity());
436 8
        }
437 21
    }
438
439
    /**
440
     * An action executed every time a state is exited.
441
     * An exit action will not be executed for a 'final' state since a machine
442
     * will not leave a 'final' state.
443
     *
444
     * @param Context $context            
445
     * @throws Exception
446
     */
447 21
    public function exitAction(Context $context)
448
    {
449 21
        $command = $this->getCommand($this->getExitCommandName(), $context);
450 21
        $this->execute($command);
451 21
        $this->callCallable($this->getExitCallable(), $context);
452 21
    }
453
454
    /**
455
     * helper method
456
     * 
457
     * @param ICommand $command            
458
     * @throws Exception
459
     */
460 21
    protected function execute(ICommand $command)
461
    {
462
        try {
463 21
            $command->execute();
464 21
        } catch(\Exception $e) {
465
            // command failure
466 1
            $e = new Exception($e->getMessage(), Exception::COMMAND_EXECUTION_FAILURE, $e);
467 1
            throw $e;
468
        }
469 21
    }
470
471
    /**
472
     * returns the associated Command for the entry/exit action.
473
     * the Command will be configured with the domain model via dependency injection
474
     *
475
     * @param string $command_name
476
     *            entry or exit command name
477
     * @param Context $context            
478
     * @return ICommand
479
     * @throws Exception
480
     */
481 21
    protected function getCommand($command_name, Context $context)
482
    {
483 21
        return Utils::getCommand($command_name, $context);
484
    }
485
486
    /**
487
     * get the transition for this state that can be triggered by an event code.
488
     * 
489
     * @param string $event
490
     *            the event code that can trigger a transition (mealy machine)
491
     * @return Transition[]
492
     */
493 11
    public function getTransitionsTriggeredByEvent($event)
494
    {
495 11
        $output = array();
496 11
        foreach ($this->getTransitions() as $transition) {
497 11
            if ($transition->isTriggeredBy($event)) {
498 11
                $output [] = $transition;
499 11
            }
500 11
        }
501 11
        return $output;
502
    }
503
504
    /**
505
     * get the fully qualified command name for entry of the state
506
     * 
507
     * @return string
508
     */
509 23
    public function getEntryCommandName()
510
    {
511 23
        return $this->command_entry_name;
512
    }
513
514
    /**
515
     * get the fully qualified command name for exit of the state
516
     * 
517
     * @return string
518
     */
519 23
    public function getExitCommandName()
520
    {
521 23
        return $this->command_exit_name;
522
    }
523
524
    /**
525
     * set the exit command name
526
     * @param string $name a fully qualified command name
527
     */
528 74
    public function setExitCommandName($name)
529
    {
530 74
        $this->command_exit_name = trim($name);
531 74
        return $this;
532
    }
533
534
    /**
535
     * set the entry command name
536
     * @param string $name a fully qualified command name
537
     */
538 74
    public function setEntryCommandName($name)
539
    {
540 74
        $this->command_entry_name = trim($name);
541 74
        return $this;
542
    }
543
544
    /**
545
     * set the description of the state (for uml generation for example)
546
     * 
547
     * @param string $description            
548
     */
549 15
    public function setDescription($description)
550
    {
551 15
        $this->description = $description;
552 15
        return $this;
553
    }
554
555
    /**
556
     * get the description for this state (if any)
557
     * 
558
     * @return string
559
     */
560 3
    public function getDescription()
561
    {
562 3
        return $this->description;
563
    }
564
}