- converted class names and directories for PSR0 - replaced the basic __autoload() by the canonical PSR0 loader example. - added a GraphViz-based Grapher class to plot Machine structures.
336 lines
10 KiB
PHP
336 lines
10 KiB
PHP
<?php
|
|
/**
|
|
* Finite state machine writer.
|
|
*
|
|
* Keeping the FSM writer (generation) functions out of the root class minimizes
|
|
* the code volume for FSM readers, which should represent the majority of FSM
|
|
* users.
|
|
*
|
|
* @copyright (c) 2007-2012 Ouest Systèmes Informatiques
|
|
* @author Frédéric G. MARAND
|
|
* @license Licensed under the CeCILL 2.0
|
|
* @link http://wiki.audean.com/fsm/fsm
|
|
*/
|
|
|
|
namespace OSInet\Finite_State_Machine;
|
|
|
|
/**
|
|
* Needed notably for func_name()
|
|
*/
|
|
require_once('misc.php');
|
|
|
|
$erFiniteStateMachine = error_reporting(E_ALL|E_STRICT);
|
|
|
|
/**
|
|
* Read/Write concrete FSM class.
|
|
*
|
|
* This (concrete) class extends the default abstract Finite_State_Machine to
|
|
* add write behaviours. It is to be used by applications wishing to write FSM
|
|
* files. Other applications should not use it to avoid code bloat.
|
|
*/
|
|
class Writer extends Machine {
|
|
|
|
const FORMAT_VERSION = '1.1';
|
|
|
|
public function __construct() {
|
|
$this->fTransitions = array();
|
|
parent::__construct();
|
|
}
|
|
|
|
/**
|
|
* Return the initial and final states from an abstract FSM graph.
|
|
*
|
|
* @param array $graph
|
|
* A concrete FSM derived from the abstract FSM.
|
|
*
|
|
* @return array
|
|
*/
|
|
protected function getSpecialStates($graph) {
|
|
$ret = array(NULL, NULL);
|
|
foreach ($graph['states'] as $state) {
|
|
if (isset($state['type']) && $state['type'] == 'initial') {
|
|
$ret[0] = $state['name'];
|
|
}
|
|
if (isset($state['type']) && $state['type'] == 'final') {
|
|
$ret[1] = $state['name'];
|
|
}
|
|
if (isset($ret[0]) && isset($ret[1])) {
|
|
break;
|
|
}
|
|
}
|
|
return $ret;
|
|
}
|
|
|
|
/**
|
|
* Return the transitions (triggers) available from a given state
|
|
*
|
|
* @param array $graph
|
|
* A concrete Machine.
|
|
* @param string $id
|
|
* State id in the abstract graph (not id in the XML FSM).
|
|
*
|
|
* @return array
|
|
*/
|
|
protected function getTransitionsForState($graph, $id) {
|
|
$ret = array();
|
|
foreach ($graph['transitions'] as $txid => $transition) {
|
|
// echo "id = $id, txid = $txid from = " . $transition['from'] . PHP_EOL;
|
|
if ($transition['from'] == $id) {
|
|
$ret[] = $txid;
|
|
}
|
|
}
|
|
return $ret;
|
|
}
|
|
|
|
/**
|
|
* Generate the XML FSM file and optionally PHP stubs file.
|
|
*
|
|
* Source is an abstract FSM representation as an array of states/transitions.
|
|
*
|
|
* @param array $graph
|
|
* @param string $prefix
|
|
* @param boolean $php
|
|
* @param boolean $overwrite
|
|
*
|
|
* @todo take $overwrite and $php into account
|
|
*
|
|
* @deprecated
|
|
*/
|
|
public function oldSaveFsm($graph, $prefix = 'fsm', $php = FALSE, $overwrite = FALSE) {
|
|
echo "This is a dump for FSM $prefix"
|
|
. ', ' . ($php ? 'with' : 'without') . ' PHP generation'
|
|
. ', ' . ($overwrite ? 'with' : 'without') . ' overwrite mode'
|
|
. ".\n";
|
|
|
|
$doc = new DOMDocument('1.0', 'utf-8');
|
|
$comment = new DOMComment(' Generated by '
|
|
. basename(__FILE__, '.php')
|
|
. " version " . Fsm_Writer::VERSION
|
|
. " ");
|
|
$doc->appendChild($comment);
|
|
$fsm = new DOMElement('fsm');
|
|
$doc->appendChild($fsm);
|
|
$fsm->setAttribute('fsm_version', "1.3");
|
|
$fsm->setAttribute('idle_work', "1");
|
|
$fsm->setAttribute('allow_actions', "1");
|
|
list($init, $final) = $this->getSpecialStates($graph);
|
|
$fsm->setAttribute('init', $init);
|
|
$fsm->setAttribute('final', $final);
|
|
|
|
foreach ($graph['states'] as $id => $state) {
|
|
$state_node = new DOMElement('state');
|
|
$fsm->appendChild($state_node);
|
|
// setIdAttribute not yet implemented in php 5.1
|
|
$state_node->setAttribute('id', $state['name']);
|
|
$transitions = $this->getTransitionsForState($graph, $id);
|
|
|
|
/**
|
|
* FGM on ne peut pas simplement boucler ainsi: on a dans le graphe
|
|
* abstrait une transition par évènement, alors que dans la FSM les
|
|
* transitions partant du même état sont groupées. En se contentant
|
|
* de boucler ainsi, on crée un "event" pour chaque transition alors
|
|
* qu'il faut créer 1 event et "n" next.
|
|
*/
|
|
foreach ($transitions as $transition_id) {
|
|
$event_node = new DOMElement('event');
|
|
$state_node->appendChild($event_node);
|
|
$transition = $graph['transitions'][$transition_id];
|
|
$event_node->setAttribute('name', $transition['trigger']);
|
|
/**
|
|
* @todo support other event types
|
|
*/
|
|
$event_node->setAttribute('type', 'string');
|
|
}
|
|
}
|
|
$doc->save("$prefix.xml");
|
|
print_r($graph['transitions']);
|
|
}
|
|
|
|
/**
|
|
* Add a new state to the transitions table.
|
|
*
|
|
* @param string $name
|
|
*
|
|
* @return integer 0 on success, < 0 on failure
|
|
*/
|
|
public function addState($name) {
|
|
// echo func_name() . "($name)";
|
|
|
|
if (array_key_exists($name, $this->fTransitions)) {
|
|
$ret = -1;
|
|
}
|
|
else {
|
|
$this->fTransitions[$name] = array();
|
|
$ret = 0;
|
|
}
|
|
// echo ", ret = $ret\n";
|
|
return $ret;
|
|
}
|
|
|
|
/**
|
|
* Add an event definition (name + optional type of handler) to an existing state
|
|
*
|
|
* @param string $state
|
|
* @param string $event
|
|
* @param string $type
|
|
*
|
|
* @return integer
|
|
* - 0 on success
|
|
* - < 0 on failure
|
|
*/
|
|
public function addEvent($state, $event, $type = 'string') {
|
|
if (!array_key_exists($state, $this->fTransitions)) {
|
|
$ret = -1;
|
|
}
|
|
elseif (array_key_exists($event, $this->fTransitions[$state])) {
|
|
$ret = -2;
|
|
}
|
|
// state exists, event doesn't: OK
|
|
else {
|
|
$this->fTransitions[$state][$event] = array();
|
|
$this->fTransitions[$state][$event]["#type"] = $type;
|
|
$ret = 0;
|
|
}
|
|
|
|
return $ret;
|
|
}
|
|
|
|
/**
|
|
* Add an outcome definition to an existing event:
|
|
* - event handler result
|
|
* - next state
|
|
* - event to fire after transition to next state
|
|
* All outcome fields are optional
|
|
*
|
|
* @param name $state
|
|
* @param name $event
|
|
*
|
|
* @return integer
|
|
* - 0 on success
|
|
* - < 0 on failure
|
|
*/
|
|
public function addOutcome($state, $event,
|
|
$result = NULL, // if null, single possible outcome; only possible on void events
|
|
$next_state = NULL, // if null, stay on same state
|
|
$action = NULL) // if null, do not send an event after transitioning
|
|
{
|
|
$t = &$this->fTransitions;
|
|
if (!array_key_exists($state, $t)) {
|
|
$ret = -1;
|
|
}
|
|
elseif (!array_key_exists($event, $t[$state])) {
|
|
$ret = -2;
|
|
}
|
|
elseif (($result == NULL) && ($t[$state][$event]['#type'] <> 'void')) {
|
|
$ret = -3; // Undefined, single, results are only available for void events
|
|
}
|
|
elseif (($result != NULL) && array_key_exists($result, $t[$state][$event])) {
|
|
$ret = -4; // results must be unique per event
|
|
}
|
|
else {
|
|
$t[$state][$event][$result] = array($next_state, $action);
|
|
$ret = 0;
|
|
}
|
|
return $ret;
|
|
}
|
|
|
|
/**
|
|
* Defines the special states in the FSM
|
|
*
|
|
* @param string $kind Finite_State_Machine::INIT_STATE or ..FINAL_STATE
|
|
* @param string $name
|
|
*/
|
|
public function setSpecialState($kind, $name) {
|
|
// echo func_name() . "($kind, $name)\n";
|
|
if (($kind != Machine::INIT_STATE) && ($kind != Machine::FINAL_STATE)) {
|
|
$ret = -1; // unknown special state
|
|
}
|
|
elseif (!array_key_exists($name, $this->fTransitions)) {
|
|
$ret = -2; // non existent state declared as special
|
|
}
|
|
else {
|
|
$this->fTransitions["#$kind"] = $name;
|
|
$ret = 0;
|
|
}
|
|
return $ret;
|
|
}
|
|
|
|
/*
|
|
* Generate the XML FSM file and optionally PHP stubs file.
|
|
*
|
|
* Source is a valid instance of FSM.
|
|
*
|
|
* @param string $prefix
|
|
* @param boolean $php
|
|
* @param boolean $overwrite
|
|
*
|
|
* @todo take $overwrite and $php into account
|
|
*
|
|
* @throws Exception from within checkTransitions
|
|
*/
|
|
public function saveFsm($prefix = 'fsm', $php = FALSE, $overwrite = FALSE) {
|
|
$this->checkTransitions();
|
|
|
|
$t = &$this->fTransitions;
|
|
echo "This is a dump for FSM $prefix"
|
|
. ', ' . ($php ? 'with' : 'without') . ' PHP generation'
|
|
. ', ' . ($overwrite ? 'with' : 'without') . ' overwrite mode'
|
|
. ".\n";
|
|
|
|
$doc = new DOMDocument('1.0', 'utf-8');
|
|
$comment = new DOMComment(' Generated by '
|
|
. basename(__FILE__, '.php')
|
|
. " version " . Writer::FORMAT_VERSION
|
|
. " on FSM version " . Machine::VERSION
|
|
. " ");
|
|
$doc->appendChild($comment);
|
|
$fsm = new DOMElement('fsm');
|
|
$doc->appendChild($fsm);
|
|
$fsm->setAttribute('fsm_version', "1.3");
|
|
$fsm->setAttribute('idle_work', "1");
|
|
$fsm->setAttribute('allow_actions', "1");
|
|
|
|
$fsm->setAttribute('init', $t['#' . Machine::INIT_STATE]);
|
|
$fsm->setAttribute('final', $t['#' . Machine::FINAL_STATE]);
|
|
|
|
foreach ($this->fTransitions as $stateName => $state) {
|
|
if ($stateName[0] == '#') {
|
|
continue; // ignore special state definitions
|
|
}
|
|
$stateNode = new DOMElement('state', "\n");
|
|
$fsm->appendChild($stateNode);
|
|
$stateNode->setAttribute('id', $stateName);
|
|
foreach ($state as $eventName => $event) {
|
|
$eventNode = new DOMElement('event', "\n");
|
|
$stateNode->appendChild($eventNode);
|
|
$eventNode->setAttribute('name', $eventName);
|
|
$eventNode->setAttribute('type', $event['#type']);
|
|
foreach ($event as $outcomeName => $outcome) {
|
|
if ($outcomeName[0] == '#') {
|
|
continue; // ignore special outcome definitions (event type)
|
|
}
|
|
$outcomeNode = new DOMElement('next', "\n");
|
|
$eventNode->appendChild($outcomeNode);
|
|
/**
|
|
* Generated FSMs currently always use the "string" event handler type,
|
|
* meaning they always have outcome results. This will not always be
|
|
* the case, hence this test (think ftp.xml)
|
|
*/
|
|
if (!empty($outcomeName)) {
|
|
$outcomeNode->setAttribute('result', $outcomeName);
|
|
}
|
|
|
|
if (!empty($outcome[0])) {
|
|
$outcomeNode->setAttribute('state', $outcome[0]);
|
|
}
|
|
|
|
if (!empty($outcome[1])) {
|
|
$outcomeNode->setAttribute('action', $outcome[1]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$doc->save("$prefix.xml");
|
|
}
|
|
}
|