From 3b5a291de9f4d82025b1a72574bef3668933fa3d Mon Sep 17 00:00:00 2001 From: Dominique FOURNIER Date: Wed, 21 Jul 2021 16:11:43 +0200 Subject: [PATCH] Add StateMachine support --- src/StateMachine.php | 131 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 src/StateMachine.php diff --git a/src/StateMachine.php b/src/StateMachine.php new file mode 100644 index 0000000..2b67c37 --- /dev/null +++ b/src/StateMachine.php @@ -0,0 +1,131 @@ + + * @license BSD + */ + +namespace Domframework; + +/** + * This class manage a state machine. + * Define the states and the associated transitions. + * The states and the transitions are methods + * The states are the actions. + * The transitions are tests : return false if the test do not match, or true + * if the test match. The state machine will skip the test if it doesn't match, + * and go to the define toStateName if the test return true. + * Officially, the transitions should not have priority. In practise, the + * transistions are tested in the added order. + */ +class StateMachine +{ + private $states = array(); + private $transitions = array(); + private $debug = false; + + public function addState(string $stateName, callable $methodName): self + { + $this->states[$stateName] = new StateMachineState($methodName); + return $this; + } + + public function addTransition(string $transitionName, string $fromStateName, string $toStateName, callable $methodName): self + { + if (! key_exists($fromStateName, $this->states)) + throw new \Exception("StateMachine can not add Transition from '$fromStateName' : state not defined", 404); + if (! key_exists($toStateName, $this->states)) + throw new \Exception("StateMachine can not add Transition to '$toStateName' : state not defined", 404); + if (key_exists($transitionName, $this->transitions)) + throw new \Exception("StateMachine can not add Transition '$transitionName' : transition already defined", 406); + $this->transitions[$transitionName] = new StateMachineTransition($fromStateName, $toStateName, $methodName); + return $this; + } + + /** + * Start the state machine at the provided state + * Will run until no transition match, then return + */ + public function run(string $stateName) + { + if (! key_exists ($stateName, $this->states)) + throw new \Exception("StateMachine can not runState '$stateName' : state not defined", 404); + while(1) + { + $this->debug("Start state '$stateName'"); + $rc = $this->states[$stateName]->run(); + $this->debug("End state '$stateName' with type '".gettype($rc)."'"); + foreach($this->transitions as $transitionName => $transition) + { + if ($transition->getFromStateName() !== $stateName) + continue; + if ($transition->run() === false) + { + $this->debug("Look at transition '$transitionName' : Return FALSE (not match)"); + continue; + } + $this->debug("Look at transition '$transitionName' : Match"); + $stateName = $transition->getToStateName(); + continue 2; + } + break; + } + return $rc; + } + + private function debug(string $msg) + { + if ($this->debug === false) + return; + echo "$msg\n"; + } +} + +class StateMachineState +{ + private $methodName; + + public function __construct(callable $methodName) + { + $this->methodName = $methodName; + } + + public function run() + { + return call_user_func($this->methodName); + } +} + +class StateMachineTransition +{ + private $fromStateName; + private $toStateName; + private $methodName; + + public function __construct(string $fromStateName, string $toStateName, callable $methodName) + { + $this->fromStateName = $fromStateName; + $this->toStateName = $toStateName; + $this->methodName = $methodName; + } + + public function getFromStateName(): string + { + return $this->fromStateName; + } + + public function getToStateName(): string + { + return $this->toStateName; + } + + public function getMethodName(): callable + { + return $this->methodName; + } + + public function run() + { + return call_user_func($this->methodName); + } +}