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 * - 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 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'); } /** * 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(); $events = array_keys($this->f_transitions[$this->f_state]); // echo func_name() . ": " . print_r($events, true). "\n"; 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(); 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]); //echo "outcomes for event $event_name: " . print_r($outcomes, true) . "\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 ", 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() . "\n"; 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 \"$this->f_state\""); $method_name = "f_$event_name"; $outcomes = $this->get_accepted_outcomes($event_name); $result = $this->$method_name(); if (!is_object($result)) { $result = new fsm_result($result, NULL); } if (!in_array($result->fsm_state, $outcomes)) throw new Exception(func_name() . ": event guard. Transition on \"$event_name\" return invalid result: " . var_dump($result) . "\n"); $this->f_state = $this->f_transitions[$this->f_state][$event_name][$result->fsm_state]; // echo func_name() . ", new state: $this->f_state, action: $result->fsm_action\n"; 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; } }