This commit is contained in:
Xes
2025-08-14 22:41:49 +02:00
parent 2de81ccc46
commit 8ce45119b6
39774 changed files with 4309466 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
vendor
bin
composer.lock

35
vendor/winzou/state-machine/.travis.yml vendored Normal file
View File

@@ -0,0 +1,35 @@
sudo: false
branches:
only:
- master
language: php
php:
- 5.3
- 5.4
- 5.5
- 5.6
- 7.0
- 7.1
- 7.2
- hhvm
matrix:
fast_finish: true
include:
- php: 5.3
dist: precise
allow_failures:
- php: 5.3
dist: xenial
- php: 5.3
dist: trusty
before_script:
- if [[ $TRAVIS_PHP_VERSION != hhvm ]]; then phpenv config-rm xdebug.ini; fi;
- composer --no-interaction --prefer-source install
script:
- bin/phpspec run -f dot

19
vendor/winzou/state-machine/LICENSE vendored Normal file
View File

@@ -0,0 +1,19 @@
Copyright (c) 2014 Alexandre Bacco
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

107
vendor/winzou/state-machine/README.md vendored Normal file
View File

@@ -0,0 +1,107 @@
A very lightweight yet powerful PHP state machine
=================================================
Define your states, define your transitions and your callbacks: we do the rest.
The era of hard-coded states is over!
[![Build Status](https://travis-ci.org/winzou/state-machine.svg?branch=master)](https://travis-ci.org/winzou/state-machine)
Installation (via composer)
---------------
```js
{
"require": {
"winzou/state-machine": "~0.1"
}
}
```
Usage
-----
### Configure a state machine graph
In order to use the state machine, you first need to define a graph. A graph is a definition of states, transitions and optionnally callbacks ; all attached on an object from your domain. Multiple graphes can be attached to the same object.
Let's define a graph called *myGraphA* for our `DomainObject` object:
```php
$config = array(
'graph' => 'myGraphA', // Name of the current graph - there can be many of them attached to the same object
'property_path' => 'stateA', // Property path of the object actually holding the state
'states' => array(
'checkout',
'pending',
'confirmed',
'cancelled'
),
'transitions' => array(
'create' => array(
'from' => array('checkout'),
'to' => 'pending'
),
'confirm' => array(
'from' => array('checkout', 'pending'),
'to' => 'confirmed'
),
'cancel' => array(
'from' => array('confirmed'),
'to' => 'cancelled'
)
),
'callbacks' => array(
'guard' => array(
'guard-cancel' => array(
'to' => array('cancelled'), // Will be called only for transitions going to this state
'do' => function() { var_dump('guarding to cancelled state'); return false; }
)
),
'before' => array(
'from-checkout' => array(
'from' => array('checkout'), // Will be called only for transitions coming from this state
'do' => function() { var_dump('from checkout transition'); }
)
),
'after' => array(
'on-confirm' => array(
'on' => array('confirm'), // Will be called only on this transition
'do' => function() { var_dump('on confirm transition'); }
),
'to-cancelled' => array(
'to' => array('cancelled'), // Will be called only for transitions going to this state
'do' => function() { var_dump('to cancel transition'); }
),
'cancel-date' => array(
'to' => array('cancelled'),
'do' => array('object', 'setCancelled'),
),
)
)
);
```
So, in the previous example, the graph has 6 possible states, and those can be achieved by applying some transitions to the object. For example, when creating a new `DomainObject`, you would apply the 'create' transition to the object, and after that the state of it would become *pending*.
### Using the state machine
#### Definitions
The state machine is the object actually manipulating your object. By using the state machine you can test if a transition can be applied, actually apply a transition, retrieve the current state, etc. *A state machine is specific to a couple object + graph.* It means that if you want to manipulate another object, or the same object with another graph, *you need another state machine*.
The factory helps you to get the state machine for these couples object + graph. You give an object and a graph name to it, and it will return you the state machine for this couple. If you want to have this factory as a service in your Symfony2 application, please see the [corresponding StateMachineBundle](https://github.com/winzou/StateMachineBundle).
#### Usage
Please refer to the several examples in the `examples` folder.
#### Callbacks
Callbacks are used to guard transitions or execute some code before or after applying transitions.
Guarding callbacks must return a `bool`. If a guard returns `false`, a transition cannot be performed.
##### Credits
This library has been highly inspired by [https://github.com/yohang/Finite](https://github.com/yohang/Finite), but has taken another direction.

View File

@@ -0,0 +1,37 @@
{
"name": "winzou/state-machine",
"description": "A very lightweight yet powerful PHP state machine",
"keywords": ["statemachine", "state", "event", "callback"],
"homepage": "https://github.com/winzou/StateMachine",
"type": "library",
"license": "MIT",
"authors": [
{
"name": "Alexandre Bacco",
"email": "alexandre.bacco@gmail.com",
"homepage": "http://alex.bacco.fr"
}
],
"require": {
"php": ">=5.3.0",
"symfony/event-dispatcher": "~2.1|~3.0|~4.0",
"symfony/property-access": "~2.1|~3.0|~4.0",
"symfony/expression-language": "~2.4|~3.0|~4.0"
},
"suggest": {
"twig/twig": "Access the state machine in your twig templates (~1.0)"
},
"require-dev": {
"phpspec/phpspec": "~2.0",
"twig/twig": "~1.0"
},
"autoload": {
"psr-0": { "SM": "src/" }
},
"autoload-dev": {
"psr-4": { "spec\\": "spec/" }
},
"config": {
"bin-dir": "bin"
}
}

View File

@@ -0,0 +1,41 @@
<?php
/*
* This file is part of the StateMachine package.
*
* (c) Alexandre Bacco
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
class DomainObject
{
private $stateA = 'checkout';
private $stateB = 'checkout';
public function getStateA()
{
return $this->stateA;
}
public function setStateA($state)
{
$this->stateA = $state;
}
public function getStateB()
{
return $this->stateB;
}
public function setStateB($state)
{
$this->stateB = $state;
}
public function setConfirmedNow()
{
var_dump('I (the object) am set confirmed at '.date('Y-m-d').'.');
}
}

View File

@@ -0,0 +1,109 @@
<?php
/*
* This file is part of the StateMachine package.
*
* (c) Alexandre Bacco
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
require '../vendor/autoload.php';
require 'DomainObject.php';
$config = array(
'graph' => 'myGraphA', // Name of the current graph - there can be many of them attached to the same object
'property_path' => 'stateA', // Property path of the object actually holding the state
'states' => array(
'checkout',
'pending',
'confirmed',
'cancelled'
),
'transitions' => array(
'create' => array(
'from' => array('checkout'),
'to' => 'pending'
),
'confirm' => array(
'from' => array('checkout', 'pending'),
'to' => 'confirmed'
),
'cancel' => array(
'from' => array('confirmed'),
'to' => 'cancelled'
)
),
'callbacks' => array(
'guard' => array(
'guard-cancel' => array(
'to' => array('cancelled'), // Will be called only for transitions going to this state
'do' => function() { var_dump('guarding to cancelled state'); return false; }
)
),
'before' => array(
'from-checkout' => array(
'from' => array('checkout'), // Will be called only for transitions coming from this state
'do' => function() { var_dump('from checkout transition'); }
)
),
'after' => array(
'on-confirm' => array(
'on' => array('confirm'), // Will be called only on this transition
'do' => function() { var_dump('on confirm transition'); }
),
'to-cancelled' => array(
'to' => array('cancelled'), // Will be called only for transitions going to this state
'do' => function() { var_dump('to cancel transition'); }
),
'confirm-date' => array(
'on' => array('confirm'),
'do' => array('object', 'setConfirmedNow'), // 'object' will be replaced by the object undergoing the transition
),
)
)
);
// Our object
$object = new DomainObject;
// State machine is created being given an object and a config
$stateMachine = new \SM\StateMachine\StateMachine($object, $config);
// Current state is checkout
var_dump($stateMachine->getState());
// Return true, we can apply this transition
var_dump($stateMachine->can('create'));
// Return true, this transitions is applied
// In addition, callback 'from-checkout' is called
var_dump($stateMachine->apply('create'));
// Current state is pending
var_dump($stateMachine->getState());
// All possible transitions for pending state are just "confirm"
var_dump($stateMachine->getPossibleTransitions());
// Return false, this transition is not applied
// 2nd argument is soft mode: it returns false instead of throwing an exception
var_dump($stateMachine->apply('cancel', true));
// Current state is still pending
var_dump($stateMachine->getState());
// Return true, this transition is applied
// In addition, callback 'on-confirm' is called
// And callback 'confirm-date' calls the method 'setConfirmedNow' on the object itself
var_dump($stateMachine->apply('confirm'));
// Current state is confirmed
var_dump($stateMachine->getState());
// Returns false, as it is guarded
var_dump($stateMachine->can('cancel'));
// Current state is still confirmed
var_dump($stateMachine->getState());

View File

@@ -0,0 +1,163 @@
<?php
namespace spec\SM\Callback;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use SM\Event\TransitionEvent;
use SM\StateMachine\StateMachineInterface;
class CallbackSpec extends ObjectBehavior
{
protected $specs = array();
protected $callable;
protected $sm;
function let(StateMachineInterface $sm)
{
$sm->getState()->willReturn('checkout');
$this->beConstructedWith($this->specs, $this->callable);
}
function it_is_initializable()
{
$this->shouldHaveType('SM\Callback\Callback');
}
function it_satisfies_simple_on(TransitionEvent $event)
{
$specs = array('on' => 'tested-transition');
$this->beConstructedWith($specs, $this->callable);
$event->getConfig()->willReturn($this->getConfig(array('dummy'), 'dummy'));
$event->getTransition()->willReturn('tested-transition');
$event->getState()->willReturn('dummy');
$this->isSatisfiedBy($event)->shouldReturn(true);
}
function it_doesnt_satisfies_simple_on(TransitionEvent $event)
{
$specs = array('on' => 'tested-transition');
$this->beConstructedWith($specs, $this->callable);
$event->getConfig()->willReturn($this->getConfig(array('dummy'), 'dummy'));
$event->getTransition()->willReturn('tested-transition-not-matching');
$this->isSatisfiedBy($event)->shouldReturn(false);
}
function it_satisfies_simple_from(TransitionEvent $event)
{
$specs = array('from' => 'tested-state');
$this->beConstructedWith($specs, $this->callable);
$event->getConfig()->willReturn($this->getConfig(array('tested-state'), 'dummy'));
$event->getTransition()->willReturn('dummy');
$event->getState()->willReturn('tested-state');
$this->isSatisfiedBy($event)->shouldReturn(true);
}
function it_doesnt_satisfies_simple_from(TransitionEvent $event)
{
$specs = array('from' => 'tested-state');
$this->beConstructedWith($specs, $this->callable);
$event->getConfig()->willReturn($this->getConfig(array('tested-state-not-matching'), 'dummy'));
$event->getTransition()->willReturn('dummy');
$event->getState()->willReturn('tested-state-not-matching');
$this->isSatisfiedBy($event)->shouldReturn(false);
}
function it_satisfies_simple_to(TransitionEvent $event)
{
$specs = array('to' => 'tested-state');
$this->beConstructedWith($specs, $this->callable);
$event->getConfig()->willReturn($this->getConfig(array('dummy'), 'tested-state'));
$event->getTransition()->willReturn('dummy');
$event->getState()->willReturn('dummy');
$this->isSatisfiedBy($event)->shouldReturn(true);
}
function it_doesnt_satisfies_simple_to(TransitionEvent $event)
{
$specs = array('from' => 'tested-state');
$this->beConstructedWith($specs, $this->callable);
$event->getConfig()->willReturn($this->getConfig(array('tested-state-not-matching'), 'dummy'));
$event->getTransition()->willReturn('dummy');
$event->getState()->willReturn('dummy');
$this->isSatisfiedBy($event)->shouldReturn(false);
}
function it_satisfies_complex_specs(TransitionEvent $event)
{
$specs = array('to' => 'tested-state-to', 'from' => 'tested-state-from', 'on' => 'tested-transition');
$this->beConstructedWith($specs, $this->callable);
$event->getConfig()->willReturn($this->getConfig(array('tested-state-from'), 'tested-state-to'));
$event->getTransition()->willReturn('tested-transition');
$event->getState()->willReturn('tested-state-from');
$this->isSatisfiedBy($event)->shouldReturn(true);
}
function it_doesnt_satisfies_wrong_from(TransitionEvent $event)
{
$specs = array('to' => 'tested-state-to', 'from' => 'tested-wrong', 'on' => 'tested-transition');
$this->beConstructedWith($specs, $this->callable);
$event->getConfig()->willReturn($this->getConfig(array('dummy'), 'tested-state-to'));
$event->getTransition()->willReturn('tested-transition');
$event->getState()->willReturn('dummy');
$this->isSatisfiedBy($event)->shouldReturn(false);
}
function it_doesnt_satisfies_wrong_to(TransitionEvent $event)
{
$specs = array('to' => 'tested-wrong', 'from' => 'tested-state-from', 'on' => 'tested-transition');
$this->beConstructedWith($specs, $this->callable);
$event->getConfig()->willReturn($this->getConfig(array('tested-state-from'), 'dummy'));
$event->getTransition()->willReturn('tested-transition');
$event->getState()->willReturn('tested-state-from');
$this->isSatisfiedBy($event)->shouldReturn(false);
}
function it_doesnt_satisfies_wrong_on(TransitionEvent $event)
{
$specs = array('to' => 'tested-state-to', 'from' => 'tested-state-from', 'on' => 'tested-wrong');
$this->beConstructedWith($specs, $this->callable);
$event->getConfig()->willReturn($this->getConfig(array('tested-state-from'), 'tested-state-to'));
$event->getTransition()->willReturn('dummy');
$event->getState()->willReturn('tested-state-from');
$this->isSatisfiedBy($event)->shouldReturn(false);
}
function it_doesnt_satisfies_excluded_from(TransitionEvent $event)
{
$specs = array('to' => 'tested-state-to', 'excluded_from' => 'tested-state-from');
$this->beConstructedWith($specs, $this->callable);
$event->getConfig()->willReturn($this->getConfig(array('tested-state-from'), 'tested-state-to'));
$event->getTransition()->willReturn('dummy');
$event->getState()->willReturn('tested-state-from');
$this->isSatisfiedBy($event)->shouldReturn(false);
}
protected function getConfig($from = array(), $to)
{
return array('from' => $from, 'to' => $to);
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace spec\SM\Callback;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use SM\Event\TransitionEvent;
use SM\Factory\FactoryInterface;
use SM\StateMachine\StateMachineInterface;
use spec\SM\DummyObject;
class CascadeTransitionCallbackSpec extends ObjectBehavior
{
function let(FactoryInterface $factory)
{
$this->beConstructedWith($factory);
}
function it_is_initializable()
{
$this->shouldHaveType('SM\Callback\CascadeTransitionCallback');
}
function it_applies($factory, TransitionEvent $event, DummyObject $object, StateMachineInterface $sm)
{
$factory->get($object, 'graph')->willReturn($sm);
$sm->can('transition')->willReturn(true);
$sm->apply('transition', true)->shouldBeCalled();
$this->apply($object, $event, 'transition', 'graph');
}
function it_applies_with_default_graph(
$factory,
TransitionEvent $event,
DummyObject $object,
StateMachineInterface $sm1,
StateMachineInterface $sm2
) {
$event->getStateMachine()->willReturn($sm2);
$sm2->getGraph()->willReturn('graph');
$factory->get($object, 'graph')->willReturn($sm1);
$sm1->can('transition')->willReturn(true);
$sm1->apply('transition', true)->shouldBeCalled();
$this->apply($object, $event, 'transition');
}
function it_applies_with_default_graph_and_default_transition(
$factory,
TransitionEvent $event,
DummyObject $object,
StateMachineInterface $sm1,
StateMachineInterface $sm2
) {
$event->getStateMachine()->willReturn($sm2);
$event->getTransition()->willReturn('transition');
$sm2->getGraph()->willReturn('graph');
$factory->get($object, 'graph')->willReturn($sm1);
$sm1->can('transition')->willReturn(true);
$sm1->apply('transition', true)->shouldBeCalled();
$this->apply($object, $event);
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace spec\SM;
class DummyObject
{
public function getState()
{
}
public function setState($state)
{
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace spec\SM\Extension\Twig;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use SM\Factory\FactoryInterface;
use SM\StateMachine\StateMachineInterface;
use spec\SM\DummyObject;
class SMExtensionSpec extends ObjectBehavior
{
function let(FactoryInterface $factory, StateMachineInterface $stateMachine)
{
$this->beConstructedWith($factory);
$factory->get(new DummyObject(), 'simple')->willReturn($stateMachine);
}
function it_is_initializable()
{
$this->shouldHaveType('SM\Extension\Twig\SMExtension');
}
function it_is_a_twig_extension()
{
$this->shouldBeAnInstanceOf('\Twig_Extension');
}
function it_should_have_a_name()
{
$this->getName()->shouldReturn('sm');
}
function it_provide_sm_can_function(FactoryInterface $factory, StateMachineInterface $stateMachine)
{
$this->can($object = new DummyObject(), 'new', 'simple');
$factory->get($object, 'simple')->shouldHaveBeenCalled();
$stateMachine->can('new')->shouldHaveBeenCalled();
}
function it_provide_sm_getState_function(FactoryInterface $factory, StateMachineInterface $stateMachine)
{
$this->getState($object = new DummyObject(), 'simple');
$factory->get($object, 'simple')->shouldHaveBeenCalled();
$stateMachine->getState()->shouldHaveBeenCalled();
}
function it_provide_sm_getPossibleTransitions_function(FactoryInterface $factory, StateMachineInterface $stateMachine)
{
$this->getPossibleTransitions($object = new DummyObject(), 'simple');
$factory->get($object, 'simple')->shouldHaveBeenCalled();
$stateMachine->getPossibleTransitions()->shouldHaveBeenCalled();
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace spec\SM\Factory;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use SM\Callback\CallbackFactoryInterface;
use spec\SM\DummyObject;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
class FactorySpec extends ObjectBehavior
{
protected $configs = array(
'graph1' => array('state_machine_class' => 'SM\\StateMachine\\StateMachine', 'class' => 'spec\\SM\\DummyObject'),
'graph2' => array('class' => 'spec\\SM\\DummyObject'),
);
function let(EventDispatcherInterface $dispatcher, CallbackFactoryInterface $callbackFactory)
{
$this->beConstructedWith($this->configs, $dispatcher, $callbackFactory);
}
function it_is_initializable()
{
$this->shouldHaveType('SM\Factory\Factory');
}
function it_creates_statemachine(DummyObject $object)
{
$graph = 'graph1';
$this->get($object, $graph)->shouldReturnAnInstanceOf($this->configs[$graph]['state_machine_class']);
}
function it_creates_statemachine_with_default_class(DummyObject $object)
{
$this->get($object, 'graph2')->shouldReturnAnInstanceOf('SM\\StateMachine\\StateMachine');
}
function it_throws_exception_when_configuration_doesnt_exist(DummyObject $object)
{
$this->shouldThrow('SM\\SMException')->during('get', array($object, 'non-existing-graph'));
}
}

View File

@@ -0,0 +1,207 @@
<?php
namespace spec\SM\StateMachine;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use SM\Callback\CallbackFactoryInterface;
use SM\Callback\CallbackInterface;
use SM\Event\SMEvents;
use spec\SM\DummyObject;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
class StateMachineSpec extends ObjectBehavior
{
protected $config = array(
'graph' => 'graph1',
'property_path' => 'state',
'states' => array('checkout', 'pending', 'confirmed', 'cancelled'),
'transitions' => array(
'create' => array(
'from' => array('checkout'),
'to' => 'pending'
),
'confirm' => array(
'from' => array('checkout', 'pending'),
'to' => 'confirmed'
),
'cancel' => array(
'from' => array('confirmed'),
'to' => 'cancelled'
)
),
'callbacks' => array(
'guard' => array(
'guard-confirm' => array(
'from' => array('pending'),
'do' => 'dummy'
)
),
'before' => array(
'from-checkout' => array(
'from' => array('checkout'),
'do' => 'dummy'
)
),
'after' => array(
'on-confirm' => array(
'on' => array('confirm'),
'do' => 'dummy'
),
'to-cancelled' => array(
'to' => array('cancelled'),
'do' => 'dummy'
)
)
)
);
function let(DummyObject $object, EventDispatcherInterface $dispatcher, CallbackFactoryInterface $callbackFactory)
{
$this->beConstructedWith($object, $this->config, $dispatcher, $callbackFactory);
}
function it_is_initializable()
{
$this->shouldHaveType('SM\StateMachine\StateMachine');
}
function it_can($object, $dispatcher, $callbackFactory, CallbackInterface $guard)
{
$object->getState()->shouldBeCalled()->willReturn('checkout');
$object->setState(Argument::any())->shouldNotBeCalled();
$dispatcher->dispatch(SMEvents::TEST_TRANSITION, Argument::type('SM\\Event\\TransitionEvent'))->shouldBeCalled();
$callbackFactory->get($this->config['callbacks']['guard']['guard-confirm'])->shouldBeCalled()->willReturn($guard);
$guard->__invoke(Argument::type('SM\\Event\\TransitionEvent'))->shouldBeCalled()->willReturn(true);
$this->can('create')->shouldReturn(true);
}
function it_cannot($object, $dispatcher)
{
$object->getState()->shouldBeCalled()->willReturn('cancel');
$object->setState(Argument::any())->shouldNotBeCalled();
$dispatcher->dispatch(Argument::any())->shouldNotBeCalled();
$this->can('create')->shouldReturn(false);
}
function it_is_guarded_and_can($object, $dispatcher, $callbackFactory, CallbackInterface $guard)
{
$object->getState()->shouldBeCalled()->willReturn('pending');
$object->setState(Argument::any())->shouldNotBeCalled();
$dispatcher->dispatch(SMEvents::TEST_TRANSITION, Argument::type('SM\\Event\\TransitionEvent'))->shouldBeCalled();
$callbackFactory->get($this->config['callbacks']['guard']['guard-confirm'])->shouldBeCalled()->willReturn($guard);
$guard->__invoke(Argument::type('SM\\Event\\TransitionEvent'))->shouldBeCalled()->willReturn(true);
$this->can('confirm')->shouldReturn(true);
}
function it_is_guarded_and_cannot($object, $dispatcher, $callbackFactory, CallbackInterface $guard)
{
$object->getState()->shouldBeCalled()->willReturn('pending');
$object->setState(Argument::any())->shouldNotBeCalled();
$dispatcher->dispatch(SMEvents::TEST_TRANSITION, Argument::type('SM\\Event\\TransitionEvent'))->shouldBeCalled();
$callbackFactory->get($this->config['callbacks']['guard']['guard-confirm'])->shouldBeCalled()->willReturn($guard);
$guard->__invoke(Argument::type('SM\\Event\\TransitionEvent'))->shouldBeCalled()->willReturn(false);
$this->can('confirm')->shouldReturn(false);
}
function it_throws_an_exception_if_transition_doesnt_exist_on_can()
{
$this->shouldThrow('SM\\SMException')->during('can', array('non-existing-transition'));
}
function it_applies_transition(
$object,
$dispatcher,
$callbackFactory,
CallbackInterface $guard,
CallbackInterface $callback1,
CallbackInterface $callback2,
CallbackInterface $callback3
) {
$object->getState()->shouldBeCalled()->willReturn('checkout');
$object->setState('confirmed')->shouldBeCalled();
$dispatcher->dispatch(SMEvents::TEST_TRANSITION, Argument::type('SM\\Event\\TransitionEvent'))->shouldBeCalled();
$dispatcher->dispatch(SMEvents::PRE_TRANSITION, Argument::type('SM\\Event\\TransitionEvent'))->shouldBeCalled();
$dispatcher->dispatch(SMEvents::POST_TRANSITION, Argument::type('SM\\Event\\TransitionEvent'))->shouldBeCalled();
$callbackFactory->get($this->config['callbacks']['guard']['guard-confirm'])->shouldBeCalled()->willReturn($guard);
$callbackFactory->get($this->config['callbacks']['before']['from-checkout'])->shouldBeCalled()->willReturn($callback1);
$callbackFactory->get($this->config['callbacks']['after']['on-confirm'])->shouldBeCalled()->willReturn($callback2);
$callbackFactory->get($this->config['callbacks']['after']['to-cancelled'])->shouldBeCalled()->willReturn($callback3);
$guard->__invoke(Argument::type('SM\\Event\\TransitionEvent'))->shouldBeCalled()->willReturn(true);
$callback1->__invoke(Argument::type('SM\\Event\\TransitionEvent'))->shouldBeCalled();
$callback2->__invoke(Argument::type('SM\\Event\\TransitionEvent'))->shouldBeCalled();
$callback3->__invoke(Argument::type('SM\\Event\\TransitionEvent'))->shouldBeCalled();
$this->apply('confirm');
}
function it_throws_an_exception_if_transition_cannot_be_applied($object, $dispatcher)
{
$object->getState()->shouldBeCalled()->willReturn('cancel');
$object->setState(Argument::any())->shouldNotBeCalled();
$dispatcher->dispatch(Argument::any())->shouldNotBeCalled();
$this->shouldThrow('SM\\SMException')->during('apply', array('confirm'));
}
function it_does_nothing_if_transition_cannot_be_applied_in_soft_mode($object, $dispatcher)
{
$object->getState()->shouldBeCalled()->willReturn('cancel');
$object->setState(Argument::any())->shouldNotBeCalled();
$dispatcher->dispatch(Argument::any())->shouldNotBeCalled();
$this->apply('confirm', true);
}
function it_throws_an_exception_if_transition_doesnt_exist_on_apply()
{
$this->shouldThrow('SM\\SMException')->during('apply', array('non-existing-transition'));
}
function it_returns_current_state($object)
{
$object->getState()->shouldBeCalled()->willReturn('my-state');
$this->getState()->shouldReturn('my-state');
}
function it_returns_current_graph()
{
$this->getGraph()->shouldReturn($this->config['graph']);
}
function it_returns_current_object($object)
{
$this->getObject()->shouldReturn($object);
}
function it_returns_possible_transitions($object, $callbackFactory, CallbackInterface $guard)
{
$object->getState()->shouldBeCalled()->willReturn('checkout');
$callbackFactory->get($this->config['callbacks']['guard']['guard-confirm'])->shouldBeCalled()->willReturn($guard);
$guard->__invoke(Argument::type('SM\\Event\\TransitionEvent'))->shouldBeCalled()->willReturn(true);
$this->getPossibleTransitions()->shouldReturn(array('create', 'confirm'));
}
}

View File

@@ -0,0 +1,144 @@
<?php
/*
* This file is part of the StateMachine package.
*
* (c) Alexandre Bacco
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SM\Callback;
use SM\Event\TransitionEvent;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
use Symfony\Component\PropertyAccess\PropertyAccessor;
class Callback implements CallbackInterface
{
/**
* @var array
*/
protected $specs;
/**
* @var mixed
*/
protected $callable;
/**
* @param array $specs Specification for the Callback to be called
* @param mixed $callable Closure or Callable that will be called if specifications pass
*/
public function __construct(array $specs, $callable)
{
foreach (array('from', 'to', 'on', 'excluded_from', 'excluded_to', 'excluded_on') as $clause) {
if (!isset($specs[$clause])) {
$specs[$clause] = array();
} elseif (!is_array($specs[$clause])) {
$specs[$clause] = array($specs[$clause]);
}
}
$this->specs = $specs;
$this->callable = $callable;
}
/**
* @param TransitionEvent $event
*
* @return mixed The returned value from the callback
*/
public function call(TransitionEvent $event)
{
if (!isset($this->specs['args'])) {
$args = array($event);
} else {
$expr = new ExpressionLanguage();
$args = array_map(
function($arg) use($expr, $event) {
if (!is_string($arg)) {
return $arg;
}
return $expr->evaluate($arg, array(
'object' => $event->getStateMachine()->getObject(),
'event' => $event
));
}, $this->specs['args']
);
}
$callable = $this->filterCallable($this->callable, $event);
return call_user_func_array($callable, $args);
}
/**
* {@inheritDoc}
*/
public function __invoke(TransitionEvent $event)
{
if ($this->isSatisfiedBy($event)) {
return $this->call($event);
}
return true;
}
/**
* {@inheritDoc}
*/
public function isSatisfiedBy(TransitionEvent $event)
{
$config = $event->getConfig();
return
$this->isSatisfiedByClause('on', $event->getTransition())
&& $this->isSatisfiedByClause('from', $event->getState())
&& $this->isSatisfiedByClause('to', $config['to'])
;
}
/**
* @param string $clause The clause to check (on, from or to)
* @param string $value The value to check the clause against
*
* @return bool
*/
protected function isSatisfiedByClause($clause, $value)
{
if (0 < count($this->specs[$clause]) && !in_array($value, $this->specs[$clause])) {
return false;
}
if (0 < count($this->specs['excluded_'.$clause]) && in_array($value, $this->specs['excluded_'.$clause])) {
return false;
}
return true;
}
/**
* @param callable|array $callable A callable or array with index 0 starting with "object" that will evaluated as a property path with "object" being the object undergoing the transition
* @param TransitionEvent $event
*
* @return callable
*/
protected function filterCallable($callable, TransitionEvent $event)
{
if (is_array($callable) && isset($callable[0]) && is_string($callable[0]) && 'object' === substr($callable[0], 0, 6)) {
$object = $event->getStateMachine()->getObject();
// callable could be "object.property" and not just "object", so we evaluate the "property" path
if ('object' !== $callable[0]) {
$accessor = new PropertyAccessor();
$object = $accessor->getValue($object, substr($callable[0], 7));
}
return array($object, $callable[1]);
}
return $callable;
}
}

View File

@@ -0,0 +1,50 @@
<?php
/*
* This file is part of the StateMachine package.
*
* (c) Alexandre Bacco
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SM\Callback;
use SM\SMException;
class CallbackFactory implements CallbackFactoryInterface
{
/**
* @var string
*/
protected $class;
public function __construct($class)
{
if (!class_exists($class)) {
throw new SMException(sprintf(
'Class %s given to CallbackFactory does not exist.',
$class
));
}
$this->class = $class;
}
/**
* {@inheritDoc}
*/
public function get(array $specs)
{
if (!isset($specs['do'])) {
throw new SMException(sprintf(
'CallbackFactory::get needs the index "do" to be able to build a callback, array %s given.',
json_encode($specs)
));
}
$class = $this->class;
return new $class($specs, $specs['do']);
}
}

View File

@@ -0,0 +1,24 @@
<?php
/*
* This file is part of the StateMachine package.
*
* (c) Alexandre Bacco
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SM\Callback;
interface CallbackFactoryInterface
{
/**
* Return an instance of CallbackInterface loaded with the given $specs
*
* @param array $specs
*
* @return CallbackInterface
*/
public function get(array $specs);
}

View File

@@ -0,0 +1,35 @@
<?php
/*
* This file is part of the StateMachine package.
*
* (c) Alexandre Bacco
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SM\Callback;
use SM\Event\TransitionEvent;
interface CallbackInterface
{
/**
* Calls the callback if its specifications pass the event
*
* @param TransitionEvent $event
*
* @return mixed
*/
public function __invoke(TransitionEvent $event);
/**
* Determines if the callback specifications pass the event
*
* @param TransitionEvent $event
*
* @return bool
*/
public function isSatisfiedBy(TransitionEvent $event);
}

