Passed
Push — master ( cfa4be...cf9b22 )
by Javier
07:22
created

StateMachine   B

Complexity

Total Complexity 44

Size/Duplication

Total Lines 258
Duplicated Lines 0 %

Test Coverage

Coverage 98.43%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 115
dl 0
loc 258
ccs 125
cts 127
cp 0.9843
rs 8.8798
c 2
b 0
f 0
wmc 44

20 Methods

Rating   Name   Duplication   Size   Complexity  
A getMachineToArray() 0 3 1
A eventMustExistOrFail() 0 4 2
A argumentIsNotNullOrFail() 0 4 2
A argumentIsValidOrFail() 0 4 1
A setCurrentStateIfThisIsInitialState() 0 4 2
A getCurrentState() 0 3 1
A stateMustExistOrFail() 0 4 2
A argumentIsNotBlankOrFail() 0 4 2
A getNextState() 0 3 1
A getCurrentEvent() 0 3 1
A cancelTransition() 0 3 1
A execGuard() 0 11 4
A addState() 0 9 1
A can() 0 25 2
A execAction() 0 6 3
A addTransition() 0 22 4
A to() 0 2 1
A __construct() 0 13 5
A addCommonTransition() 0 11 2
B fireEvent() 0 40 6

How to fix   Complexity   

Complex Class

