Passed
Push — master ( 532ba1...7a5dd1 )
by Javier
02:52
created

StateMachine::getNextState()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
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 33
    public function __construct(StateMachineBuilder $smb = null)
25
    {
26 33
        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 33
        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 33
    }
41
42
	public function to(){
43
        if( $this->smb == null )
44
            return;
45
46
        $this->smb->to($this->getMachineToArray());
47
    }
48
49 24
    public function addState($state)
50
    {
51 24
        $this->argumentIsValidOrFail($state);
52
53 22
        $this->setCurrentStateIfThisIsInitialState($state);
54
55 22
        $this->sm[$state] = [];
56
57 22
        return $this;
58
    }
59
60 25
    public function addTransition($currentState, $currentEvent, $nextState,
61
                                  /*\Closure|array*/ $execAction = null,
62
                                  \Closure $execGuard = null,
63
                                  \Closure $execBefore = null,
64
                                  \Closure $execAfter = null)
65
    {
66 25
        $this->argumentIsValidOrFail($currentState);
67 25
        $this->argumentIsValidOrFail($currentEvent);
68 25
        $this->argumentIsValidOrFail($nextState);
69
70 25
        $this->setCurrentStateIfThisIsInitialState($currentState);
71
72 25
        if( is_array($execAction) ){
73 4
            $this->sm[$currentState][$currentEvent] = [ self::NEXT_STATE => $nextState ];
74 4
            $arrayActions = $execAction;
75 4
            foreach ($arrayActions as $key => $value) {
76 4
                $this->sm[$currentState][$currentEvent][$key] = $value;
77
            }
78
        } else {
79 21
            $this->sm[$currentState][$currentEvent] = [
80 21
                self::NEXT_STATE => $nextState,
81 21
                self::EXEC_ACTION => $execAction,
82 21
                self::EXEC_GUARD => $execGuard,
83 21
                self::EXEC_BEFORE => $execBefore,
84 21
                self::EXEC_AFTER => $execAfter,
85
            ];
86
        }
87 25
        return $this;
88
    }
89
90 5
    public function addCommonTransition($currentEvent, $nextState,
91
                                        \Closure $execAction = null,
92
                                        \Closure $execGuard = null,
93
                                        \Closure $execBefore = null,
94
                                        \Closure $execAfter = null)
95
    {
96 5
        $this->argumentIsValidOrFail($currentEvent);
97 5
        $this->argumentIsValidOrFail($nextState);
98
99 5
        $states = array_keys($this->sm);
100 5
        foreach ($states as $state) {
101 5
            $this->addTransition($state, $currentEvent, $nextState,
102
                                    $execAction, $execGuard, $execBefore, $execAfter);
103
        }
104
105 5
        return $this;
106
    }
107
108
    /**
109
     * @param $event
110
     * @noinspection PhpArrayPushWithOneElementInspection
111
     */
112 21
    public function fireEvent($event)
113
    {
114 21
        $this->argumentIsValidOrFail($event);
115
116 21
        if ($this->transitionInProgress) {
117 2
            array_push($this->eventsQueued, $event);
118 2
            return $this;
119
        }
120 21
        $this->transitionInProgress = true;
121
122 21
        $this->eventMustExistOrFail($event);
123
124 20
        $transition = $this->sm[$this->currentState][$event];
125
126 20
        $this->nextState = $transition[self::NEXT_STATE];
127
128 20
        $this->stateMustExistOrFail($this->nextState);
129
130 19
        $this->currentEvent = $event;
131
132 19
        $wasGuarded = false;
133 19
        if( array_key_exists(self::EXEC_GUARD, $transition) ){
134 17
            $guard = $transition[self::EXEC_GUARD];
135 17
            if ($guard) {
136 1
                if (($guard)($this) === false)
137 1
                    $wasGuarded = true;
138
            }
139
        }
140 19
        if ( ! $wasGuarded) {
141 18
            if( array_key_exists(self::EXEC_BEFORE, $transition) ) {
142 18
                $before = $transition[self::EXEC_BEFORE];
143 18
                if ($before) {
144 2
                    ($before)($this);
145
                }
146
            }
147 18
            if( array_key_exists(self::EXEC_ACTION, $transition) ) {
148 16
                $action = $transition[self::EXEC_ACTION];
149 16
                if ($action) {
150 14
                    ($action)($this);
151
                }
152
            }
153 17
            if( array_key_exists(self::EXEC_AFTER, $transition) ) {
154 16
                $after = $transition[self::EXEC_AFTER];
155 16
                if ($after) {
156 1
                    ($after)($this);
157
                }
158
            }
159
        }
160
161 18
        if( $this->cancelTransition || $wasGuarded ){
162 6
            $this->cancelTransition = false;
163
        } else {
164 13
            $this->currentState = $transition[self::NEXT_STATE];
165
        }
166
167 18
        $this->transitionInProgress = false;
168 18
        $event = array_shift($this->eventsQueued);
169 18
        if(  $event != null ){
170 2
            $this->fireEvent($event);
171
        }
172
173 18
        return $this;
174
    }
175
176 2
    public function can($event)
177
    {
178
        try{
179 2
            $this->eventMustExistOrFail($event);
180 1
        } catch(\InvalidArgumentException $e){
181 1
            return false;
182
        }
183
184 2
        $transition = $this->sm[$this->currentState][$event];
185 2
        if( ! array_key_exists(self::EXEC_GUARD, $transition)){
186 1
            return true;
187
        }
188
189 2
        $can = true;
190 2
        $guard = $transition[self::EXEC_GUARD];
191 2
        if( $guard ){
192 1
            if( ($guard)($this) === false )
193 1
                $can = false;
194
        }
195 2
        return $can;
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 32
    private function argumentIsValidOrFail($arg): void
233
    {
234 32
        $this->argumentIsNotNullOrFail($arg);
235 31
        $this->argumentIsNotBlankOrFail($arg);
236 30
    }
237
238 32
    private function argumentIsNotNullOrFail($arg): void
239
    {
240 32
        if( $arg === null )
241 5
            throw new \InvalidArgumentException("Null is not an valid argument");
242 31
    }
243
244 31
    private function argumentIsNotBlankOrFail($arg): void
245
    {
246 31
        if( trim($arg) === "" )
247 2
            throw new \InvalidArgumentException("Blank is not an valid argument");
248 30
    }
249
250 22
    private function eventMustExistOrFail($event)
251
    {
252 22
        if( !( isset($this->sm[$this->currentState][$event]) ) )
253 2
            throw new \InvalidArgumentException("Unexpected event '{$event}' on '{$this->currentState}' state");
254 21
    }
255
256 20
    private function stateMustExistOrFail($state)
257
    {
258 20
        if( ! isset($this->sm[$state]) )
259 1
            throw new \InvalidArgumentException("Event '{$this->currentEvent}' fired an unexpected '{$state}' state");
260 19
    }
261
262 30
    private function setCurrentStateIfThisIsInitialState($state): void
263
    {
264 30
        if( $this->currentState == null){
265 30
            $this->currentState = $state;
266
        }
267 30
    }
268
}
269