PACKAGE PHILOSOPHY This whole package strives to follow the open/closed principle, making it open for extension (adding your own logic through subclassing) but closed for modification. For that purpose, we provide Loader interfaces, Builders and Persistence Adapters for the backend implementation. States and Transitions can also be subclassed to store data and provide functionality. Following the same philosophy: if you want more functionality in the statemachine, you can use the provided methods and override them in your subclass. multiple hooks are provided. Most functionality should be provided by using the diverse ways of interacting with the statemachine: callables can be injected, commands can be injected, event handlers can be defined and hooks can be overriden. We have provided multiple persistance backends to function as a data store to store all relevant information for a statemachine including configuration, transition history and current states. - relational databases: postgresql, mysql, sqlite - nosql key/value: redis - nosql document based: mongodb Memory and session backend adapters can be used to temporarily store the state information. yaml, json and xml loaders can be used to load configuration data from files containing those data formats. Examples are provided in the 'examples' folder and serve to highlight some of the features and the way to work with the package. The unittests can serve as examples too, including interaction with databases. ENVIRONMENTS OF USAGE: - 1: a one time process: -- on webpages where there are page refreshes in between (use session/pdo adapter) see 'examples/session' -- an api where succesive calls are made (use pdo adapter) -- cron jobs (pdo adapter) - 2: a longer running process: -- a php daemon that runs as a background process (use memory adapter) -- an interactive shell environment (use memory adapter) see 'examples/interactive' DESCRIPTION OF THE 4 MAIN MODELS OF USAGE: - 1: DELEGATION: Use an existing domain model. You can use a subclass of the AbstractFactory to get a StateMachine, since that will put the creation of all the relevant Contextual classes and Transitions in a reusable model. usage: This is the most formal, least invasive and powerful model of usage, but the most complex. Use Rules and Commands to interact with your domain model without altering a domain model to work with the statemachine. see 'examples/trafficlight' - 2: INHERITANCE: Subclass a statemachine. Build the full Context and all transitions in the constructor of your subclass (which could be a domain model itself) and call the parent constructor. use the hooks to provide functionality (optionally a ModelBuilder and callbacks). usage: This is the most flexible model of usage, but mixes your domain logic with the statemachine. see 'examples/inheritance' - 3: COMPOSITION: Use object composition. Instantiate and build a statemachine in a domain model and build the full Context, Transitions and statemachine there. Use a ModelBuilder and callbacks to drive functionality. usage: This is a good mixture between encapsulating statemachine logic and flexibility/formal usage. see 'examples/composition' - 4: STANDALONE: Use the statemachine as is, without any domain models. Your application will use it and inspect the statemachine to act on it. Use callables to provide functionality usage: This is the easiest model of usage, but the least powerful. see 'examples/interactive' MECHANISMS FOR GUARD AND TRANSITION LOGIC 1. rules and commands: they are fully qualified class names of Rule and Command (sub)classes that can be injected in Transition and State instances. they accept the domain model provided via an EntityBuilder in their constructor. 2. hooks: the methods in this class that start with an underscore and can be overriden in a subclass. these can be used to provide a subclass of this machine specifically tailored to your application domain.
Author: Rolf Vreijdenberger
 /**
  * @test
  */
 public function shouldLoadTransitionsFromFile()
 {
     $machine = new StateMachine(new Context(new Identifier('xml-test', 'test-machine')));
     $this->assertCount(0, $machine->getTransitions());
     //this is a symbolic link to the asset/xml/example.xml file
     $loader = XML::createFromFile(__DIR__ . '/fixture-example.xml');
     $count = $loader->load($machine);
     $this->assertCount(4, $machine->getTransitions(), 'there is a regex transition that adds 2 transitions (a-c and b-c)');
     $this->assertEquals(4, $count);
     $this->assertEquals(0, MyStatic::$guard);
     $this->assertTrue($machine->ab());
     $this->assertEquals(1, MyStatic::$guard, 'guard callable specified in xml should be called');
     $this->assertTrue($machine->bdone());
     $this->assertEquals(2, MyStatic::$entry, '2 entry state callables in config');
     $this->assertEquals(1, MyStatic::$exit, '1 exit state callable in config');
 }
 /**
  * {@inheritDoc}
  */
 public function load(StateMachine $stateMachine)
 {
     $count = 0;
     $unsorted = $this->getTransitions();
     $has_regex = array();
     $has_no_regex = array();
     foreach ($unsorted as $transition) {
         $to = $transition->getStateTo();
         $from = $transition->getStateFrom();
         //sort on regexes. they should come last in an automated loader like this
         if ($from->isRegex() || $to->isRegex()) {
             $has_regex[] = $transition;
         } else {
             $has_no_regex[] = $transition;
         }
     }
     $sorted = array_merge($has_no_regex, $has_regex);
     // add the sorted transitions. the transitions added will set the
     // states (from/to) on the statemachine
     foreach ($sorted as $transition) {
         // when using transitions with 'regex' states, the statemachine will handle this for you.
         $count += $stateMachine->addTransition($transition);
     }
     return $count;
 }
 /**
  * {@inheritDoc}
  */
 public function load(StateMachine $stateMachine)
 {
     //load the xml in a php object structure. suppres warning with @ operator since we explicitely check the return value
     $xml = @simplexml_load_string($this->getXML());
     if ($xml === false) {
         //could not load
         throw new Exception(sprintf('could not load xml data. check the xml format'), Exception::BAD_LOADERDATA);
     }
     $name = $stateMachine->getContext()->getMachine();
     $found = false;
     $data = null;
     foreach ($xml->machine as $data) {
         if ((string) @$data->name === $name) {
             $found = true;
             break;
         }
     }
     if (!$found) {
         //no name match found
         throw new Exception(sprintf('no machine data found for %s in xml. seems like a wrong configuration.', $name), Exception::BAD_LOADERDATA);
     }
     //accessing xml as an object with the @ error suppresion operator ('shut the f**k up' operator)
     //allows you to get properties, even if they do not exist, without notices.
     //this let's us be a littlebit lazy since we know some nonessential properties could not be there
     $states = array();
     foreach ($data->states->state as $state) {
         $tmp = new State((string) $state->name, (string) $state->type, (string) @$state->entry_command, (string) @$state->exit_command, (string) @$state->entry_callable, (string) @$state->exit_callable);
         $tmp->setDescription((string) @$state->description);
         $states[$tmp->getName()] = $tmp;
     }
     $transitions = array();
     foreach ($data->transitions->transition as $transition) {
         $tmp = new Transition($states[(string) @$transition->state_from], $states[(string) @$transition->state_to], (string) @$transition->event, (string) @$transition->rule, (string) @$transition->command, (string) @$transition->guard_callable, (string) @$transition->transition_callable);
         $tmp->setDescription((string) @$transition->description);
         $transitions[] = $tmp;
     }
     //delegate to loader
     $loader = new LoaderArray($transitions);
     return $loader->load($stateMachine);
 }
