A State instance can (and should) be shared by multiple Transition
objects when it is the same State for their origin/from State.
A State can be a regex state (or negated regex).
A regex state can be used in a transition and when added to a
statemachine the regular expression will be matched on all currently
known states on that statemachine and new Transitions will be added
to the statemachine that match the from/to state regexes. This is very
useful to build a lot of transitions very quickly.
to build a full mesh of transitions (all states to all states):
$a = new State('a');
$b = new State('b');
$c = new State('c');
$machine->addState($a);
$machine->addState($b);
$machine->addState($c);
$state_regex_all = new State('regex:|.*|');
$machine->addTransition(new Transition($state_regex_all, $state_regex_all));
protected function createLoader() { //we use the array loader //in a non-example situation we would use a backend like a //database for example //@see PDO adapter and loader //define the states $new = new State('new', State::TYPE_INITIAL); $green = new State('green', State::TYPE_NORMAL, State::COMMAND_NULL); $orange = new State('orange', State::TYPE_NORMAL); $red = new State('red', State::TYPE_NORMAL); //create the transtions by using the states $ng = new Transition($new, $green, 'go-green', Transition::RULE_TRUE, Transition::COMMAND_NULL); $go = new Transition($green, $orange, 'go-orange', 'izzum\\examples\\trafficlight\\rules\\CanSwitch', 'izzum\\examples\\trafficlight\\command\\SwitchOrange'); $or = new Transition($orange, $red, 'go-red', 'izzum\\examples\\trafficlight\\rules\\CanSwitch', 'izzum\\examples\\trafficlight\\command\\SwitchRed'); $rg = new Transition($red, $green, 'go-green', 'izzum\\examples\\trafficlight\\rules\\CanSwitch', 'izzum\\examples\\trafficlight\\command\\SwitchGreen'); //set some descriptions for uml generation $ng->setDescription("from green to orange. use the switch to orange command"); $go->setDescription("from new to green. this will start the cycle"); $or->setDescription("from orange to red. use the appropriate command"); $rg->setDescription("from red back to green."); $new->setDescription('the init state'); $green->setDescription("go!"); $orange->setDescription("looks like a shade of green..."); $red->setDescription('stop'); $transitions[] = $ng; $transitions[] = $go; $transitions[] = $or; $transitions[] = $rg; $loader = new LoaderArray($transitions); return $loader; }
/** * sets the state as the current state and on the backend. * This should only be done: * - initially, right after a machine has been created, to set it in a * certain state if the state has not been persisted before. * - when changing context (since this resets the current state) via * $machine->setState($machine->getCurrentState()) * * This method allows you to bypass the transition guards and the transition * logic. no exit/entry/transition logic will be performed * * @param State $state * @param string $message optional message. this can be used by the persistence adapter * to be part of the transition history to provide extra information about the transition. * @throws Exception in case the state is not valid/known for this machine */ public function setState(State $state, $message = null) { if ($this->getState($state->getName()) === null) { throw new Exception(sprintf("%s state '%s' not known to this machine", $this->toString(), $state->getName()), Exception::SM_UNKNOWN_STATE); } //get the state known to this machine so we are sure we have the correct reference //even if the client provides another instance of State with the same name $state = $this->getState($state->getName()); $this->getContext()->setState($state->getName(), $message); $this->state = $state; }
/** * does an input regex state match a target states' name? * * @param State $regex * the regex state * @param State $target * the state to match the regular expression to * @return boolean * @link https://php.net/manual/en/function.preg-match.php * @link http://regexr.com/ for trying out regular expressions */ public static function matchesRegex(State $regex, State $target) { $matches = false; if ($regex->isNormalRegex()) { $expression = str_replace(State::REGEX_PREFIX, '', $regex->getName()); $matches = preg_match($expression, $target->getName()) === 1; } if ($regex->isNegatedRegex()) { $expression = str_replace(State::REGEX_PREFIX_NEGATED, '', $regex->getName()); $matches = preg_match($expression, $target->getName()) !== 1; } return $matches; }
/** * {@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); }
/** * gets all data for transitions. * This method is public for testing purposes * * @param string $machine * the machine name * @return Transition[] */ public function getLoaderData($machine) { $rows = $this->getTransitions($machine); $output = array(); // array for local caching of states $states = array(); foreach ($rows as $row) { $state_from = $row['state_from']; $state_to = $row['state_to']; // create the 'from' state if (isset($states[$state_from])) { $from = $states[$state_from]; } else { $from = new State($row['state_from'], $row['state_from_type'], $row['state_from_entry_command'], $row['state_from_exit_command']); $from->setDescription($row['state_from_description']); } // cache the 'from' state for the next iterations $states[$from->getName()] = $from; // create the 'to' state if (isset($states[$state_to])) { $to = $states[$state_to]; } else { $to = new State($row['state_to'], $row['state_to_type'], $row['state_to_entry_command'], $row['state_to_exit_command']); $to->setDescription($row['state_to_description']); } // cache to 'to' state for the next iterations $states[$to->getName()] = $to; // build the transition $transition = new Transition($from, $to, $row['event'], $row['rule'], $row['command']); $transition->setDescription($row['transition_description']); $output[] = $transition; } return $output; }
/** * @test * @group regex */ public function shouldReturnArrayOfMatchedStates() { $a = new State('a'); $b = new State('ab'); $c = new State('ba'); $d = new State('abracadabra'); $e = new State('action-hero'); $f = new State('action-bad-guy'); $g = new State('ac'); $targets = array($a, $b, $c, $d, $e, $f, $g); $regex = new State('regex:/.*/'); $this->assertEquals($targets, Utils::getAllRegexMatchingStates($regex, $targets)); $regex = new State('regex:/^a.*/'); $this->assertEquals(array($a, $b, $d, $e, $f, $g), Utils::getAllRegexMatchingStates($regex, $targets)); $regex = new State('regex:/^a.+/'); $this->assertEquals(array($b, $d, $e, $f, $g), Utils::getAllRegexMatchingStates($regex, $targets)); $regex = new State('regex:/^a.*a.+$/'); $this->assertEquals(array($d, $f), Utils::getAllRegexMatchingStates($regex, $targets)); $regex = new State('regex:/^ac.*-.+$/'); $this->assertEquals(array($e, $f), Utils::getAllRegexMatchingStates($regex, $targets)); $regex = new State('ac'); $this->assertFalse($regex->isRegex()); $this->assertEquals(array($g), Utils::getAllRegexMatchingStates($regex, $targets), 'non regex state'); }
/** * @test * @group regex */ public function shouldNotReturnRegexState() { $name = 'rege:.*'; $regex = new State($name); $this->assertFalse($regex->isRegex()); $this->assertFalse($regex->isNormalRegex()); $this->assertFalse($regex->isNegatedRegex()); }
/** * {@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); }
/** * {@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; 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); }
/** * * @param State $state_from * @param State $state_to * @param string $event * optional: an event name by which this transition can be * triggered * @param string $rule * optional: one or more fully qualified Rule (sub)class name(s) * to check to see if we are allowed to transition. * This can actually be a ',' seperated string of multiple rules * that will be applied as a chained 'and' rule. * @param string $command * optional: one or more fully qualified Command (sub)class * name(s) to execute for a transition. * This can actually be a ',' seperated string of multiple * commands that will be executed as a composite. * @param callable $callable_guard * optional: a php callable to call. eg: "function(){echo 'closure called';};" * @param callable $callable_transition * optional: a php callable to call. eg: "izzum\MyClass::myStaticMethod" */ public function __construct(State $state_from, State $state_to, $event = null, $rule = self::RULE_EMPTY, $command = self::COMMAND_EMPTY, $callable_guard = self::CALLABLE_NULL, $callable_transition = self::CALLABLE_NULL) { $this->state_from = $state_from; $this->state_to = $state_to; $this->setRuleName($rule); $this->setCommandName($command); $this->setGuardCallable($callable_guard); $this->setTransitionCallable($callable_transition); // setup bidirectional relationship with state this transition // originates from. only if it's not a regex or final state type if (!$state_from->isRegex() && !$state_from->isFinal()) { $state_from->addTransition($this); } // set and sanitize event name $this->setEvent($event); }
/** * @test */ public function shouldHaveBidirectionalAssociation() { $from = new State('a'); $to = new State('b'); $transition = new Transition($from, $to); $this->assertTrue($from->hasTransition($transition->getName())); $this->assertFalse($to->hasTransition($transition->getName()), 'not on an incoming transition'); }