View File

@@ -0,0 +1,64 @@
<?php
/*
* This file is part of the StateMachine package.
*
* (c) Alexandre Bacco
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SM\Callback;
use SM\Event\TransitionEvent;
use SM\Factory\FactoryInterface;
/**
* Add the ability to cascade a transition to a different graph or different object via a simple callback
*
* @author Alexandre Bacco <alexandre.bacco@gmail.com>
*/
class CascadeTransitionCallback
{
/**
* @var FactoryInterface
*/
protected $factory;
/**
* @param FactoryInterface $factory
*/
public function __construct(FactoryInterface $factory)
{
$this->factory = $factory;
}
/**
* Apply a transition to the object that has just undergone a transition
*
* @param \Traversable|array $objects Object or array|traversable of objects to apply the transition on
* @param TransitionEvent $event Transition event
* @param string|null $transition Transition that is to be applied (if null, same as the trigger)
* @param string|null $graph Graph on which the new transition will apply (if null, same as the trigger)
* @param bool $soft If true, check if it can apply the transition first (no Exception thrown)
*/
public function apply($objects, TransitionEvent $event, $transition = null, $graph = null, $soft = true)
{
if (!is_array($objects) && !$objects instanceof \Traversable) {
$objects = array($objects);
}
if (null === $transition) {
$transition = $event->getTransition();
}
if (null === $graph) {
$graph = $event->getStateMachine()->getGraph();
}
foreach ($objects as $object) {
$this->factory->get($object, $graph)->apply($transition, $soft);
}
}
}

