Passed
Push — master ( dbf372...eb96f1 )
by Javier
03:07
created

StateMachine::__construct()   A

Complexity

Conditions 5
Paths 8

Size

Total Lines 13
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 5

Importance

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