|
<?php |
|
|
|
use Finite\StateMachine\StateMachine; |
|
|
|
/** |
|
* The FiniteStateMachine Trait. |
|
* It provides easy ways to deal with Stateful objects and StateMachine |
|
* Prerequisite: install Finite package (https://github.com/yohang/Finite#readme) |
|
* Usage: in your Stateful Class, add the stateMachineConfig() protected method |
|
* and call initStateMachine() method at initialization (__contruct() method) |
|
* |
|
* @author Tortue Torche <[email protected]> |
|
*/ |
|
trait FiniteStateMachineTrait |
|
{ |
|
/** |
|
* @var \Finite\StateMachine\StateMachine |
|
*/ |
|
protected $stateMachine; |
|
|
|
/** |
|
* @var array |
|
*/ |
|
protected $finiteLoader; |
|
|
|
/** |
|
* @return array |
|
*/ |
|
protected abstract function stateMachineConfig(); |
|
|
|
protected function initStateMachine(array $config = null) |
|
{ |
|
$this->finiteLoader = $config ?: $this->stateMachineConfig(); |
|
$loader = new \Finite\Loader\ArrayLoader($this->finiteLoader); |
|
$sm = new StateMachine($this); |
|
|
|
$loader->load($sm); |
|
|
|
$sm->initialize(); |
|
$this->stateMachine = $sm; |
|
} |
|
|
|
/** |
|
* Sets the object state |
|
* |
|
* @param string $state |
|
*/ |
|
public function setFiniteState($state) |
|
{ |
|
$this->state = $state; |
|
} |
|
|
|
/** |
|
* Get the object state |
|
* |
|
* @return string |
|
*/ |
|
public function getFiniteState() |
|
{ |
|
return $this->state; |
|
} |
|
|
|
/** |
|
* @return \Finite\StateMachine\StateMachine |
|
*/ |
|
public function getStateMachine() |
|
{ |
|
return $this->stateMachine; |
|
} |
|
|
|
/** |
|
* @return \Finite\State\State |
|
*/ |
|
public function getCurrentState() |
|
{ |
|
return $this->getStateMachine()->getCurrentState(); |
|
} |
|
|
|
/** |
|
* @return string |
|
*/ |
|
public function getState() |
|
{ |
|
return $this->getCurrentState()->getName(); |
|
} |
|
|
|
/** |
|
* @return string |
|
*/ |
|
public function getHumanState() |
|
{ |
|
return $this->humanize($this->getState()); |
|
} |
|
|
|
/** |
|
* @param string $transitionName |
|
* |
|
* @return string|null |
|
*/ |
|
public function getHumanStateTransition($transitionName) |
|
{ |
|
$transitionIndex = array_search($transitionName, $this->getTransitions()); |
|
if ($transitionIndex !== null && $transitionIndex !== false) { |
|
return $this->humanize(array_get($this->getTransitions(), $transitionIndex)); |
|
} |
|
} |
|
|
|
/** |
|
* Returns if this state is the initial state |
|
* |
|
* @return boolean |
|
*/ |
|
public function isInitial() |
|
{ |
|
return $this->getCurrentState()->isInitial(); |
|
} |
|
|
|
/** |
|
* Returns if this state is the final state |
|
* |
|
* @return mixed |
|
*/ |
|
public function isFinal() |
|
{ |
|
return $this->getCurrentState()->isFinal(); |
|
} |
|
|
|
/** |
|
* Returns if this state is a normal state (!($this->isInitial() || $this->isFinal()) |
|
* |
|
* @return mixed |
|
*/ |
|
public function isNormal() |
|
{ |
|
return $this->getCurrentState()->isNormal(); |
|
} |
|
|
|
/** |
|
* Returns the state type |
|
* |
|
* @return string |
|
*/ |
|
public function getType() |
|
{ |
|
return $this->getCurrentState()->getType(); |
|
} |
|
|
|
/** |
|
* @return array<string> |
|
*/ |
|
public function getTransitions() |
|
{ |
|
return $this->getCurrentState()->getTransitions(); |
|
} |
|
|
|
/** |
|
* @return array<string> |
|
*/ |
|
public function getProperties() |
|
{ |
|
return $this->getCurrentState()->getProperties(); |
|
} |
|
|
|
/** |
|
* @param array $properties |
|
*/ |
|
public function setProperties(array $properties) |
|
{ |
|
$this->getCurrentState()->setProperties($properties); |
|
} |
|
|
|
/** |
|
* @param string $property |
|
* |
|
* @return bool |
|
*/ |
|
public function hasProperty($property) |
|
{ |
|
return $this->getCurrentState()->has($property); |
|
} |
|
|
|
/** |
|
* @param string $targetState |
|
* |
|
* @return bool |
|
*/ |
|
public function is($targetState) |
|
{ |
|
return $this->getState() === $targetState; |
|
} |
|
|
|
/** |
|
* @param string $transitionName |
|
* |
|
* @return bool |
|
*/ |
|
public function can($transitionName) |
|
{ |
|
return $this->getStateMachine()->can($transitionName); |
|
} |
|
|
|
/** |
|
* @param string $transitionName |
|
* |
|
* @return mixed |
|
* |
|
* @throws \Finite\Exception\StateException |
|
*/ |
|
public function apply($transitionName) |
|
{ |
|
return $this->getStateMachine()->apply($transitionName); |
|
} |
|
|
|
/** |
|
* @param callable $callback |
|
* @param array $spec |
|
*/ |
|
public function addBefore($callback, array $spec = []) |
|
{ |
|
$callbackHandler = new \Finite\Event\CallbackHandler($this->getStateMachine()->getDispatcher()); |
|
$callbackHandler->addBefore($this->getStateMachine(), $callback, $spec); |
|
} |
|
|
|
/** |
|
* @param callable $callback |
|
* @param array $spec |
|
*/ |
|
public function addAfter($callback, array $spec = []) |
|
{ |
|
$callbackHandler = new \Finite\Event\CallbackHandler($this->getStateMachine()->getDispatcher()); |
|
$callbackHandler->addAfter($this->getStateMachine(), $callback, $spec); |
|
} |
|
|
|
/** |
|
* @param callable $callback |
|
* @param array $spec |
|
*/ |
|
public function prependBefore($callback, array $spec = []) |
|
{ |
|
$config = $this->finiteLoader; |
|
array_set($config, 'callbacks.before', array_merge( |
|
[array_merge($spec, ['do' => $callback])], |
|
array_get($config, 'callbacks.before', []) |
|
)); |
|
$this->initStateMachine($config); |
|
} |
|
|
|
/** |
|
* @param callable $callback |
|
* @param array $spec |
|
*/ |
|
public function prependAfter($callback, array $spec = []) |
|
{ |
|
$config = $this->finiteLoader; |
|
array_set($config, 'callbacks.after', array_merge( |
|
[array_merge($spec, ['do' => $callback])], |
|
array_get($config, 'callbacks.after', []) |
|
)); |
|
$this->initStateMachine($config); |
|
} |
|
|
|
/** |
|
* Find and return the Initial state if exists |
|
* |
|
* @return string |
|
* |
|
* @throws Exception\StateException |
|
*/ |
|
public function findInitialState() |
|
{ |
|
foreach (get_property($this->getStateMachine(), 'states') as $state) { |
|
if (\Finite\State\State::TYPE_INITIAL === $state->getType()) { |
|
return $state->getName(); |
|
} |
|
} |
|
|
|
throw new \Finite\Exception\StateException('No initial state found.'); |
|
} |
|
|
|
/** |
|
* |
|
* @param string $attribute Attribute name who contains transition name |
|
* @param string $errorMessage |
|
* @return mixed Returns false if there are errors |
|
*/ |
|
public function applyStateTransition($attribute = null, $errorMessage = null) |
|
{ |
|
$attribute = $attribute ?: 'state_transition'; |
|
$attributes = $this->getAttributes(); |
|
if (($stateTransition = array_get($attributes, $attribute))) { |
|
if ($this->can($stateTransition)) { |
|
unset($this->$attribute); |
|
$this->apply($stateTransition); |
|
} else { |
|
$defaultErrorMessage = sprintf( |
|
'The "%s" transition can not be applied to the "%s" state.', |
|
$stateTransition, |
|
$this->getState() |
|
); |
|
\Log::error($defaultErrorMessage); |
|
$errorMessage = $errorMessage ?: $defaultErrorMessage; |
|
if (method_exists($this, 'errors')) { |
|
$this->errors()->add($attribute, $errorMessage); |
|
} else { |
|
throw new \Exception($errorMessage, 1); |
|
} |
|
|
|
return false; |
|
} |
|
} |
|
} |
|
|
|
/** |
|
* $this->humanize("my beautiful hat");//-> 'My beautiful hat' |
|
* |
|
* @param string $value |
|
* @return string |
|
*/ |
|
protected function humanize($value) |
|
{ |
|
return ucfirst(snake_case(camel_case($value), ' ')); |
|
} |
|
} |
I'm a bit busy at the moment, but I'll try to do this in September.
Why not, my only wish is to keep the ability to add multiple states and exclude some of the states
['all-s2' => 's1,s2']
. Because it's a really cool feature 😄 (credits toStateMachine
gem).