fsm_state = $state; $this->fsm_action = $action; } } abstract class fsm { /** * 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); $event_name = $result->fsm_action; // can be 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 (! $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; } }