Skip to content

Instantly share code, notes, and snippets.

@mtvbrianking
Last active October 30, 2021 17:07
Show Gist options
  • Save mtvbrianking/eea8b7c7edc07e8914feafc702577d5c to your computer and use it in GitHub Desktop.
Save mtvbrianking/eea8b7c7edc07e8914feafc702577d5c to your computer and use it in GitHub Desktop.
State Machines
<?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);
@mtvbrianking
Copy link
Author

mtvbrianking commented Oct 25, 2021

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);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment