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.

State   B
last analyzed

Complexity

Total Complexity 44

Size/Duplication

Total Lines 526
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 5

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 44
lcom 2
cbo 5
dl 0
loc 526
ccs 114
cts 114
cp 1
rs 8.3396
c 0
b 0
f 0

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 7 2
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
    const CALLABLE_ENTRY = 'state entry';
80
    const CALLABLE_EXIT = 'state exit';
81
    
82
    /**
83
     * the state types:
84
     * - 'initial':     a statemachine has exactly 1 initial type, this is always the only
85
     *                  entrance into the statemachine.
86
     * - 'normal':      a statemachine can have 0-n normal types.
87
     * - 'done':        a statemachine should have at least 1 final type where it has no
88
     *                  further transitions.
89
     * - 'regex':       a statemachine configuration could have regex states, which serve a purpose to create transitions
90
     *                  from or to multiple other states
91
     * 
92
     * @var string
93
     */
94
    const TYPE_INITIAL = 'initial', TYPE_NORMAL = 'normal', TYPE_FINAL = 'final', TYPE_REGEX = 'regex';
95
    
96
    /**
97
     * The state type:
98
     * - State::TYPE_INITIAL
99
     * - State::TYPE_NORMAL
100
     * - State::TYPE_FINAL
101
     * - State::TYPE_REGEX
102
     * @var string
103
     */
104
    protected $type;
105
    
106
    /**
107
     * an array of transitions that are outgoing for this state.
108
     * These will be set by Transition objects (they provide the association)
109
     *
110
     * this is not a hashmap, so the order of Transitions *might* be important.
111
     * whenever a State is asked for it's transitions, the first transition
112
     * might
113
     * be tried first. this might have performance and configuration benefits
114
     *
115
     * @var Transition[]
116
     */
117
    protected $transitions;
118
    
119
    /**
120
     * The name of the state
121
     * 
122
     * @var string
123
     */
124
    protected $name;
125
    
126
    /**
127
     * fully qualified command name for the command to be executed
128
     * when entering a state as part of a transition.
129
     * This can actually be a ',' seperated string of multiple commands that
130
     * will be executed as a composite.
131
     * 
132
     * @var string
133
     */
134
    protected $command_entry_name;
135
    
136
    /**
137
     * fully qualified command name for the command to be executed
138
     * when exiting a state as part of a transition.
139
     * This can actually be a ',' seperated string of multiple commands that
140
     * will be executed as a composite.
141
     * 
142
     * @var string
143
     */
144
    protected $command_exit_name;
145
    
146
    /**
147
     *  the entry callable method
148
     * @var callable
149
     */
150
    protected $callable_entry;
151
    
152
    /**
153
     *  the exit callable method
154
     * @var callable
155
     */
156
    protected $callable_exit;
157
    
158
    /**
159
     * a description for the state
160
     * 
161
     * @var string
162
     */
163
    protected $description;
164
165
    /**
166
     *
167
     * @param string $name
168
     *            the name of the state (can also be a regex in format: [not-]regex:/<regex-specification-here>/)
169
     * @param string $type
170
     *            the type of the state (on of self::TYPE_<*>)
171
     * @param $command_entry_name optional:
172
     *            a command to be executed when a transition enters this state
173
     *            One or more fully qualified command (sub)class name(s) to
174
     *            execute when entering this state.
175
     *            This can actually be a ',' seperated string of multiple
176
     *            commands that will be executed as a composite.
177
     * @param $command_exit_name optional:
178
     *            a command to be executed when a transition leaves this state
179
     *            One or more fully qualified command (sub)class name(s) to
180
     *            execute when exiting this state.
181
     *            This can actually be a ',' seperated string of multiple
182
     *            commands that will be executed as a composite.
183
     * @param callable $callable_entry
184
     *            optional: a php callable to call. eg: "function(){echo 'closure called';};"
185
     * @param callable $callable_exit
186
     *            optional: a php callable to call. eg: "izzum\MyClass::myStaticMethod"
187
     */
188 79
    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)
189
    {
190 79
        $this->setName($name);
191 79
        $this->setType($type);
192 79
        $this->setEntryCommandName($command_entry_name);
193 79
        $this->setExitCommandName($command_exit_name);
194 79
        $this->setEntryCallable($callable_entry);
0 ignored issues
show
Bug introduced by
It seems like $callable_entry defined by parameter $callable_entry on line 188 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...
195 79
        $this->setExitCallable($callable_exit);
0 ignored issues
show
Bug introduced by
It seems like $callable_exit defined by parameter $callable_exit on line 188 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...
196 79
        $this->transitions = array();
197 79
    }
198
199
    /**
200
     * get the entry callable, the callable to be called when entering this state
201
     * @return callable 
202
     */
203 25
    public function getEntryCallable()
204
    {
205 25
        return $this->callable_entry;
206
    }
207
208
    /**
209
     * set the entry callable, the callable to be called when entering this state
210
     * @param callable $callable
211
     */
