|
<?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), ' ')); |
|
} |
|
} |
@tortuetorche