jagarsoft /
php-state-machine
| 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
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 |