212 79
    public function setEntryCallable($callable)
213
    {
214 79
        $this->callable_entry = $callable;
215 79
        return $this;
216
    }
217
218
    /**
219
     * get the exit callable, the callable to be called when exiting this state
220
     * @return callable
221
     */
222 25
    public function getExitCallable()
223
    {
224 25
        return $this->callable_exit;
225
        
226
    }
227
228
    /**
229
     * set the exit callable, the callable to be called when exiting this state
230
     * @param callable $callable
231
     */
232 79
    public function setExitCallable($callable)
233
    {
234 79
        $this->callable_exit = $callable;
235 79
        return $this;
236
    }
237
238
    /**
239
     * is it an initial state
240
     * 
241
     * @return boolean
242
     */
243 25
    public function isInitial()
244
    {
245 25
        return $this->type === self::TYPE_INITIAL;
246
    }
247
248
    /**
249
     * is it a normal state
250
     * 
251
     * @return boolean
252
     */
253 5
    public function isNormal()
254
    {
255 5
        return $this->type === self::TYPE_NORMAL;
256
    }
257
258
    /**
259
     * is it a final state
260
     * 
261
     * @return boolean
262
     */
263 68
    public function isFinal()
264
    {
265 68
        return $this->type === self::TYPE_FINAL;
266
    }
267
268
    /**
269
     * is this state a regex type of state?
270
     * formats:
271
     *      "regex:<regular-expression-here>"
272
     *      "not-regex:<regular-expression-here>"
273
     *
274
     * @return boolean
275
     * @link https://php.net/manual/en/function.preg-match.php
276
     * @link http://regexr.com/ for trying out regular expressions
277
     */
278 79
    public function isRegex()
279
    {
280
        //check the type (and check the state name for regex matches)
281 79
        return $this->type === self::TYPE_REGEX || $this->isNormalRegex() || $this->isNegatedRegex();
282
    }
283
284
    /**
285
     * is this state a normal regex type of state?
286
     * "regex:<regular-expression-here>"
287
     *
288
     * @return boolean
289
     */
290 79
    public function isNormalRegex()
291
    {
292 79
        return strpos($this->getName(), self::REGEX_PREFIX) === 0;
293
    }
294
295
    /**
296
     * is this state a negated regex type of state?
297
     * "not-regex:<regular-expression-here>"
298
     *
299
     * @return boolean
300
     */
301 79
    public function isNegatedRegex()
302
    {
303 79
        return strpos($this, self::REGEX_PREFIX_NEGATED) === 0;
304
    }
305
306
    /**
307
     * get the state type
308
     * 
309
     * @return string
310
     */
311 2
    public function getType()
312
    {
313
        
314 2
        $this->isRegex();
315 2
        return $this->type;
316
    }
317
318
    /**
319
     * set the state type
320
     *
321
     * @param string $type
322
     */
323 79
    protected function setType($type)
324
    {
325
        //if a client mistakenly creates a regex State (a name of [not-]<regex:>), but with a non-regex type, 
326
        //we will set it to a regex state.
327 79
        if($this->isRegex()) {
328 23
            $type = self::TYPE_REGEX;
329 23
        }
330 79
        $this->type = trim($type);
331 79
        return $this;
332
    }
333
334
    /**
335
     * add an outgoing transition from this state.
336
     *
337
     * TRICKY: this method should be package visibility only,
338
     * so don't use directly. it is used to set the bidirectional association
339
     * for State and Transition from a Transition instance on the state the transition will be allowed to 
340
     * run from ('state from').
341
     *
342
     * @param Transition $transition            
343
     * @return boolan yes in case the transition was not on the State already or in case of an invalid transition
344
     */
345 67
    public function addTransition(Transition $transition)
346
    {
347 67
        $output = false;
348
        // check all existing transitions.
349 67
        if (!$this->hasTransition($transition->getName()) 
350 67
                && $transition->getStateFrom()->getName() == $this->getName() 
351 67
                && !$this->isFinal()
352 67
                 && !$this->isRegex()) {
353 67
            $output = true;
354 67
            $this->transitions [] = $transition;
355 67
        }
356
        
357 67
        return $output;
358
    }
359
360
    /**
361
     * get all outgoing transitions
362
     * 
363
     * @return Transition[] an array of transitions
364
     */
365 20
    public function getTransitions()
366
    {
367
        // a subclass might return an ordered/prioritized array
368 20
        return $this->transitions;
369
    }
370
371
    /**
372
     * gets the name of this state
373
     */
374 79
    public function getName()
375
    {
376 79
        return $this->name;
377
    }
378
379
    /**
380
     * sets the name of this state
381
     * @param string $name
382
     */
383 79
    protected function setName($name)
384
    {
385 79
        $this->name = trim($name);
386 79
        return $this;
387
    }
388
389
    /**
390
     *
391
     * @return string
392
     */
393 79
    public function __toString()
394
    {
395 79
        return $this->getName();
396
    }