Exemple #4
0
 /**
  * {@inheritDoc}
  */
 public function load(StateMachine $stateMachine)
 {
     //decode the json in a php object structure
     $decoded = yaml_parse($this->getYaml(), false);
     //yaml decoding returns a php array.
     $name = $stateMachine->getContext()->getMachine();
     $found = false;
     $data = null;
     if (is_array(@$decoded['machines'])) {
         foreach ($decoded['machines'] as $data) {
             if ($data['name'] === $name) {
                 $found = true;
                 break;
             }
         }
     }
     if (!$found) {
         //no name match found
         throw new Exception(sprintf('no machine data found for "%s" in yaml. seems like a wrong configuration.', $name), Exception::BAD_LOADERDATA);
     }
     //accessing an array with an @ error suppresion operator ('shut the f**k up' operator),
     //allows you to get properties, even if they do not exist, without notices.
     //this lets us be a little lazy in mapping the array properties to the state and transition properties
     $states = array();
     foreach ($data['states'] as $state) {
         $tmp = new State($state['name'], $state['type'], @$state['entry_command'], @$state['exit_command'], @$state['entry_callable'], @$state['exit_callable']);
         $tmp->setDescription(@$state['description']);
         $states[$tmp->getName()] = $tmp;
     }
     $transitions = array();
     foreach ($data['transitions'] as $transition) {
         $tmp = new Transition($states[$transition['state_from']], $states[$transition['state_to']], @$transition['event'], @$transition['rule'], @$transition['command'], @$transition['guard_callable'], @$transition['transition_callable']);
         $tmp->setDescription(@$transition['description']);
         $transitions[] = $tmp;
     }
     //delegate to loader
     $loader = new LoaderArray($transitions);
     return $loader->load($stateMachine);
 }
 /**
  * Checks a fully loaded statemachine for a valid configuration.
  *
  * This is useful in a debug situation so you can check for the validity of
  * rules, commands and callables in guard logic, transition logic and state entry/exit logic
  * before they hit a
  *
  * This does not instantiate any objects. It just checks if callables can be found
  * and if classes for rules and commands can be found
  *
  * @param StateMachine $machine
  * @return Exception[] an array of exceptions if anything is wrong with the configuration
  */
 public static function checkConfiguration(StateMachine $machine)
 {
     //TODO: also check the rules and commands
     $exceptions = array();
     $output = array();
     //check state callables
     foreach ($machine->getStates() as $state) {
         $exceptions[] = self::getExceptionForCheckingCallable($state->getExitCallable(), State::CALLABLE_ENTRY, $state);
         $exceptions[] = self::getExceptionForCheckingCallable($state->getEntryCallable(), State::CALLABLE_ENTRY, $state);
     }
     //check transition callables
     foreach ($machine->getTransitions() as $transition) {
         $exceptions[] = self::getExceptionForCheckingCallable($transition->getGuardCallable(), Transition::CALLABLE_GUARD, $transition);
         $exceptions[] = self::getExceptionForCheckingCallable($transition->getTransitionCallable(), Transition::CALLABLE_TRANSITION, $transition);
     }
     //get the exceptions
     foreach ($exceptions as $e) {
         if (is_a($e, '\\Exception')) {
             $output[] = $e;
         }
     }
     return $output;
 }
 /**
  * @test
  */
 public function shouldLoadAndWriteViaDelegator()
 {
     $loader = XML::createFromFile(__DIR__ . '/../loader/fixture-example.xml');
     $writer = new Memory();
     $identifier = new Identifier('readerwriter-test', 'test-machine');
     $delegator = new ReaderWriterDelegator($loader, $writer);
     $context = new Context($identifier, null, $delegator);
     $machine = new StateMachine($context);
     $this->assertCount(0, $machine->getTransitions());
     $count = $delegator->load($machine);
     //add to the backend
     $this->assertTrue($context->add('a'));
     $this->assertCount(4, $machine->getTransitions(), 'there is a regex transition that adds 2 transitions (a-c and b-c)');
     $this->assertEquals(4, $count);
     $this->assertTrue($machine->ab());
     //get the data from the memory storage facility
     $data = $writer->getStorageFromRegistry($machine->getContext()->getIdentifier());
     $this->assertEquals('b', $data->state);
     $this->assertEquals('b', $machine->getCurrentState()->getName());
     $this->assertTrue($machine->bdone());
     $data = $writer->getStorageFromRegistry($machine->getContext()->getIdentifier());
     $this->assertEquals('done', $data->state);
 }