View File

@@ -0,0 +1,19 @@
<?php
/*
* This file is part of the StateMachine package.
*
* (c) Alexandre Bacco
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SM\Event;
abstract class SMEvents
{
const PRE_TRANSITION = 'winzou.state_machine.pre_transition';
const POST_TRANSITION = 'winzou.state_machine.post_transition';
const TEST_TRANSITION = 'winzou.state_machine.test_transition';
}

View File

@@ -0,0 +1,105 @@
<?php
/*
* This file is part of the StateMachine package.
*
* (c) Alexandre Bacco
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SM\Event;
use SM\StateMachine\StateMachineInterface;
use Symfony\Component\EventDispatcher\Event;
class TransitionEvent extends Event
{
/**
* @var string
*/
protected $transition;
/**
* @var string
*/
protected $fromState;
/**
* @var array
*/
protected $config;
/**
* @var StateMachineInterface
*/
protected $stateMachine;
/**
* @var bool
*/
protected $rejected = false;
/**
* @param string $transition Name of the transition being applied
* @param string $fromState State from which the transition is applied
* @param array $config Configuration of the transition
* @param StateMachineInterface $stateMachine State machine
*/
public function __construct($transition, $fromState, array $config, StateMachineInterface $stateMachine)
{
$this->transition = $transition;
$this->fromState = $fromState;
$this->config = $config;
$this->stateMachine = $stateMachine;
}
/**
* @return string
*/
public function getTransition()
{
return $this->transition;
}
/**
* @return array
*/
public function getConfig()
{
return $this->config;
}
/**
* @return StateMachineInterface
*/
public function getStateMachine()
{
return $this->stateMachine;
}
/**
* @return string
*/
public function getState()
{
return $this->fromState;
}
/**
* @param bool $reject
*/
public function setRejected($reject = true)
{
$this->rejected = (bool) $reject;
}
/**
* @return bool
*/
public function isRejected()
{
return $this->rejected;
}
}