397
398
    /**
399
     * Do we have a transition from this state with a certain name?
400
     * 
401
     * @param string $transition_name            
402
     * @return boolean
403
     */
404 67
    public function hasTransition($transition_name)
405
    {
406 67
        $has = false;
407 67
        foreach ($this->transitions as $transition) {
408 52
            if ($transition_name === $transition->getName()) {
409 50
                $has = true;
410 50
                break;
411
            }
412 67
        }
413 67
        return $has;
414
    }
415
416
    /**
417
     * An action executed every time a state is entered.
418
     * An entry action will not be executed for an 'initial' state.
419
     *
420
     * @param Context $context            
421
     * @throws Exception
422
     */
423 22
    public function entryAction(Context $context)
424
    {
425 22
        $command = $this->getCommand($this->getEntryCommandName(), $context);
426 22
        $this->execute($command);
427 22
        $this->callCallable($this->getEntryCallable(), $context, self::CALLABLE_ENTRY);
428 21
    }
429
430
    /**
431
     * calls a $callable if it exists, with the arguments $context->getEntity()
432
     * @param callable $callable
433
     * @param Context $context
434
     * @param string $type the type of callable (self::CALLABLE_ENTRY | self::CALLABLE_EXIT)
435
     */
436 22
    protected function callCallable($callable, Context $context, $type = 'n/a')
437
    {
438 22
        if ($callable != self::CALLABLE_NULL){
439 9
            Utils::checkCallable($callable, $type, $this, $context);
440 8
            call_user_func($callable, $context->getEntity());
441 8
        }
442 21
    }
443
444
    /**
445
     * An action executed every time a state is exited.
446
     * An exit action will not be executed for a 'final' state since a machine
447
     * will not leave a 'final' state.
448
     *
449
     * @param Context $context            
450
     * @throws Exception
451
     */
452 22
    public function exitAction(Context $context)
453
    {
454 22
        $command = $this->getCommand($this->getExitCommandName(), $context);
455 22
        $this->execute($command);
456 22
        $this->callCallable($this->getExitCallable(), $context, self::CALLABLE_EXIT);
457 21
    }
458
459
    /**
460
     * helper method
461
     * 
462
     * @param ICommand $command            
463
     * @throws Exception
464
     */
465 22
    protected function execute(ICommand $command)
466
    {
467
        try {
468 22
            $command->execute();
469 22
        } catch(\Exception $e) {
470
            // command failure
471 1
            $e = new Exception($e->getMessage(), Exception::COMMAND_EXECUTION_FAILURE, $e);
472 1
            throw $e;
473
        }
474 22
    }
475
476
    /**
477
     * returns the associated Command for the entry/exit action.
478
     * the Command will be configured with the domain model via dependency injection
479
     *
480
     * @param string $command_name
481
     *            entry or exit command name
482
     * @param Context $context            
483
     * @return ICommand
484
     * @throws Exception
485
     */
486 22
    protected function getCommand($command_name, Context $context)
487
    {
488 22
        return Utils::getCommand($command_name, $context);
489
    }
490
491
    /**
492
     * get the transition for this state that can be triggered by an event code.
493
     * 
494
     * @param string $event
495
     *            the event code that can trigger a transition (mealy machine)
496
     * @return Transition[]
497
     */
498 11
    public function getTransitionsTriggeredByEvent($event)
499
    {
500 11
        $output = array();
501 11
        foreach ($this->getTransitions() as $transition) {
502 11
            if ($transition->isTriggeredBy($event)) {
503 11
                $output [] = $transition;
504 11
            }
505 11
        }
506 11
        return $output;
507
    }
508
509
    /**
510
     * get the fully qualified command name for entry of the state
511
     * 
512
     * @return string
513
     */
514 24
    public function getEntryCommandName()
515
    {
516 24
        return $this->command_entry_name;
517
    }
518
519
    /**
520
     * get the fully qualified command name for exit of the state
521
     * 
522
     * @return string
523
     */
524 24
    public function getExitCommandName()
525
    {
526 24
        return $this->command_exit_name;
527
    }
528
529
    /**
530
     * set the exit command name
531
     * @param string $name a fully qualified command name
532
     */
533 79
    public function setExitCommandName($name)
534
    {
535 79
        $this->command_exit_name = trim($name);
536 79
        return $this;
537
    }
538
539
    /**
540
     * set the entry command name
541
     * @param string $name a fully qualified command name
542
     */
543 79
    public function setEntryCommandName($name)
544
    {
545 79
        $this->command_entry_name = trim($name);
546 79
        return $this;
547
    }
548
549
    /**
550
     * set the description of the state (for uml generation for example)
551
     * 
552
     * @param string $description            
553
     */
554 15
    public function setDescription($description)
555
    {
556 15
        $this->description = $description;
557 15
        return $this;
558
    }
559
560
    /**
561
     * get the description for this state (if any)
562
     * 
563
     * @return string
564
     */
565 3
    public function getDescription()
566
    {
567 3
        return $this->description;
568
    }
569
}