Last active
October 30, 2021 17:07
-
-
Save mtvbrianking/eea8b7c7edc07e8914feafc702577d5c to your computer and use it in GitHub Desktop.
State Machines
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
interface StateMachineInterface | |
{ | |
public function getTransitions(): array; | |
public function setTransitions(array $transitions): void; | |
public function getState(): string; | |
public function setState(string $state): void; | |
public function getStates(): array; | |
public function getActions(): array; | |
public function hasState(string $state): bool; | |
public function hasAction(string $action): bool; | |
public function next(?string $from = null); | |
public function nextActions(?string $from = null): array; | |
public function nextStates(?string $from = null): array; | |
public function canTrigger(string $action, ?string $from = null): bool; | |
public function trigger(string $action): void; | |
public function canTransitionTo(string $target, ?string $from = null): bool; | |
public function transitionTo(string $target): void; | |
} | |
class StateMachine implements StateMachineInterface | |
{ | |
protected string $state = 'initiated'; | |
protected array $transitions = [ | |
'initiated' => [], | |
]; | |
public function __construct(?string $state = '') | |
{ | |
if (! $state) { | |
return; | |
} | |
if (! $this->hasState($state)) { | |
throw new \Exception("Invalid state '{$state}'."); | |
} | |
$this->state = $state; | |
} | |
public function __toString(): string | |
{ | |
return $this->state; | |
} | |
public function getTransitions(): array | |
{ | |
return $this->transitions; | |
} | |
public function setTransitions(array $transitions): void | |
{ | |
trigger_error('Resets the state machine.', E_USER_WARNING); | |
$this->transitions = $transitions; | |
$this->state = array_keys($transitions)[0]; | |
} | |
public function getState(): string | |
{ | |
return $this->state; | |
} | |
public function setState(string $state): void | |
{ | |
trigger_error('Undermines the state machine transitions.', E_USER_WARNING); | |
if (! $this->hasState($state)) { | |
throw new \Exception("Invalid state '{$state}'."); | |
} | |
$this->state = $state; | |
} | |
public function getStates(): array | |
{ | |
return array_keys($this->transitions); | |
} | |
public function getActions(): array | |
{ | |
$actions = []; | |
foreach ($this->transitions as $state => $next) { | |
$actions = array_merge($actions, array_column($next, 'action')); | |
} | |
return array_unique($actions); | |
} | |
public function hasState(string $state): bool | |
{ | |
return in_array($state, $this->getStates()); | |
} | |
public function hasAction(string $action): bool | |
{ | |
return in_array($action, $this->getActions()); | |
} | |
public function next(?string $from = null) | |
{ | |
$after = $from ?? $this->state; | |
if (array_search($after, array_keys($this->transitions)) === false) { | |
throw new \Exception("Invalid state '{$after}'."); | |
} | |
return $this->transitions[$after] ?? []; | |
} | |
public function nextActions(?string $from = null): array | |
{ | |
$next = $this->next($from); | |
return array_map(function ($step) { | |
return $step['action']; | |
}, $next); | |
} | |
public function nextStates(?string $from = null): array | |
{ | |
$next = $this->next($from); | |
return array_map(function ($step) { | |
return $step['target']; | |
}, $next); | |
} | |
public function canTrigger(string $action, ?string $from = null): bool | |
{ | |
$next = $this->next($from); | |
return array_search($action, array_column($next, 'action')) !== false; | |
} | |
public function trigger(string $action): void | |
{ | |
$next = $this->next(); | |
$idx = array_search($action, array_column($next, 'action')); | |
if ($idx === false) { | |
$state = $from ?? $this->state; | |
throw new \Exception("Can't trigger '{$action}' on '{$state}'."); | |
} | |
$step = $next[$idx]; | |
$this->state = $step['target']; | |
} | |
public function canTransitionTo(string $target, ?string $from = null): bool | |
{ | |
$next = $this->next($from); | |
return array_search($target, array_column($next, 'target')) !== false; | |
} | |
public function transitionTo(string $target): void | |
{ | |
$next = $this->next(); | |
$idx = array_search($target, array_column($next, 'target')); | |
if ($idx === false) { | |
$state = $from ?? $this->state; | |
throw new \Exception("Can't transition to '{$target}' from '{$state}'."); | |
} | |
$step = $next[$idx]; | |
$this->state = $step['target']; | |
} | |
} | |
class PaperStateMachine extends StateMachine | |
{ | |
const CREATED = 'created'; | |
const PREPARED = 'prepared'; | |
const REVIEWED = 'reviewed'; | |
const REJECTED = 'rejected'; | |
const APPROVED = 'approved'; | |
protected string $state = self::CREATED; | |
protected array $transitions = [ | |
self::CREATED => [ | |
[ | |
'action' => 'prepare', | |
'target' => self::PREPARED, | |
'permission' => 'prepare papers', | |
], | |
], | |
self::PREPARED => [ | |
[ | |
'action' => 'recall', | |
'target' => self::CREATED, | |
'permission' => 'create papers', | |
], | |
[ | |
'action' => 'review', | |
'target' => self::REVIEWED, | |
'permission' => 'review papers', | |
], | |
], | |
self::REVIEWED => [ | |
[ | |
'action' => 'reject', | |
'target' => self::REJECTED, | |
'permission' => 'reject papers', | |
], | |
[ | |
'action' => 'approve', | |
'target' => self::APPROVED, | |
'permission' => 'approve papers', | |
], | |
], | |
self::REJECTED => [ | |
[ | |
'action' => 'retry', | |
'target' => self::PREPARED, | |
'permission' => 'prepare papers', | |
], | |
], | |
self::APPROVED => [], | |
]; | |
} | |
$paperStateMachine = new PaperStateMachine(); | |
echo json_encode($paperStateMachine->getTransitions(), JSON_PRETTY_PRINT); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
InvalidStateException("Unknown state: recalled")
InvalidActionException("Action approve is not allowed on the rejected state.")
InvalidTransitionException("Transition from the approved to the created state is not permitted.")
$stateMachine->getEvents();
$stateMachine->getEvent($eventId);
$stateMachine->loadEvents($events);
$stateMachine->logEvent($state, $action, $meta);