event to one of the fsm::EVENT_* constants * - disable post-event actions by setting $this->allowsActions to false * - disable the builtin idle event by setting $this->idleWork to false * - query the current state by using $this->getState() * - send an idle event by using $this->idle() * - submit any event (including idle) by using $this->applyEvent($eventName) */ abstract class Machine { const VERSION = '12D22'; const IDLE_EVENT = 'idle'; // must not change name: method fIdle 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 $idleWork = TRUE; public $allowActions = TRUE; protected $fEventMode = self::EVENT_NORMAL; protected $fQueue = array(); // event queue for EVENT_QUEUE mode /** * the current state of the object * @var string */ protected $fState; /** * Transitions holds the transitions table * state1 * event1 * result1 * state_name|Result * event2 * ... * .. * .. * @var array */ public $fTransitions = null; /** * constructor initializes the FSM to the first * state in the transitions table * @return void */ public function __construct() { $this->checkTransitions(); reset($this->fTransitions); $x = each($this->fTransitions); $x = $x[0]; $this->fState = $x; } /** * Make sure a transitions graph has been defined * * @return void */ protected function checkTransitions() { if (!isset($this->fTransitions)) throw new Exception('No FSM processing is allowed without a transitions table\n'); } /** * Getter for fState * * @return string */ public function getState() { return $this->fState; } /** * return the list of events accepted in the current state * @return array */ public function getAcceptedEvents() { $this->checkTransitions(); try { $events = array_keys($this->fTransitions[$this->fState]); // echo func_name() . ": state $this->fState, accepted events are:\n" . print_r($events, true). "\n"; } catch (Exception $e) { echo "Exception in getAcceptedEvents" . 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 $eventName * @param mixed $outcome * @return array */ public function getAcceptedOutcomes($eventName) { // echo func_name() . "\n"; $this->checkTransitions(); /** * Spare some paranioa * if (!$this->isEventAllowed($eventName)) throw new Exception(func_name() . ": event \"$eventName\" not allowed in state \"$this->fState\"\n"); */ $outcomes = array_keys($this->fTransitions[$this->fState][$eventName]); // print_r($this->fTransitions[$this->fState][$eventName]); // echo "outcomes for event $eventName: " . var_dump($outcomes) . "\n"; return $outcomes; } /** * is this event accepted in the current state * the FSM is in ? * * @param string $eventName * @return boolean */ public function isEventAllowed($eventName) { // echo func_name() . "($eventName)"; $this->checkTransitions(); $ret = in_array($eventName, $this->getAcceptedEvents()); // echo " in state $this->fState, result = <$ret>\n"; return $ret; } /** * is a given outcome available for a given event, * considering the current context ? * * @param string $eventName * @param mixed $outcome * @return boolean */ public function isOutcomeAllowed($eventName, $outcome) { $this->checkTransitions(); if (!$this->isEventAllowed($eventName)) { return false; } $ret = array_key_exists($outcome, $this->getAcceptedOutcomes($eventName)); return $ret; } /** * apply an event, and the resulting event chain if needed * * @param string $eventName * @param array $params the * @return Result resulting state */ public function applyEvent($eventName) { // echo "Start of " . func_name() . "\n"; do { $result = $this->applySimpleEvent($eventName); if ($this->allowActions) { $eventName = $result->fsmAction; // can be NULL } else { $eventName = NULL; } } while ($eventName); // echo "End of " . func_name() . "\n"; return $result; } /** * Helper for applyEvent that does not implement the post-transition action * * @see applyEvent() * * @param string $eventName * * @return Result */ private function applySimpleEvent($eventName) { // echo "Start of " . func_name() . ", event = $eventName\n"; $currentState = $this->fState; if (($eventName == self::IDLE_EVENT) && !$this->idleWork) { return new Result(); } if (!$this->isEventAllowed($eventName)) { die("Event $eventName not allowed in current state $currentState.\n"); /* throw new Exception(func_name() . ": Event \"$eventName\" not accepted in current state \"$currentState\""); */ } $outcomes = $this->getAcceptedOutcomes($eventName); $methodName = "event$eventName"; if (!method_exists($this, $methodName)) { die (func_name() . ": missing method " . get_class($this) . "::$methodName. Aborting.\n"); } $outcome = $this->$methodName(); if (!in_array($outcome, $outcomes)) { throw new \Exception(func_name() . ": event guard. Transition on \"$eventName\" return invalid outcome: " . print_r($outcome, true) . " for state $this->fState\n"); } $transition = &$this->fTransitions[$currentState][$eventName][$outcome]; $result = new Result ( $outcome, $transition[0], $transition[1] ); if (isset($result->fsmState)) { $this->fState = $result->fsmState; } /* print_r($this->fTransitions[$currentState][$eventName]); var_dump($result); */ if (!isset($outcome)) { $outcome = 'NULL'; } /* echo func_name() . ": $currentState: " . $eventName . '[' . $outcome . ']' . " -> $this->fState / " . $result->fsmAction . PHP_EOL; */ return $result; } /** * Default event. * * @return boolean */ public function fIdle() { return TRUE; } /** * Apply an fsm::IDLE_EVENT event. Do not confuse with fIdle ! * * @return Result */ public function idle() { return $this->applyEvent(self::IDLE_EVENT); } /** * return the current operating mode of the FSM * @return int */ public function getEventMode() { return $this->fEventMode; } /** * set the current operating mode for the FSM * * @param int $mode fsm::EVENT_* constants * @return int $mode fsm::EVENT_* constants */ public function setEventMode($mode) { switch ($mode) { case self::EVENT_NORMAL : if (count($this->fQueue) > 0) { while (($event = array_shift($this->fQueue)) !== NULL) { $this->applyEvent($event); } } break; // Nothing special to do case self::EVENT_QUEUE: break; // Empty queue if needed case self::EVENT_SINK : if (count($this->fQueue) > 0) { $this->fQueue = array(); } break; default: throw new Exception("Trying to set unknown FSM mode $mode"); } $this->fEventMode = $mode; return $this->fEventMode; } /** * Load the fTransitions table from an external resource. * * @param string $url * Optional: defaults to the name of the class, . ".xml" */ public function loadFsm($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); $fsmVersion = (string) $fsm['fsm_version']; if ($fsmVersion !== '1.3') { die("Revision $fsmVersion of schema is not supported.\n"); } $this->idleWork = ($fsm['idle_work'] == 1); $this->allowActions = ($fsm['allow_actions'] == 1); $this->fTransitions = array(); $t = &$this->fTransitions; // (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 self::INIT_STATE : if ($osd) { echo " Initial state\n"; } break; case self::FINAL_STATE : if ($osd) { echo " Final state\n"; } break; } foreach ($state->event as $event) { $name = (string) $event['name']; if ($osd) { echo " Event $name"; } if (!isset($event['type'])) { $event['type'] = 'void'; } $eventType = (string) $event['type']; if ($osd) { echo ", type $eventType\n"; } foreach ($event as $next) { if ($event['type'] == 'void') { $next['result'] = 'always'; $result = null; } else { $result = (string) $next['result']; } if (!isset($next['state'])) { $next['state'] = (string) $state['id']; } if ($osd) { echo " Next(" . $next['result'] . ') = ' . $next['state']; if (isset($next['action'])) { echo " + event " . $next['action']; } echo PHP_EOL; } $t[$id][$name][$result] = array( (string) $next['state'], (string) $next['action']); } } } } }