123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413 |
- <?php
- namespace Osinet\FiniteStateMachine;
- /**
- * 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']);
- }
- }
- }
- }
- }
|