Exemple #7
0
 /**
  * {@inheritDoc}
  */
 public function load(StateMachine $stateMachine)
 {
     //decode the json in a php object structure
     $decoded = json_decode($this->getJSON(), false);
     if (!$decoded) {
         //could not decode (make sure that fully qualified names are escaped with
         //2 backslashes: \\izzum\\commands\\Null and that only double quotes are used.
         throw new Exception(sprintf('could not decode json data. did you only use double quotes? check the json format against %s', 'http://jsonlint.com/'), Exception::BAD_LOADERDATA);
     }
     $name = $stateMachine->getContext()->getMachine();
     $found = false;
     $data = null;
     if (is_array(@$decoded->machines)) {
         foreach ($decoded->machines as $data) {
             if ($data->name === $name) {
                 $found = true;
                 break;
             }
         }
     }
     if (!$found) {
         //no name match found
         throw new Exception(sprintf('no machine data found for %s in json. seems like a wrong configuration.', $name), Exception::BAD_LOADERDATA);
     }
     //accessing json as an object with an @ error suppresion operator ('shut the f**k up' operator),
     //allows you to get properties, even if they do not exist, without notices.
     //this lets us be a little lazy in mapping the json properties to the state and transition properties
     $states = array();
     foreach ($data->states as $state) {
         $tmp = new State($state->name, $state->type, @$state->entry_command, @$state->exit_command, @$state->entry_callable, @$state->exit_callable);
         $tmp->setDescription(@$state->description);
         $states[$tmp->getName()] = $tmp;
     }
     $transitions = array();
     foreach ($data->transitions as $transition) {
         $tmp = new Transition($states[$transition->state_from], $states[$transition->state_to], @$transition->event, @$transition->rule, @$transition->command, @$transition->guard_callable, @$transition->transition_callable);
         $tmp->setDescription(@$transition->description);
         $transitions[] = $tmp;
     }
     //delegate to loader
     $loader = new LoaderArray($transitions);
     return $loader->load($stateMachine);
 }
 public function __construct($name, $alias)
 {
     $this->name = $name;
     $this->alias = $alias;
     //create machine with unique superhero name.
     //associate the domain object (that will be used on callables) with $this
     $context = new Context(new Identifier($name . ":" . $alias, "superhero-machine"), new ModelBuilder($this));
     //call parent constructor
     parent::__construct($context);
     //define the states and the state types, and some entry states
     $start = new State('start', State::TYPE_INITIAL);
     $callback_dress_normal_for_entering_state = array($this, 'changeIntoNormalClothes');
     $normal = new State('normal', State::TYPE_NORMAL, null, null, $callback_dress_normal_for_entering_state);
     $callback_entering_superhero_state = array($this, 'changeIntoCostume');
     $super = new State('superhero', State::TYPE_NORMAL, null, null, $callback_entering_superhero_state);
     $posing = new State('posing');
     $fighting = new State('fighting');
     $resqueing = new State('resqueing');
     $done = new State('done', State::TYPE_FINAL);
     //add transitions to this class (a subclass of statmachine), with event names
     $this->addTransition(new Transition($start, $normal, 'wakeup'));
     $this->addTransition(new Transition($normal, $done, 'done'));
     $this->addTransition(new Transition($super, $fighting, 'fight'));
     $this->addTransition(new Transition($super, $posing, 'pose'));
     $this->addTransition(new Transition($super, $resqueing, 'rescue'));
     //allow to go from super to every other state
     $this->addTransition(new Transition($super, new State('regex:/.*/')));
     //allow to go from every state to super
     $this->addTransition(new Transition(new State('regex:/.*/'), $super, 'beSuper'));
     //allow to go from every state to normal
     $this->addTransition(new Transition(new State('regex:/.*/'), $normal, 'standDown'));
     //allow to pose, rescue of fight from every state except start and normal
     $this->addTransition(new Transition(new State('not-regex:/start|normal/'), $posing, 'pose'));
     $this->addTransition(new Transition(new State('not-regex:/start|normal/'), $resqueing, 'resque'));
     $this->addTransition(new Transition(new State('not-regex:/start|normal/'), $fighting, 'fight'));
 }
 /**
  * {@inheritDoc}
  * Load the statemachine with data from a JSON string.
  * the JSON string is stored at the redis key '<prefix:>configuration' by default.
  * you can alter the configuration key by using Redis::setPrefix() and Redis::setConfigurationKey()
  * 
  * First, the key '<prefix>:configuration:<machine-name>' is checked for existence.
  * If it exists, take the configuration from that key, else take the configuration form
  * the '<prefix>:configuration' key.
  * 
  * This method can be overriden in a subclass to use another loader when 
  * the data is stored in redis in YAML or XML form for example.
  * You could use the ReaderWriterDelegator to use another source to load the configuration from.
  */
 public function load(StateMachine $statemachine)
 {
     //use the JSON loader to load the configuration (see the json schema we expect in JSON::getJSONSchema)
     $key = $this->getConfigurationKey();
     $redis = $this->getRedis();
     $specific_key = sprintf(self::KEY_CONFIGURATION_SPECIFIC, $key, $statemachine->getContext()->getMachine());
     if ($redis->exists($specific_key)) {
         $key = $specific_key;
     }
     $loader = new JSON($this->getRedis()->get($key));
     $count = $loader->load($statemachine);
     return $count;
 }
 * go to the browser and open localhost:2468 and refresh a couple of times
 * and stop the webserver when you're done with ctrl+c
 */
