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
![]() |
|||
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 |