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 stateMachineRules() method and call initStateMachine() method at initialization (__contruct() method).

Example

Check MyStatefulClass.php and Usage.php.

License

MIT

Proof of concept. Use At Your Own Risk

<?php
use Finite\StateMachine\StateMachine, Finite\State\State, Finite\Event\FiniteEvents,
Finite\Event\TransitionEvent, Finite\StateMachine\ListenableStateMachine;
/**
* 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 stateMachineRules() method
* and call initStateMachine() method at initialization (__contruct() method)
*
* @author Tortue Torche <tortuetorche@spam.me>
*/
trait FiniteStateMachine
{
/**
* @var Finite\StateMachine\StateMachine
*/
protected $stateMachine;
/**
* @var array
*/
protected $finiteConfig;
/**
* @param Finite\StateMachine\StateMachine $sm
*/
public function initFiniteEventHandlers(Finite\StateMachine\StateMachine $sm)
{
$dispatcher = new Symfony\Component\EventDispatcher\EventDispatcher;
$sm->setEventDispatcher($dispatcher);
$self = $this;
$dispatcher->addListener(FiniteEvents::PRE_TRANSITION, function(TransitionEvent $e) use($self) {
$this->fireTransitionCallbacks( 'before', $e->getTransition(), [ $self, $e->getTransition() ] );
});
$dispatcher->addListener(FiniteEvents::POST_TRANSITION, function(TransitionEvent $e) use($self) {
$this->fireTransitionCallbacks( 'after', $e->getTransition(), [ $self, $e->getTransition() ] );
});
}
/**
* @return array
*/
protected abstract function stateMachineRules();
public function initStateMachine() {
$this->finiteConfig = $this->stateMachineRules();
$loader = new Finite\Loader\ArrayLoader($this->finiteConfig);
$sm = new ListenableStateMachine($this);
$this->initFiniteEventHandlers($sm);
$loader->load($sm);
$sm->initialize();
$this->stateMachine = $sm;
}
/**
* Reserved words: 'on', 'all', 'any', 'do', '-'(dash character, just after 'all' and 'any' keywords) and ','(comma character)
*
* @param string $type
* @param Finite\Transition\TransitionInterface $transition
* @param array $payload
*/
protected function fireTransitionCallbacks($type, Finite\Transition\TransitionInterface $transition, $payload = [])
{
// Previous States
$from = $transition->getInitialStates();
// Next/Current State
$to = $transition->getState();
// Current Transition(Event)
$event = $transition->getName();
if( array_key_exists($type, $this->finiteConfig['callbacks']) ){
$callbacks = $this->finiteConfig['callbacks'][$type];
$allStates = $this->getStateMachine()->getStates();
foreach ($callbacks as $callback) {
$result = null;
if( array_key_exists('on', $callback) && $callback['on'] === $event ) {
$result = $callback['do'];
} else {
foreach ($callback as $key => $value) {
// TODO: DRY
if( preg_match('/^(all|any)(\s?\-\s?(.+))?$/', $key, $matchesKeys) ) {
$key = null;
if( array_key_exists(3, $matchesKeys) ) {
$key = $matchesKeys[3];
}
$key = implode(",", array_diff( $allStates, explode(",", $key) ) );
}
if( is_string($value) ) {
if( preg_match('/^(all|any)(\s?\-\s?(.+))?$/', $value, $matchesValues) ) {
$value = null;
if( array_key_exists(3, $matchesValues) ) {
$value = $matchesValues[3];
}
$value = implode(",", array_diff( $allStates, explode(",", $value) ) );
}
}
if ( $key !== 'do' && count( array_intersect( explode(",", $key), (array) $from ) ) > 0 && in_array( $to, explode(",", $value) ) ) {
$result = $callback['do'];
}
}
}
if($result) {
if( is_callable($callback['do']) ) { // Global or anonymous function
call_user_func_array($callback['do'], $payload);
} else { // Instance method
// Send only the transition object to method's parameters
call_user_func_array([$this, $callback['do']], [$payload[1]]);
}
}
}
}
}
public function setFiniteState($state)
{
$this->state = $state;
}
public function getFiniteState()
{
return $this->state;
}
/**
* @return Finite\StateMachine\StateMachine
*/
protected 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 array<string>
*/
public function getTransitions()
{
return $this->getCurrentState()->getTransitions();
}
/**
* @return array<string>
*/
public function getProperties()
{
return $this->getCurrentState()->getProperties();
}
/**
* @param string $property
*
* @return bool
*/
public function hasProperty($property)
{
return $this->getCurrentState()->has($property);
}
// Helpers
/**
* @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);
}
}
<?php
class MyStatefulClass implements Finite\StatefulInterface
{
use FiniteStateMachine;
public function __construct()
{
$this->initStateMachine();
}
protected function stateMachineRules() {
return [
//'class' => get_called_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' => 'beforeTransitionT12'],
['s2' => 's3', 'do' => function($myStatefulInstance, $transition) {
echo "Before callback from 's2' to 's3'";// debug
}
],
['all-s3' => 's3,s1', 'do' => 'fromStatesS1S2ToS1S3'],
],
'after' => [
['all' => 'all', 'do' => 'afterAllTransitions'],
],
],
];
}
public function beforeTransitionT12($transition) {
echo "Function called before transition: '".$transition->getName()."' !";// debug
}
public function fromStatesS1S2ToS1S3()
{
echo "Before callback from states 's1' or 's2' to 's1' or 's3'";// debug
}
public function afterAllTransitions($transition) {
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
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
Copy link

yohang commented Aug 29, 2013

@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