require_once '../autoload.php';
//all states, the color of the rainbow
$new = new State('white', State::TYPE_INITIAL);
$red = new State('red');
$orange = new State('orange');
$yellow = new State('yellow');
$green = new State('green');
$blue = new State('blue');
$indigo = new State('indigo');
$violet = new State('violet');
// create the machine with the correct session adapter to store the state accross page refreshes
$adapter = new Session();
$machine = new StateMachine(new Context(new Identifier('session-example', 'rainbow-machine'), null, $adapter));
//add the transitions, going from one color to the next and back to the first
$machine->addTransition(new Transition($new, $red));
$machine->addTransition(new Transition($red, $orange));
$machine->addTransition(new Transition($orange, $yellow));
$machine->addTransition(new Transition($yellow, $green));
$machine->addTransition(new Transition($green, $blue));
$machine->addTransition(new Transition($blue, $indigo));
$machine->addTransition(new Transition($indigo, $violet));
$machine->addTransition(new Transition($violet, $red));
//initialize the first time to 'red' and then cycle through the
//colors for each page refresh
$machine->run();
//get some data to put in the output
$current = $machine->getCurrentState();
$next_transitions = implode(',', $current->getTransitions());
 * Example script that uses the 'standalone mode' as one of the four usage models for the statemachine.
 * The other three usage models being inheritance, composition and delegation.
 *
 * This script allows you to interact with the statemachine from the command line
 * and does not use anything fancy for guard or transition logic.
 * To see an example of guard and transition logic for the standalone mode you
 * can see 'examples/standalone'
 *
 *
 * run this script from the (bash) command line:
 * php -f index.php
 * and stop it with ctrl+c
 */
