Passed
Push — master ( 7a5dd1...cfa4be )
by Javier
03:16
created

StateMachine   B

Complexity

Total Complexity 48

Size/Duplication

Total Lines 268
Duplicated Lines 0 %

Test Coverage

Coverage 98.46%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 122
dl 0
loc 268
ccs 128
cts 130
cp 0.9846
rs 8.5599
c 2
b 0
f 0
wmc 48

18 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 13 5
A getMachineToArray() 0 3 1
A eventMustExistOrFail() 0 4 2
A argumentIsNotNullOrFail() 0 4 2
A argumentIsValidOrFail() 0 4 1
A addState() 0 9 1
A setCurrentStateIfThisIsInitialState() 0 4 2
A can() 0 28 5
A getCurrentState() 0 3 1
A stateMustExistOrFail() 0 4 2
A argumentIsNotBlankOrFail() 0 4 2
A addTransition() 0 28 3
A to() 0 1 1
A addCommonTransition() 0 16 2
A getNextState() 0 3 1
A getCurrentEvent() 0 3 1
A cancelTransition() 0 3 1
F fireEvent() 0 61 15

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