Complex classes like StateMachine 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.

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 StateMachine, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace jagarsoft\StateMachine;
4
5
use jagarsoft\StateMachine\StateMachineBuilder;
6
use phpDocumentor\Reflection\Types\Array_;
7
8
class StateMachine
9
{
10
    public const NEXT_STATE = 0;
11
    public const EXEC_ACTION = 'EXEC_ACTION';
12
    public const EXEC_GUARD = 'EXEC_GUARD';
13
    public const EXEC_BEFORE = 'EXEC_BEFORE';
14
    public const EXEC_AFTER = 'EXEC_AFTER';
15
16
    protected $sm = [];
17
    protected $currentState = null;
18
    protected $currentEvent = null;
19
    protected $nextState = null;
20
21
    protected $cancelTransition = false;
22
    protected $transitionInProgress = false;
23
    protected $eventsQueued = [];
24
25 36
    public function __construct(StateMachineBuilder $smb = null)
26
    {
27 36
        if ($smb != null)
28 2
            $this->smb = $smb->from();
0 ignored issues
show
Bug Best Practice introduced by
The property smb does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
29
30 36
        if (!empty($this->smb)) {
31
            // StateEnum::CURRENT_STATE => [ EventEnum::ON_EVENT => [ NEXT_STATE, ClosureOrFunctionsArray ] ],
32 2
            foreach ($this->smb as $state => $transition) {
33 2
                $this->addState($state);
34 2
                foreach ($transition as $onEvent => $nextStateAndAction) {
35 2
                    $this->addTransition($state, $onEvent,
36 2
                        $nextStateAndAction[self::NEXT_STATE],
37
                        $nextStateAndAction);
38
                }
39
            }
40
        }
41 36
    }
42
43
    public function to()
44
    {
45
        // TODO: pendig of implementation
46
        /*
47
        if( $this->smb == null )
48
            return;
49
50
        $this->smb->to($this->getMachineToArray());
51
        */
52
    }
53
54 27
    public function addState($state)
55
    {
56 27
        $this->argumentIsValidOrFail($state);
57
58 25
        $this->setCurrentStateIfThisIsInitialState($state);
59
60 25
        $this->sm[$state] = [];
61
62 25
        return $this;
63
    }
64
65 27
    public function addTransition($currentState, $currentEvent, $nextState, /*\Closure|array*/ $execAction = null)
66
    {
67 27
        $this->argumentIsValidOrFail($currentState);
68 27
        $this->argumentIsValidOrFail($currentEvent);
69 27
        $this->argumentIsValidOrFail($nextState);
70
71 27
        $this->setCurrentStateIfThisIsInitialState($currentState);
72
73 27
        if( $execAction === null ){
74 10
            $this->sm[$currentState][$currentEvent] = [ self::NEXT_STATE => $nextState ];
75 19
        } elseif (is_array($execAction)) {
76 4
            $arrayActions = $execAction;
77 4
            $this->sm[$currentState][$currentEvent] = [ self::NEXT_STATE => $nextState ] + $arrayActions;
78 15
        } elseif($execAction instanceof \Closure) {
79 14
            $this->sm[$currentState][$currentEvent] = [
80 14
                self::NEXT_STATE => $nextState,
81 14
                self::EXEC_ACTION => $execAction
82
            ];
83
        } else {
84 1
            throw new \InvalidArgumentException('execAction argument must be Closure or array');
85
        }
86 26
        return $this;
87
    }
88
89 5
    public function addCommonTransition($currentEvent, $nextState, /*\Closure|array*/ $execAction = null)
90
    {
91 5
        $this->argumentIsValidOrFail($currentEvent);
92 5
        $this->argumentIsValidOrFail($nextState);
93
94 5
        $states = array_keys($this->sm);
95 5
        foreach ($states as $state) {
96 5
            $this->addTransition($state, $currentEvent, $nextState, $execAction);
97
        }
98
99 5
        return $this;
100
    }
101
102
    /**
103
     * @param $event
104
     * @noinspection PhpArrayPushWithOneElementInspection
105
     */
106 21
    public function fireEvent($event)
107
    {
108 21
        $this->argumentIsValidOrFail($event);
109
110 21
        if ($this->transitionInProgress) {
111 2
            array_push($this->eventsQueued, $event);
112 2
            return $this;
113
        }
114 21
        $this->transitionInProgress = true;
115
116 21
        $this->eventMustExistOrFail($event);
117
118 20
        $transition = $this->sm[$this->currentState][$event];
119
120 20
        $this->nextState = $transition[self::NEXT_STATE];
121 20
        $this->currentEvent = $event;
122
123 20
        $this->stateMustExistOrFail($this->nextState);
124
125 19
        $wasGuarded = $this->execGuard($transition);
126
127 19
        if (!$wasGuarded) {
128 18
            $this->execAction(self::EXEC_BEFORE, $transition);
129 18
            $this->execAction(self::EXEC_ACTION, $transition);
130 17
            $this->execAction(self::EXEC_AFTER, $transition);
131
        }
132
133 18
        if ($this->cancelTransition || $wasGuarded) {
134 6
            $this->cancelTransition = false;
135
        } else {
136 13
            $this->currentState = $this->nextState;
137
        }
138
139 18
        $this->transitionInProgress = false;
140 18
        $event = array_shift($this->eventsQueued);
141 18
        if ($event != null) {
142 2
            $this->fireEvent($event);
143
        }
144
145 18
        return $this;
146
    }
147
148 20
    private function execGuard($transition)
149
    {
150 20
        $wasGuarded = false;
151 20
        if( array_key_exists(self::EXEC_GUARD, $transition) ){
152 3
            $guard = $transition[self::EXEC_GUARD];
153 3
            if ($guard) {
154 2
                if (($guard)($this) === false)
155 2
                    $wasGuarded = true;
156
            }
157
        }
158 20
        return $wasGuarded;
159
    }
160
161 18
    private function execAction($actionIndex, $transition): void
162
    {
163 18
        if (array_key_exists($actionIndex, $transition)) {
164 16
            $action = $transition[$actionIndex];
165 16
            if ($action) {
166 16
                ($action)($this);
167
            }
168
        }
169 18
    }
170
171 4
    public function can($event)
172
    {
173 4
        $this->argumentIsValidOrFail($event);
174
175
        try{
176 3
            $this->eventMustExistOrFail($event);
177 1
        } catch(\InvalidArgumentException $e){
178 1
            return false;
179
        }
180
181 3
        $transition = $this->sm[$this->currentState][$event];
182
183 3
        $nextState = $this->nextState;
184 3
        $currentEvent = $this->currentEvent;
185 3
        $this->nextState = $transition[self::NEXT_STATE];
186 3
        $this->currentEvent = $event;
187
188 3
        $this->stateMustExistOrFail($this->nextState);
189
190 2
        $wasGuarded = ! $this->execGuard($transition);
191
192 2
        $this->nextState = $nextState;
193 2
        $this->currentEvent = $currentEvent;
194
195 2
        return $wasGuarded;
196
    }
197
198 5
    public function cancelTransition()
199
    {
200 5
        $this->cancelTransition = true;
201 5
    }
202
203 10
    public function getCurrentState()
204
    {
205 10
        return $this->currentState;
206
    }
207
208 4
    public function getCurrentEvent()
209
    {
210 4
        return $this->currentEvent;
211
    }
212
213 1
    public function getNextState()
214
    {
215 1
        return $this->nextState;
216
    }
217
218 2
    public function getMachineToArray()
219
    {
220 2
        return $this->sm;
221
    }
222
223
    /**
224
     * All possible transitions from current state.
225
     *
226
     * @return array
227
     */
228
    /*public function getPossibleTransitions(){
229
        // TODO: pending to implementation
230
    }*/
231
232 35
    private function argumentIsValidOrFail($arg): void
233
    {
234 35
        $this->argumentIsNotNullOrFail($arg);
235 34
        $this->argumentIsNotBlankOrFail($arg);
236 33
    }
237
238 35
    private function argumentIsNotNullOrFail($arg): void
239
    {
240 35
        if( $arg === null )
241 5
            throw new \InvalidArgumentException("Null is not an valid argument");
242 34
    }
243
244 34
    private function argumentIsNotBlankOrFail($arg): void
245
    {
246 34
        if( trim($arg) === "" )
247 3
            throw new \InvalidArgumentException("Blank is not an valid argument");
248 33
    }
249
250 23
    private function eventMustExistOrFail($event)
251
    {
252 23
        if( !( isset($this->sm[$this->currentState][$event]) ) )
253 2
            throw new \InvalidArgumentException("Unexpected event '{$event}' on '{$this->currentState}' state");
254 22
    }
255
256 22
    private function stateMustExistOrFail($state)
257
    {
258 22
        if( ! isset($this->sm[$state]) )
259 2
            throw new \InvalidArgumentException("Event '{$this->currentEvent}' fired to unadded '{$state}' state");
260 20
    }
261
262 33
    private function setCurrentStateIfThisIsInitialState($state): void
263
    {
264 33
        if( $this->currentState == null){
265 33
            $this->currentState = $state;
266
        }
267 33
    }
268
}
269