123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456 |
- <?php
- /**
- * Finite state machine skeleton
- *
- * (c) 2006 Ouest Systèmes Informatiques (OSI)
- * Licensed under the CeCILL 2.0 license
- *
- * $Id: u_fsm.php,v 1.4 2007-05-08 20:52:06 marand Exp $
- */
- require_once('misc.php'); // for func_name()
- error_reporting(E_ALL|E_STRICT);
- /**
- * This class defines a possible outcome for a given FSM transition
- *
- */
- class fsm_result
- {
- /**
- * The return value of the event handler
- * @var mixed
- */
- public $fsm_return;
- /**
- * The name of the state to which the FSM must change. If NULL, do not change
- * the current state.
- *
- * @var string
- */
- public $fsm_state;
- /**
- * The name of an event to be fired after the state change has been applied
- *
- * @var string
- */
- public $fsm_action;
- /**
- * @param string $state
- * @param string $action
- * @return void
- */
- public function __construct($return = NULL, $state = NULL, $action = NULL)
- {
- $this->fsm_return = $return;
- $this->fsm_state = $state;
- $this->fsm_action = $action;
- }
- }
- /**
- * This class must be inherited by code implementing actual FSMs.
- *
- * Applications must create a fsm descendant in which they will:
- *
- * - define the f_transitions table, usually in their constructor
- * - invoke the parent constructor to set up the FSM from the transitions table
- * - create "f_foo" functions for each "foo" event, except "idle", which is builtin.
- *
- * Applications may:
- * - disable event processing by setting $this->event to one of the fsm::EVENT_* constants
- * - disable post-event actions by setting $this->allows actions to false
- * - disable the builtin idle event by setting $this->idle_work to false
- * - query the current state by using $this->get_state()
- * - send an idle event by using $this->idle()
- * - submit any event (including idle) by using $this->apply_event($event_name)
- *
- */
- abstract class fsm
- {
- const IDLE_EVENT = 'idle'; // must not change name: method f_idle depends on it
- const INIT_STATE = 'init';
- const FINAL_STATE = 'final';
- const EVENT_NORMAL = 1; // processes events
- const EVENT_QUEUE = 2; // queue events for later use
- const EVENT_SINK = 3; // throw away events
- public $idle_work = TRUE;
- public $allow_actions = TRUE;
- protected $f_event_mode = fsm::EVENT_NORMAL;
- protected $f_queue = array(); // event queue for EVENT_QUEUE mode
- /**
- * the current state of the object
- * @var string
- */
- protected $f_state;
- /**
- * Transitions holds the transitions table
- * state1
- * event1
- * result1
- * state_name|fsm_result
- * event2
- * ...
- * ..
- * ..
- * @var array
- */
- protected $f_transitions = null;
- /**
- * constructor initializes the FSM to the first
- * state in the transitions table
- * @return void
- */
- public function __construct()
- {
- $this->_check_transitions();
- reset($this->f_transitions);
- $x = each($this->f_transitions);
- $x = $x[0];
- $this->f_state = $x;
- }
- /**
- * make sure a transitions graph has been defined
- * @return void
- */
- private function _check_transitions()
- {
- if (!isset($this->f_transitions))
- throw new Exception('No FSM processing is allowed without a transitions table\n');
- }
- /**
- * getter for f_state
- */
- public function get_state()
- {
- return $this->f_state;
- }
- /**
- * return the list of events accepted in the current state
- * @return array
- */
- public function get_accepted_events()
- {
- $this->_check_transitions();
- try
- {
- $events = array_keys($this->f_transitions[$this->f_state]);
- // echo func_name() . ": state $this->f_state, accepted events are:\n" . print_r($events, true). "\n";
- }
- catch (Exception $e)
- {
- echo "Exception in get_accepted_events" . print_r($e);
- print_r(debug_backtrace());
- $events = array();
- }
- return $events;
- }
- /**
- * return the list of outcome accepted in the current state for the give event
- * @param string $event_name
- * @param mixed $outcome
- * @return array
- */
- public function get_accepted_outcomes($event_name)
- {
- // echo func_name() . "\n";
- $this->_check_transitions();
- /**
- * Spare some paranioa
- *
- if (!$this->is_event_allowed($event_name))
- throw new Exception(func_name() . ": event \"$event_name\" not allowed in state \"$this->f_state\"\n");
- */
- $outcomes = array_keys($this->f_transitions[$this->f_state][$event_name]);
- // print_r($this->f_transitions[$this->f_state][$event_name]);
- // echo "outcomes for event $event_name: " . var_dump($outcomes) . "\n";
- return $outcomes;
- }
- /**
- * is this event accepted in the current state
- * the FSM is in ?
- *
- * @param string $event_name
- * @return boolean
- */
- public function is_event_allowed($event_name)
- {
- // echo func_name() . "($event_name)";
- $this->_check_transitions();
- $ret = in_array($event_name, $this->get_accepted_events());
- // echo " in state $this->f_state, result = <$ret>\n";
- return $ret;
- }
- /**
- * is a given outcome available for a given event,
- * considering the current context ?
- *
- * @param string $event_name
- * @param mixed $outcome
- * @return boolean
- */
- public function is_outcome_allowed($event_name, $outcome)
- {
- $this->_check_transitions();
- if (!$this->is_event_allowed($event_name))
- return false;
- $ret = array_key_exists($outcome, $this->get_accepted_outcomes($event_name));
- return $ret;
- }
- /**
- * apply an event, and the resulting event chain if needed
- *
- * @param string $event_name
- * @param array $params the
- * @return string resulting state
- */
- public function apply_event($event_name)
- {
- //echo func_name() . "\n";
- do {
- $result = $this->apply_simple_event($event_name);
- if ($this->allow_actions)
- {
- $event_name = $result->fsm_action; // can be NULL
- }
- else
- {
- $event_name = NULL;
- }
- } while($event_name);
- return $result;
- }
- /**
- * Helper for apply_event that does not implement the post-transition action
- *
- * @param string $event_name
- * @return fsm_result
- * @see apply_event()
- */
- private function apply_simple_event($event_name)
- {
- // echo func_name() . ", event = $event_name";
- $current_state = $this->f_state;
- if (($event_name == fsm::IDLE_EVENT) && !$this->idle_work)
- {
- return new fsm_result();
- }
- if (! $this->is_event_allowed($event_name))
- throw new Exception(func_name()
- . ": Event \"$event_name\" not accepted in current state \"$current_state\"");
- $outcomes = $this->get_accepted_outcomes($event_name);
- $method_name = "f_$event_name";
- if (!method_exists($this, $method_name))
- {
- die (func_name() . ": missing method "
- . get_class($this) . "::$method_name. Aborting.\n");
- }
- $outcome = $this->$method_name();
- if (!in_array($outcome, $outcomes))
- {
- throw new Exception(func_name()
- . ": event guard. Transition on \"$event_name\" return invalid outcome: "
- . print_r($outcome, true)
- . " for state $this->f_state\n");
- }
- $transition = &$this->f_transitions[$current_state][$event_name][$outcome];
- $result = new fsm_result
- (
- $outcome,
- $transition[0],
- $transition[1]
- );
- if (isset($result->fsm_state))
- {
- $this->f_state = $result->fsm_state;
- }
- /* print_r($this->f_transitions[$current_state][$event_name]);
- var_dump($result); */
- if (!isset($outcome))
- $outcome = 'NULL';
- /* echo func_name()
- . ": $current_state: " . $event_name . '[' . $outcome . ']'
- . " -> $this->f_state / " . $result->fsm_action . PHP_EOL;
- */
- return $result;
- }
- /**
- * Default event
- * @return boolean
- */
- public function f_idle()
- {
- return TRUE;
- }
- /**
- * Apply an fsm::IDLE_EVENT event. Do not confuse with f_idle !
- *
- * @return fsm_result
- */
- public function idle()
- {
- return $this->apply_event(fsm::IDLE_EVENT);
- }
- /**
- * return the current operating mode of the FSM
- * @return int
- */
- public function get_event_mode()
- {
- return $this->f_event_mode;
- }
- /**
- * set the current operating mode for the FSM
- *
- * @param int $mode fsm::EVENT_* constants
- */
- public function set_event_mode($mode)
- {
- switch ($mode)
- {
- case fsm::EVENT_NORMAL :
- if (count($this->f_queue) > 0)
- {
- while ($event = array_shift($this->f_queue))
- {
- $this->apply_event($event);
- }
- }
- break;
- case fsm::EVENT_QUEUE : // nothing special to do
- break;
- case fsm::EVENT_SINK : // empty queue if needed
- if (count($this->f_queue) > 0)
- {
- $this->f_queue = array();
- }
- break;
- default:
- throw new Exception("Trying to set unknown FSM mode $mode");
- }
- $this->f_event_mode = $mode;
- return $this->f_event_mode;
- }
- /**
- * Load the f_transitions table from an external resource.
- *
- * @param string $url Optional: defaults to the name of the class, . ".xml"
- */
- public function load_fsm($url = NULL)
- {
- $osd = FALSE; // on screen display (debug)
- if ($osd)
- echo "Loading FSM from $url\n";
- if (!isset($url))
- {
- $url = get_class($this) . ".xml";
- }
- $fsm = simplexml_load_file($url);
- $fsm_version = (string) $fsm['fsm_version'];
- if ($fsm_version !== '1.3')
- {
- die("Revision $fsm_version of schema is not supported.\n");
- }
- $this->idle_work = ($fsm['idle_work'] == 1);
- $this->allow_actions = ($fsm['allow_actions'] == 1);
- $this->f_transitions = array();
- $t = &$this->f_transitions;
- // (string) casts in this loop are required: RHS is a SimpleXMLElement
- foreach ($fsm->state as $state)
- {
- $id = (string) $state['id'];
- if ($osd)
- echo "State $id :\n";
- $t[$id] = array();
- switch ($id)
- {
- case fsm::INIT_STATE :
- if ($osd)
- echo " Initial state\n";
- break;
- case fsm::FINAL_STATE :
- if ($osd)
- echo " Final state\n";
- break;
- }
- foreach ($state->event as $event)
- {
- $ename = (string) $event['name'];
- if ($osd)
- echo " Event $ename";
- if (!isset($event['type']))
- $event['type'] = 'void';
- $etype = (string) $event['type'];
- if ($osd)
- echo ", type $etype\n";
- foreach ($event as $next)
- {
- if ($event['type'] == 'void')
- {
- $next['result'] = 'always';
- $eresult = null;
- }
- else
- $eresult = (string) $next['result'];
- if (!isset($next['state']))
- $next['state'] = (string) $state['id'];
- if ($osd)
- echo " Next(" . $next['result'] . ') = ' . $next['state'];
- if ($osd)
- if (isset($next['action']))
- echo " + event " . $next['action'];
- if ($osd)
- echo PHP_EOL;
- $t[$id][$ename][$eresult] = array(
- (string) $next['state'],
- (string) $next['action']);
- }
- }
- }
- }
- }
|