| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413 | <?phpnamespace OSInet\Finite_State_Machine;/** * Abstract state machine. * * @copyright  (c) 2007-2012 Ouest Systèmes Informatiques * @author     Frederic G. MARAND * @license    CeCILL 2.0 * @link       http://wiki.audean.com/fsm/fsm * @since      FSM 1.6 * * This class must be inherited by code implementing actual FSMs. * * Applications must create a Machine descendant in which they will: *   - define the fTransitions 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->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']);        }      }    }  }}
 |