//create machine. context defaults to in-memory state handling
$context = new Context(new Identifier("webshopcheckout-example", "webshopcheckout-machine"));
$machine = new StateMachine($context);
//define the states and the state types
$basket = new State('basket', State::TYPE_INITIAL);
$customerdata = new State('customerdata');
$shipping = new State('shipping');
$payment = new State('payment');
$complete = new State('complete', State::TYPE_FINAL);
//add transitions to the machine, with event names
$machine->addTransition(new Transition($basket, $customerdata, 'Checkout'));
$machine->addTransition(new Transition($customerdata, $shipping, 'ChooseHowToShip'));
$machine->addTransition(new Transition($shipping, $payment, 'ChooseHowToPay'));
$machine->addTransition(new Transition($payment, $complete, 'ready'));
//start the interactive demo
//with some coloring that works in the bash shell
echo PHP_EOL . "Izzum statemachine webshopcheckout demo. press ctrl+c to stop it." . PHP_EOL . PHP_EOL;
//loop the machine
 /**
  * @test
  */
 public function shouldLoadTransitionsFromJSONString()
 {
     $machine = new StateMachine(new Context(new Identifier('json-test', 'json-machine')));
     $this->assertCount(0, $machine->getTransitions());
     $json = $this->getJSON();
     $loader = new JSON($json);
     $this->assertEquals($this->getJSON(), $loader->getJSON());
     $count = $loader->load($machine);
     $this->assertCount(2, $machine->getTransitions());
     $this->assertEquals(2, $count);
     $tbd = $machine->getTransition('b_to_done');
     $b = $tbd->getStateFrom();
     $d = $tbd->getStateTo();
     $tab = $machine->getTransition('a_to_b');
     $a = $tab->getStateFrom();
     $this->assertEquals($b, $tab->getStateTo());
     $this->assertSame($b, $tab->getStateTo());
     $this->assertTrue($a->isInitial());
     $this->assertTrue($b->isNormal());
     $this->assertTrue($d->isFinal());
 }
 /**
  * @test
  */
 public function shouldAddRegexesLoaderOnlyWhenStatesAreSet()
 {
     $context = new Context(new Identifier(Identifier::NULL_ENTITY_ID, Identifier::NULL_STATEMACHINE));
     $machine = new StateMachine($context);
     $transitions = array();
     $s1 = new State("1");
     $s2 = new State("2");
     $s3 = new State("3");
     //many to many
     $transitions[] = new Transition(new State('regex:/.*/'), new State('regex:/.*/'));
     $loader = new LoaderArray($transitions);
     $this->assertEquals(count($transitions), $loader->count());
     $this->assertEquals(1, $loader->count());
     $this->assertEquals(0, count($machine->getStates()));
     $this->assertEquals(0, count($machine->getTransitions()));
     $count = $loader->load($machine);
     $this->assertEquals(0, $count, 'nothing because there are no known states');
     $this->assertTrue($machine->addState($s1));
     $this->assertTrue($machine->addState($s2));
     $this->assertTrue($machine->addState($s3));
     $this->assertEquals(3, count($machine->getStates()));
     $this->assertFalse($machine->addState($s1));
     $this->assertFalse($machine->addState($s2));
     $this->assertFalse($machine->addState($s3));
     $this->assertEquals(3, count($machine->getStates()));
     $count = $loader->load($machine);
     $this->assertEquals(6, count($machine->getTransitions()));
     $this->assertEquals(6, $count, 'regexes have matched all states and created a mesh');
     $count = $loader->load($machine);
     $this->assertEquals(0, $count, 'transitions are not added since they have already been added');
     $this->assertEquals(3, count($machine->getStates()));
     $this->assertEquals(6, count($machine->getTransitions()));
 }
 /**
  * creates plantuml state output for a statemachine
  *
  * @param string $machine            
  * @return string plant uml code, this can be used to render an image
  * @link http://www.plantuml.com/plantuml/
  * @link http://plantuml.sourceforge.net/state.html
  * @throws Exception
  */
 public function createStateDiagram(StateMachine $machine)
 {
     $transitions = $machine->getTransitions();
     // all states are aliased so the plantuml parser can handle the names
     $aliases = array();
     $end_states = array();
     $EOL = "\\n\\" . PHP_EOL;
     /* for multiline stuff in plantuml */
     $NEWLINE = PHP_EOL;
     // start with declaration
     $uml = "@startuml" . PHP_EOL;
     // skins for colors etc.
     $uml .= $this->getPlantUmlSkins() . PHP_EOL;
     // the order in which transitions are executed
     $order = array();
     // create the diagram by drawing all transitions
     foreach ($transitions as $t) {
         // get states and state aliases (plantuml cannot work with certain
         // characters, so therefore we create an alias for the state name)
         $from = $t->getStateFrom();
         $from_alias = $this->plantUmlStateAlias($from->getName());
         $to = $t->getStateTo();
         $to_alias = $this->plantUmlStateAlias($to->getName());
         // get some names to display
         $command = $t->getCommandName();
         $rule = self::escape($t->getRuleName());
         $name_transition = $t->getName();
         $description = $t->getDescription() ? "description: '" . $t->getDescription() . "'" . $EOL : '';
         $event = $t->getEvent() ? "event: '" . $t->getEvent() . "'" . $EOL : '';
         $f_description = $from->getDescription();
         $t_description = $to->getDescription();
         $f_exit = $from->getExitCommandName();
         $f_entry = $from->getEntryCommandName();
         $t_exit = $to->getExitCommandName();
         $t_entry = $to->getEntryCommandName();
         // only write aliases if not done before
         if (!isset($aliases[$from_alias])) {
             $uml .= 'state "' . $from . '" as ' . $from_alias . PHP_EOL;
             $uml .= "{$from_alias}: description: '" . $f_description . "'" . PHP_EOL;
             $uml .= "{$from_alias}: entry / '" . $f_entry . "'" . PHP_EOL;
             $uml .= "{$from_alias}: exit / '" . $f_exit . "'" . PHP_EOL;
             $aliases[$from_alias] = $from_alias;
         }
         // store order in which transitions will be handled
         if (!isset($order[$from_alias])) {
             $order[$from_alias] = 1;
         } else {
             $order[$from_alias] = $order[$from_alias] + 1;
         }
         // get 'to' alias
         if (!isset($aliases[$to_alias])) {
             $uml .= 'state "' . $to . '" as ' . $to_alias . PHP_EOL;
             $aliases[$to_alias] = $to_alias;
             $uml .= "{$to_alias}: description: '" . $t_description . "'" . PHP_EOL;
             $uml .= "{$to_alias}: entry / '" . $t_entry . "'" . PHP_EOL;
             $uml .= "{$to_alias}: exit / '" . $t_exit . "'" . PHP_EOL;
         }
         // write transition information
         $uml .= $from_alias . ' --> ' . $to_alias;
         $uml .= " : <b><size:10>{$name_transition}</size></b>" . $EOL;
         $uml .= $event;
         $uml .= "transition order from '{$from}': <b>" . $order[$from_alias] . "</b>" . $EOL;
         $uml .= "rule/guard: '{$rule}'" . $EOL;
         $uml .= "command/action: '{$command}'" . $EOL;
         $uml .= $description;
         $uml .= PHP_EOL;
         // store possible end states aliases
         if ($t->getStateFrom()->isFinal()) {
             $end_states[$from_alias] = $from_alias;
         }
         if ($t->getStateTo()->isFinal()) {
             $end_states[$to_alias] = $to_alias;
         }
     }
     // only one begin state
     $initial = $machine->getInitialState();
     $initial = $initial->getName();
     $initial_alias = $this->plantUmlStateAlias($initial);
     if (!isset($aliases[$initial_alias])) {
         $uml .= 'state "' . $initial . '" as ' . $initial_alias . PHP_EOL;
     }
     $uml .= "[*] --> {$initial_alias}" . PHP_EOL;
     // note for initial alias with explanation
     $uml .= "note right of {$initial_alias} {$NEWLINE}";
     $uml .= "state diagram for machine '" . $machine->getMachine() . "'{$NEWLINE}";
     $uml .= "created by izzum plantuml generator {$NEWLINE}";
     $uml .= "@link http://plantuml.sourceforge.net/state.html\"" . $NEWLINE;
     $uml .= "end note" . $NEWLINE;
     // add end states to diagram
     foreach ($end_states as $end) {
         $uml .= "{$end} --> [*]" . PHP_EOL;
     }
     // close plantuml
     $uml .= "@enduml" . PHP_EOL;
     return $uml;
 }
 /**
  * {@inheritDoc}
  * Load the statemachine via a document in a mongodb collection.
  * 
  * The document, originally loaded as a json string (see JSON::getJSONSchema)
  * is stored at the mongodb collection 'configuration' by default.
  * multiple machine definitions can be provided in a single document, or in multiple documents in the collection.
  * The first document containing the 'machines.name' key with the value matching 
  * the name of the $statemachine is used.
  *
  * You could use the ReaderWriterDelegator to use another source to load the configuration from.
  */
 public function load(StateMachine $statemachine)
 {
     //use the JSON loader to load the configuration (see the json schema we expect in JSON::getJSONSchema)
     //mongodb does not store JSON but documents (converts the json structure) and the mongodb
     //php library returns these documents as php objects.
     //therefore, we json_encode it again, so it can be json_decoded in the JSON class :-(
     //alternatively, we could write a PHP Loader, but the assumption is that the speed gain is not worth it.
     $loader = new JSON(json_encode($this->getClient()->izzum->configuration->findOne(array("machines.name" => $statemachine->getContext()->getMachine()))));
     $count = $loader->load($statemachine);
     return $count;
 }
 * Example script that uses the 'standalone mode' as one of the four usage models for the statemachine.
 * The other three usage models being inheritance, composition and delegation.
 * 
 * This script allows you to interact with the statemachine from the command line
 * and does not use anything fancy for guard or transition logic.
 * To see an example of guard and transition logic for the standalone mode you
 * can see 'examples/standalone'
 * 
 * 
 * run this script from the (bash) command line:
 * php -f index.php
 * and stop it with ctrl+c
 */
