|
@@ -0,0 +1,355 @@
|
|
|
+<?php
|
|
|
+
|
|
|
+ * Finite state machine writer.
|
|
|
+ *
|
|
|
+ * Keeping the FSM writer (generation) functions out of the root class minimize
|
|
|
+ * the code volume for FSM readers, which should represent the majority of FSM
|
|
|
+ * users.
|
|
|
+ *
|
|
|
+ * @copyright (c) 2007 OSI
|
|
|
+ * @author Frédéric G. MARAND
|
|
|
+ * @license Licensed under the CeCILL 2.0
|
|
|
+ * @version CVS: $Id: Fsm_Writer.php,v 1.1 2007-06-10 19:42:35 marand Exp $
|
|
|
+ * @link http:
|
|
|
+ * @since Not applicable yet
|
|
|
+ * @package fsm
|
|
|
+ * @subpackage fsm.core
|
|
|
+ */
|
|
|
+
|
|
|
+
|
|
|
+ * needed notably for autoload and 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.
|
|
|
+ * @package fsm
|
|
|
+ * @subpackage fsm.core
|
|
|
+ */
|
|
|
+class Fsm_Writer extends Finite_State_Machine
|
|
|
+ {
|
|
|
+ const VERSION = '$Id: Fsm_Writer.php,v 1.1 2007-06-10 19:42:35 marand Exp $';
|
|
|
+
|
|
|
+ public function __construct()
|
|
|
+ {
|
|
|
+ $this->f_transitions = array();
|
|
|
+ parent::__construct();
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ * Return the initial and final states from an abstract FSM graph
|
|
|
+ *
|
|
|
+ * @param array $graph Abstract FSM
|
|
|
+ * @return array
|
|
|
+ */
|
|
|
+ protected function getSpecialStates($graph)
|
|
|
+ {
|
|
|
+ $ret[0] = NULL;
|
|
|
+ $ret[1] = 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 Abstract FSM
|
|
|
+ * @param string $id State id in the abstract graph (not id in the XML FSM)
|
|
|
+ */
|
|
|
+ protected function getTransitionsForState($graph, $id)
|
|
|
+ {
|
|
|
+ $ret = array();
|
|
|
+ foreach ($graph['transitions'] as $txid => $transition)
|
|
|
+ {
|
|
|
+
|
|
|
+ if ($transition['from'] == $id)
|
|
|
+ {
|
|
|
+ $ret[] = $txid;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return $ret;
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ * Generate the XML FSM file and optionally PHP stubs file from an abstract
|
|
|
+ * FSM representation as an array of states and transitions
|
|
|
+ *
|
|
|
+ * @param array $graph
|
|
|
+ * @param string $prefix
|
|
|
+ * @param boolean $php
|
|
|
+ * @param boolean $overwrite
|
|
|
+ * @todo take $overwrite and $php into account
|
|
|
+ * @deprecated
|
|
|
+ */
|
|
|
+ public function old_save_fsm($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);
|
|
|
+ $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)
|
|
|
+ {
|
|
|
+
|
|
|
+
|
|
|
+ if (array_key_exists($name, $this->f_transitions))
|
|
|
+ {
|
|
|
+ $ret = -1;
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ $this->f_transitions[$name] = array();
|
|
|
+ $ret = 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ 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->f_transitions))
|
|
|
+ {
|
|
|
+ $ret = -1;
|
|
|
+ }
|
|
|
+ elseif (array_key_exists($event, $this->f_transitions[$state]))
|
|
|
+ {
|
|
|
+ $ret = -2;
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ $this->f_transitions[$state][$event] = array();
|
|
|
+ $this->f_transitions[$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,
|
|
|
+ $next_state = NULL,
|
|
|
+ $action = NULL)
|
|
|
+ {
|
|
|
+ $t = &$this->f_transitions;
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+ elseif (($result != NULL) && array_key_exists($result, $t[$state][$event]))
|
|
|
+ {
|
|
|
+ $ret = -4;
|
|
|
+ }
|
|
|
+ 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)
|
|
|
+ {
|
|
|
+
|
|
|
+ if (($kind != Finite_State_Machine::INIT_STATE) && ($kind != Finite_State_Machine::FINAL_STATE))
|
|
|
+ {
|
|
|
+ $ret = -1;
|
|
|
+ }
|
|
|
+ elseif (!array_key_exists($name, $this->f_transitions))
|
|
|
+ {
|
|
|
+ $ret = -2;
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ $this->f_transitions["#$kind"] = $name;
|
|
|
+ $ret = 0;
|
|
|
+ }
|
|
|
+ return $ret;
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ * Generate the XML FSM file and optionally PHP stubs file from a valid
|
|
|
+ * instanc of Finite_State_Machine
|
|
|
+ *
|
|
|
+ * @param string $prefix
|
|
|
+ * @param boolean $php
|
|
|
+ * @param boolean $overwrite
|
|
|
+ * @todo take $overwrite and $php into account
|
|
|
+ * @throws Exception from within _check_transitions
|
|
|
+ */
|
|
|
+ public function save_fsm($prefix = 'fsm', $php = FALSE, $overwrite = FALSE)
|
|
|
+ {
|
|
|
+ $this->_check_transitions();
|
|
|
+
|
|
|
+ $t = &$this->f_transitions;
|
|
|
+ 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
|
|
|
+ . " on FSM version " . Finite_State_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['#' . Finite_State_Machine::INIT_STATE]);
|
|
|
+ $fsm->setAttribute('final', $t['#' . Finite_State_Machine::FINAL_STATE]);
|
|
|
+
|
|
|
+ foreach ($this->f_transitions as $state_name => $state)
|
|
|
+ {
|
|
|
+ if ($state_name[0] == '#')
|
|
|
+ {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ $state_node = new DOMElement('state', "\n");
|
|
|
+ $fsm->appendChild($state_node);
|
|
|
+ $state_node->setAttribute('id', $state_name);
|
|
|
+ foreach ($state as $event_name => $event)
|
|
|
+ {
|
|
|
+ $event_node = new DOMElement('event', "\n");
|
|
|
+ $state_node->appendChild($event_node);
|
|
|
+ $event_node->setAttribute('name', $event_name);
|
|
|
+ $event_node->setAttribute('type', $event['#type']);
|
|
|
+ foreach ($event as $outcome_name => $outcome)
|
|
|
+ {
|
|
|
+ if ($outcome_name[0] == '#')
|
|
|
+ {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ $outcome_node = new DOMElement('next', "\n");
|
|
|
+ $event_node->appendChild($outcome_node);
|
|
|
+
|
|
|
+ * 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($outcome_name))
|
|
|
+ {
|
|
|
+ $outcome_node->setAttribute('result', $outcome_name);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!empty($outcome[0]))
|
|
|
+ {
|
|
|
+ $outcome_node->setAttribute('state', $outcome[0]);
|
|
|
+ }
|
|
|
+ if (!empty($outcome[1]))
|
|
|
+ {
|
|
|
+ $outcome_node->setAttribute('action', $outcome[1]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ $doc->save("$prefix.xml");
|
|
|
+ }
|
|
|
+ }
|