View File

@@ -0,0 +1,84 @@
<?php
/*
* This file is part of the StateMachine package.
*
* (c) Alexandre Bacco
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SM\Extension\Twig;
use SM\Factory\FactoryInterface;
class SMExtension extends \Twig_Extension
{
/**
* @var FactoryInterface
*/
protected $factory;
/**
* @param FactoryInterface $factory
*/
public function __construct(FactoryInterface $factory)
{
$this->factory = $factory;
}
/**
* @{inheritDoc}
*/
public function getFunctions()
{
return array(
new \Twig_SimpleFunction('sm_can', array($this, 'can')),
new \Twig_SimpleFunction('sm_state', array($this, 'getState')),
new \Twig_SimpleFunction('sm_possible_transitions', array($this, 'getPossibleTransitions')),
);
}
/**
* @param object $object
* @param string $transition
* @param string $graph
*
* @return bool
*/
public function can($object, $transition, $graph = 'default')
{
return $this->factory->get($object, $graph)->can($transition);
}
/**
* @param object $object
* @param string $graph
*
* @return string
*/
public function getState($object, $graph = 'default')
{
return $this->factory->get($object, $graph)->getState();
}
/**
* @param object $object
* @param string $graph
*
* @return array
*/
public function getPossibleTransitions($object, $graph = 'default')
{
return $this->factory->get($object, $graph)->getPossibleTransitions();
}
/**
* @{inheritDoc}
*/
public function getName()
{
return 'sm';
}
}

