Passed
Push — master ( cf9b22...f8edaa )
by Javier
03:21
created

StateMachine   B

Complexity

Total Complexity 49

Size/Duplication

Total Lines 273
Duplicated Lines 0 %

Test Coverage

Coverage 98.54%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 123
dl 0
loc 273
ccs 135
cts 137
cp 0.9854
rs 8.48
c 2
b 0
f 0
wmc 49

21 Methods

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