Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
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
@yohang

This comment has been minimized.

Copy link

yohang commented Aug 28, 2013

Hi,

I like the fact to provide shortcuts for the StateMachine use. Moreover traits allow in this case to take, or not, the feature.

But there is some things that i don't like :

  • All in one trait : I see 3 ≠ goals here, I'd prefer to see 3 different traits for that.
    • Define state graph configuration directly in stateful object
    • Embed StateMachine main methods in the object
    • Provide simplified event system
  • The way callbacks are defined : PHP provides a way to define callbacks, please use it, it'll simplify your code, and improve readability. ([$this, 'afterAllTransitions'] instead of 'afterAllTransitions')
  • If we add a simple way to define callbacks, why put this in a trait ? This can be useable everywhere !

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

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

@yohang

This comment has been minimized.

Copy link

yohang commented Aug 29, 2013

@tortuetorche

This comment has been minimized.

Copy link
Owner Author

tortuetorche commented Aug 29, 2013

  • 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

This comment has been minimized.

Copy link
Owner Author

tortuetorche commented Aug 29, 2013

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

This comment has been minimized.

Copy link
Owner Author

tortuetorche commented Aug 29, 2013

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

This comment has been minimized.

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

This comment has been minimized.

Copy link

yohang commented Aug 29, 2013

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

@tortuetorche

This comment has been minimized.

Copy link
Owner Author

tortuetorche commented Aug 30, 2013

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

This comment has been minimized.

Copy link

ihsaneddin commented Jun 6, 2015

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

This comment has been minimized.

Copy link
Owner Author

tortuetorche commented Oct 9, 2015

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

This comment has been minimized.

Copy link
Owner Author

tortuetorche commented Jul 12, 2016

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
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.