View File

@@ -0,0 +1,104 @@
<?php
/*
* This file is part of the StateMachine package.
*
* (c) Alexandre Bacco
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SM\Factory;
use SM\SMException;
use SM\StateMachine\StateMachineInterface;
abstract class AbstractFactory implements ClearableFactoryInterface
{
/**
* @var array
*/
protected $configs;
/**
* @var array
*/
protected $stateMachines = array();
/**
* @param array $configs Array of configs for the available state machines
*/
public function __construct(array $configs)
{
foreach ($configs as $graph => $config) {
$this->addConfig($config, $graph);
}
}
/**
* {@inheritDoc}
*/
public function get($object, $graph = 'default')
{
$hash = spl_object_hash($object);
if (isset($this->stateMachines[$hash][$graph])) {
return $this->stateMachines[$hash][$graph];
}
foreach ($this->configs as $config) {
if ($config['graph'] === $graph && $object instanceof $config['class']) {
return $this->stateMachines[$hash][$graph] = $this->createStateMachine($object, $config);
}
}
throw new SMException(sprintf(
'Cannot create a state machine because the configuration for object "%s" with graph "%s" does not exist.',
get_class($object),
$graph
));
}
/**
* {@inheritDoc}
*/
public function clear()
{
$this->stateMachines = array();
}
/**
* Adds a new config
*
* @param array $config
* @param string $graph
*
* @throws SMException If the index "class" is not configured
*/
public function addConfig(array $config, $graph = 'default')
{
if (!isset($config['graph'])) {
$config['graph'] = $graph;
}
if (!isset($config['class'])) {
throw new SMException(sprintf(
'Index "class" needed for the state machine configuration of graph "%s"',
$config['graph']
));
}
$this->configs[] = $config;
}
/**
* Create a state machine for the given object and config
*
* @param $object
* @param array $config
*
* @return StateMachineInterface
*/
abstract protected function createStateMachine($object, array $config);
}