//create machine. context defaults to in-memory state handling
$context = new Context(new Identifier("interactive-example", "interactive-machine"));
$machine = new StateMachine($context);
//define the states and the state types
$new = new State('new', State::TYPE_INITIAL);
$eating = new State('eating');
$drinking = new State('drinking');
$sleep = new State('sleep');
$hungry = new State('hungry');
$drunk = new State('drunk');
$smoking = new State('smoking');
$high = new State('high');
$dead = new State('dead', State::TYPE_FINAL);
//add transitions to the machine, with event names
$machine->addTransition(new Transition($new, $hungry, 'wakeup'));
$machine->addTransition(new Transition($hungry, $sleep, 'sleep'));
$machine->addTransition(new Transition($hungry, $eating, 'eat'));
$machine->addTransition(new Transition($hungry, $drinking, 'drink'));
 /**
  * {@inheritDoc}
  * This is an implemented method from the Loader interface.
  * All other methods are actually implemented methods from the Adapter
  * class.
  */
 public function load(StateMachine $statemachine)
 {
     $data = $this->getLoaderData($statemachine->getContext()->getMachine());
     // delegate to LoaderArray
     $loader = new LoaderArray($data);
     $loader->load($statemachine);
 }
 /**
  * @test
  */
 public function shouldBeAbleToStoreAndRetrieveData()
 {
     $redis = new Redis();
     $redis->setDatabase(15);
     //clear the redis database for testing
     $redis->flushdb();
     $machine = new StateMachine(new Context(new Identifier('1', 'test-machine'), null, $redis));
     //create the loader
     //get the configuration from the json file
     $configuration = file_get_contents(__DIR__ . '/../loader/fixture-example.json');
     //set it. normally, this would be done by a seperate process that has already loaded the configuration
     $redis->set(Redis::KEY_CONFIGURATION, $configuration);
     //load the machine
     $count = $redis->load($machine);
     //add the machine to the backend system
     $this->assertTrue($machine->add('this is the first addition'));
     $this->assertFalse($machine->add(), 'returns false, already added');
     $this->assertTrue($machine->run('this is a test run message'), 'succesful transitions so it returns true');
     $this->assertEquals('b', $machine->getCurrentState());
     $this->assertContains('1', $redis->getEntityIds('test-machine'));
     $this->assertTrue($machine->hasEvent('goToC'));
     try {
         $machine->goToC();
         $this->fail('should not come here');
     } catch (Exception $e) {
         $this->assertEquals(Exception::RULE_APPLY_FAILURE, $e->getCode());
     }
     $this->assertEquals('b', $machine->getCurrentState());
     //create new instance of same machine
     $machine2 = new StateMachine(new Context(new Identifier('1', 'test-machine'), null, $redis));
     $this->assertNotSame($machine2, $machine);
     $redis->load($machine2);
     $this->assertEquals('b', $machine2->getCurrentState(), 'should retrieve the same value');
     //create new instance of other machine
     $machine3 = new StateMachine(new Context(new Identifier('2', 'test-machine'), null, $redis));
     $this->assertNotSame($machine2, $machine3);
     $redis->load($machine3);
     $this->assertTrue($machine3->add());
     $this->assertNotEquals('b', $machine3->getCurrentState()->getName(), 'should not retrieve the same value as the other machine');
     $this->assertEquals('a', $machine3->getCurrentState()->getName(), 'initial state');
     //echo $machine3->toString(true);
     $this->assertEquals(2, $machine3->runToCompletion("go to the final state"));
     $this->assertEquals('done', $machine3->getCurrentState()->getName(), 'final state');
     $machine4 = new StateMachine(new Context(new Identifier('3', 'another-machine'), null, $redis));
     $a = new State('begin', State::TYPE_INITIAL);
     $b = new State('enter', State::TYPE_NORMAL);
     $c = new State('leave', State::TYPE_FINAL);
     $machine4->addTransition(new Transition($a, $b));
     $machine4->addTransition(new Transition($b, $c));
     $machine4->add('creating another machine to see that all goes well storing the data for multiple machines in redis');
     $this->assertEquals(2, $machine4->runToCompletion('running the machine to completion'));
     $ids = $redis->getEntityIds('test-machine');
     $this->assertEquals(array('1', '2'), $ids);
     $ids = $redis->getEntityIds('another-machine');
     $this->assertEquals(array('3'), $ids);
     $ids = $redis->getEntityIds('test-machine', 'done');
     $this->assertEquals(array('2'), $ids, 'only 2 was run to completion and in state done');
     $ids = $redis->getEntityIds('another-machine', 'leave');
     $this->assertEquals(array('3'), $ids, 'only 3 was run to completion and in state leave');
     //$redis->hmset("key" , array("name1" => "value1", "name2" => "value2"));
 }
 public function testFull()
 {
     $entity_id = "id";
     $machine = "test machine";
     $identifier = new Identifier($entity_id, $machine);
     $builder = new EntityBuilder();
     $io = new Memory();
     // all parameters
     $o = new Context($identifier, $builder, $io);
     $this->assertEquals($entity_id, $o->getEntityId());
     $this->assertEquals($machine, $o->getMachine());
     $this->assertNull($o->getStateMachine());
     $this->assertTrue(is_a($o->getPersistenceAdapter(), 'izzum\\statemachine\\persistence\\Memory'));
     $this->assertTrue(is_a($o->getBuilder(), 'izzum\\statemachine\\EntityBuilder'));
     $this->assertEquals($o->getIdentifier(), $o->getEntity());
     $this->assertTrue(is_string($o->getEntityId()));
     $this->assertTrue(is_string($o->toString()));
     $this->assertContains($entity_id, $o->toString());
     $this->assertContains($machine, $o->toString());
     $this->assertContains('izzum\\statemachine\\Context', $o->toString());
     // even though we have a valid reader, the state machine does not exist.
     $this->assertEquals(State::STATE_UNKNOWN, $o->getState());
     $this->assertTrue($o->setState('lala'));
     $this->assertEquals('lala', $o->getState());
     // for coverage.
     $this->assertNotNull($o->getId());
     $this->assertNotNull($o->getId(true));
     $this->assertNotNull($o->getId(false));
     $this->assertNotNull($o->getId(true, true));
     $this->assertNotNull($o->getId(false, true));
     // adding
     $machine = 'add-experiment-machine';
     $context = new Context(new Identifier('add-experiment-id', $machine), $builder, $io);
     $sm = new StateMachine($context);
     $sm->addTransition(new Transition(new State('c', State::TYPE_FINAL), new State('d'), State::TYPE_NORMAL));
     $sm->addTransition(new Transition(new State('a', State::TYPE_INITIAL), new State('b')));
     $this->assertCount(0, $context->getPersistenceAdapter()->getEntityIds($machine));
     $state = $sm->getInitialState()->getName();
     $this->assertEquals('a', $state);
     $this->assertTrue($context->add($state));
     // var_dump( Memory::get());
     $this->assertCount(1, $context->getPersistenceAdapter()->getEntityIds($machine));
 }