Skip to content

Instantly share code, notes, and snippets.

@tortuetorche
Last active October 12, 2017 23:06
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save tortuetorche/6365575 to your computer and use it in GitHub Desktop.
Save tortuetorche/6365575 to your computer and use it in GitHub Desktop.
FiniteStateMachine Trait with Example.Add a DSL to the PHP Finite package, borrowed from the Ruby StateMachine gem.

FiniteStateMachine Trait

Add a DSL to the PHP Finite package, borrowed from the Ruby StateMachine gem.

Usage

In your Stateful Class, add the stateMachineConfig() method and call initStateMachine() method at initialization (__contruct() method).

Example

Check MyStatefulClass.php and Usage.php.

Versions

My previous version of this DSL can be found here

License

MIT

Use At Your Own Risk

<?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 <tortuetorche@spam.me>
*/
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), ' '));
}
}
<?php
class MyStatefulClass implements Finite\StatefulInterface
{
use FiniteStateMachine;
public function __construct()
{
$this->initStateMachine();
}
protected function stateMachineConfig()
{
return [
//'class' => get_class(),//useful?
'states' => [
's1' => [
'type' => 'initial',
'properties' => ['deletable' => true, 'editable' => true],
],
's2' => [
'type' => 'normal',
'properties' => [],
],
's3' => [
'type' => 'final',
'properties' => [],
]
],
'transitions' => [
't12' => ['from' => ['s1'], 'to' => 's2'],
't23' => ['from' => ['s2'], 'to' => 's3'],
't21' => ['from' => ['s2'], 'to' => 's1'],
],
'callbacks' => [
'before' => [
['on' => 't12', 'do' => [$this, 'beforeTransitionT12']],
['from' => 's2', 'to' => 's3', 'do' => function ($myStatefulInstance, $transitionEvent) {
echo "Before callback from 's2' to 's3'";// debug
}],
['from' => '-s3', 'to' => ['s3' ,'s1'], 'do' => [$this, 'fromStatesS1S2ToS1S3']],
],
'after' => [
['from' => 'all', 'to' => 'all', 'do' => [$this, 'afterAllTransitions']],
],
],
];
}
public function beforeTransitionT12($myStatefulInstance, $transitionEvent)
{
echo "Function called before transition: '".$transitionEvent->getTransition()->getName()."' !";// debug
}
public function fromStatesS1S2ToS1S3()
{
echo "Before callback from states 's1' or 's2' to 's1' or 's3'";// debug
}
public function afterAllTransitions($myStatefulInstance, $transitionEvent)
{
echo "After All Transitions !";// debug
}
}
<?php
$myStatefulObject = new MyStatefulClass;
$myStatefulObject->getState(); // → "s1"
$myStatefulObject->can('t23'); // → false
$myStatefulObject->can('t12'); // → true
$myStatefulObject->apply('t12'); // → NULL
$myStatefulObject->is('s2'); // → true
@tortuetorche
Copy link
Author

  • All in one trait : I see 3 ≠ goals here, I'd prefer to see 3 different traits for that.

@yohang You're right. I've coded this Trait very quickly, as a proof of concept, but it should be separated in sub-traits or class.

@tortuetorche
Copy link
Author

PHP provides a way to define callbacks, please use it, it'll simplify your code, and improve readability. ([$this, 'afterAllTransitions'] instead of 'afterAllTransitions')

I'm not sure to understand.

You means, doing this :

<?php
['all' => 'all', 'do' => [$this, 'afterAllTransitions'] ],

instead of this ? :

<?php
['all' => 'all', 'do' => 'afterAllTransitions'],

@tortuetorche
Copy link
Author

I'd be proud to receive a WIP PR for working on these features.

I'm a bit busy at the moment, but I'll try to do this in September.

To simplify your callback handling, i can work a bit on the actual event system (for example, firing a distinct event for each transition).

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 to StateMachine gem).

@yohang
Copy link

yohang commented Aug 29, 2013

I'm not sure to understand.

You means, doing this :

<?php
['all' => 'all', 'do' => [$this, 'afterAllTransitions'] ],

instead of this ? :

<?php
['all' => 'all', 'do' => 'afterAllTransitions'],

Exactly.

If afterAllTransitions is a valid (and accessible) method of $this, [$this, 'afterAllTransitions'] will pass callable typehints, return true passed to is_callable, etc.. 'afterAllTransitions' won't

@yohang
Copy link

yohang commented Aug 29, 2013

@tortuetorche as the callback part seems very useful, i'm developing it.

@tortuetorche
Copy link
Author

If afterAllTransitions is a valid (and accessible) method of $this, [$this, 'afterAllTransitions'] will pass callable typehints, return true passed to is_callable, etc.. 'afterAllTransitions' won't

@yohang It's true that use [$this, 'afterAllTransitions'] could simplify the code. But I think it's less readable for developer who just uses Finite library without digging into its code.

@ihsaneddin
Copy link

I agree with @yohang, callback should be <?php ['all' => 'all', 'do' => 'callbackfunction'], no need to pass it self to it self public method params I think.

@tortuetorche
Copy link
Author

Hi @imaduddinzanky,

You mean, you're agree with me or am I wrong?

After @yohang suggestion, I updated my gist to use his syntax:

  ['all' => 'all', 'do' => [$this, 'afterAllTransitions']],

@tortuetorche
Copy link
Author

Hi folks,

Here a version of FiniteStateMachine and FiniteAuditTrail traits for Laravel 5.1 : https://gist.github.com/tortuetorche/d5a317c8661af7db67396022117c14f3

It isn't complete yet.
But it's a good start 😼

Have a good day,
Tortue Torche

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