View File

@@ -0,0 +1,20 @@
<?php
/*
* This file is part of the StateMachine package.
*
* (c) Alexandre Bacco
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SM\Factory;
interface ClearableFactoryInterface extends FactoryInterface
{
/**
* Clears all state machines from the factory
*/
public function clear();
}

View File

@@ -0,0 +1,59 @@
<?php
/*
* This file is part of the StateMachine package.
*
* (c) Alexandre Bacco
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SM\Factory;
use SM\Callback\CallbackFactoryInterface;
use SM\SMException;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
class Factory extends AbstractFactory
{
/**
* @var EventDispatcherInterface
*/
protected $dispatcher;
/**
* @var CallbackFactoryInterface
*/
protected $callbackFactory;
public function __construct(
array $configs,
EventDispatcherInterface $dispatcher = null,
CallbackFactoryInterface $callbackFactory = null
) {
parent::__construct($configs);
$this->dispatcher = $dispatcher;
$this->callbackFactory = $callbackFactory;
}
/**
* {@inheritDoc}
*/
protected function createStateMachine($object, array $config)
{
if (!isset($config['state_machine_class'])) {
$class = 'SM\\StateMachine\\StateMachine';
} elseif (class_exists($config['state_machine_class'])) {
$class = $config['state_machine_class'];
} else {
throw new SMException(sprintf(
'Class "%s" for creating a new state machine does not exist.',
$config['state_machine_class']
));
}
return new $class($object, $config, $this->dispatcher, $this->callbackFactory);
}
}

View File

@@ -0,0 +1,27 @@
<?php
/*
* This file is part of the StateMachine package.
*
* (c) Alexandre Bacco
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SM\Factory;
use SM\StateMachine\StateMachineInterface;
interface FactoryInterface
{
/**
* Returns the state machine match the couple object/graph
*
* @param object $object
* @param string $graph
*
* @return StateMachineInterface
*/
public function get($object, $graph = 'default');
}

View File

@@ -0,0 +1,16 @@
<?php
/*
* This file is part of the StateMachine package.
*
* (c) Alexandre Bacco
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SM;
class SMException extends \Exception
{
}

View File

@@ -0,0 +1,235 @@
<?php
/*
* This file is part of the StateMachine package.
*
* (c) Alexandre Bacco
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SM\StateMachine;
use SM\Callback\CallbackFactory;
use SM\Callback\CallbackFactoryInterface;
use SM\Callback\CallbackInterface;
use SM\Event\SMEvents;
use SM\Event\TransitionEvent;
use SM\SMException;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
use Symfony\Component\PropertyAccess\PropertyAccessor;
class StateMachine implements StateMachineInterface
{
/**
* @var object
*/
protected $object;
/**
* @var array
*/
protected $config;
/**
* @var EventDispatcherInterface
*/
protected $dispatcher;
/**
* @var CallbackFactoryInterface
*/
protected $callbackFactory;
/**
* @param object $object Underlying object for the state machine
* @param array $config Config array of the graph
* @param EventDispatcherInterface $dispatcher EventDispatcher or null not to dispatch events
* @param CallbackFactoryInterface $callbackFactory CallbackFactory or null to use the default one
*
* @throws SMException If object doesn't have configured property path for state
*/
public function __construct(
$object,
array $config,
EventDispatcherInterface $dispatcher = null,
CallbackFactoryInterface $callbackFactory = null
) {
$this->object = $object;
$this->dispatcher = $dispatcher;
$this->callbackFactory = $callbackFactory ?: new CallbackFactory('SM\Callback\Callback');
if (!isset($config['property_path'])) {
$config['property_path'] = 'state';
}
$this->config = $config;
// Test if the given object has the given state property path
try {
$this->getState();
} catch (NoSuchPropertyException $e) {
throw new SMException(sprintf(
'Cannot access to configured property path "%s" on object %s with graph "%s"',
$config['property_path'],
get_class($object),
$config['graph']
));
}
}
/**
* {@inheritDoc}
*/
public function can($transition)
{
if (!isset($this->config['transitions'][$transition])) {
throw new SMException(sprintf(
'Transition "%s" does not exist on object "%s" with graph "%s"',
$transition,
get_class($this->object),
$this->config['graph']
));
}
if (!in_array($this->getState(), $this->config['transitions'][$transition]['from'])) {
return false;
}
$can = true;
$event = new TransitionEvent($transition, $this->getState(), $this->config['transitions'][$transition], $this);
if (null !== $this->dispatcher) {
$this->dispatcher->dispatch(SMEvents::TEST_TRANSITION, $event);
$can = !$event->isRejected();
}
return $can && $this->callCallbacks($event, 'guard');
}
/**
* {@inheritDoc}
*/
public function apply($transition, $soft = false)
{
if (!$this->can($transition)) {
if ($soft) {
return false;
}
throw new SMException(sprintf(
'Transition "%s" cannot be applied on state "%s" of object "%s" with graph "%s"',
$transition,
$this->getState(),
get_class($this->object),
$this->config['graph']
));
}
$event = new TransitionEvent($transition, $this->getState(), $this->config['transitions'][$transition], $this);
if (null !== $this->dispatcher) {
$this->dispatcher->dispatch(SMEvents::PRE_TRANSITION, $event);
if ($event->isRejected()) {
return false;
}
}
$this->callCallbacks($event, 'before');
$this->setState($this->config['transitions'][$transition]['to']);
$this->callCallbacks($event, 'after');
if (null !== $this->dispatcher) {
$this->dispatcher->dispatch(SMEvents::POST_TRANSITION, $event);
}
return true;
}
/**
* {@inheritDoc}
*/
public function getState()
{
$accessor = new PropertyAccessor();
return $accessor->getValue($this->object, $this->config['property_path']);
}
/**
* {@inheritDoc}
*/
public function getObject()
{
return $this->object;
}
/**
* {@inheritDoc}
*/
public function getGraph()
{
return $this->config['graph'];
}
/**
* {@inheritDoc}
*/
public function getPossibleTransitions()
{
return array_filter(
array_keys($this->config['transitions']),
array($this, 'can')
);
}
/**
* Set a new state to the underlying object
*
* @param string $state
*
* @throws SMException
*/
protected function setState($state)
{
if (!in_array($state, $this->config['states'])) {
throw new SMException(sprintf(
'Cannot set the state to "%s" to object "%s" with graph %s because it is not pre-defined.',
$state,
get_class($this->object),
$this->config['graph']
));
}
$accessor = new PropertyAccessor();
$accessor->setValue($this->object, $this->config['property_path'], $state);
}
/**
* Builds and calls the defined callbacks
*
* @param TransitionEvent $event
* @param string $position
* @return bool
*/
protected function callCallbacks(TransitionEvent $event, $position)
{
if (!isset($this->config['callbacks'][$position])) {
return true;
}
$result = true;
foreach ($this->config['callbacks'][$position] as &$callback) {
if (!$callback instanceof CallbackInterface) {
$callback = $this->callbackFactory->get($callback);
}
$result = call_user_func($callback, $event) && $result;
}
return $result;
}
}

View File

@@ -0,0 +1,68 @@
<?php
/*
* This file is part of the StateMachine package.
*
* (c) Alexandre Bacco
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SM\StateMachine;
use SM\SMException;
interface StateMachineInterface
{
/**
* Can the transition be applied on the underlying object
*
* @param string $transition
*
* @return bool
*
* @throws SMException If transition doesn't exist
*/
public function can($transition);
/**
* Applies the transition on the underlying object
*
* @param string $transition Transition to apply
* @param bool $soft Soft means do nothing if transition can't be applied (no exception thrown)
*
* @return bool If the transition has been applied or not (in case of soft apply or rejected pre transition event)
*
* @throws SMException If transition can't be applied or doesn't exist
*/
public function apply($transition, $soft = false);
/**
* Returns the current state
*
* @return string
*/
public function getState();
/**
* Returns the underlying object
*
* @return object
*/
public function getObject();
/**
* Returns the current graph
*
* @return string
*/
public function getGraph();
/**
* Returns the possible transitions
*
* @return array
*/
public function getPossibleTransitions();
}