Machine.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413
  1. <?php
  2. namespace Osinet\FiniteStateMachine;
  3. /**
  4. * Abstract state machine.
  5. *
  6. * @copyright (c) 2007-2012 Ouest Systèmes Informatiques
  7. * @author Frederic G. MARAND
  8. * @license CeCILL 2.0
  9. * @link http://wiki.audean.com/fsm/fsm
  10. * @since FSM 1.6
  11. *
  12. * This class must be inherited by code implementing actual FSMs.
  13. *
  14. * Applications must create a Machine descendant in which they will:
  15. * - define the fTransitions table, usually in their constructor
  16. * - invoke the parent constructor to set up the FSM from the transitions table
  17. * - create "f_foo" functions for each "foo" event, except "idle", which is builtin.
  18. *
  19. * Applications may:
  20. * - disable event processing by setting $this->event to one of the fsm::EVENT_* constants
  21. * - disable post-event actions by setting $this->allowsActions to false
  22. * - disable the builtin idle event by setting $this->idleWork to false
  23. * - query the current state by using $this->getState()
  24. * - send an idle event by using $this->idle()
  25. * - submit any event (including idle) by using $this->applyEvent($eventName)
  26. */
  27. abstract class Machine {
  28. const VERSION = '12D22';
  29. const IDLE_EVENT = 'idle'; // must not change name: method fIdle depends on it
  30. const INIT_STATE = 'init';
  31. const FINAL_STATE = 'final';
  32. const EVENT_NORMAL = 1; // processes events
  33. const EVENT_QUEUE = 2; // queue events for later use
  34. const EVENT_SINK = 3; // throw away events
  35. public $idleWork = TRUE;
  36. public $allowActions = TRUE;
  37. protected $fEventMode = self::EVENT_NORMAL;
  38. protected $fQueue = array(); // event queue for EVENT_QUEUE mode
  39. /**
  40. * the current state of the object
  41. * @var string
  42. */
  43. protected $fState;
  44. /**
  45. * Transitions holds the transitions table
  46. * state1
  47. * event1
  48. * result1
  49. * state_name|Result
  50. * event2
  51. * ...
  52. * ..
  53. * ..
  54. * @var array
  55. */
  56. public $fTransitions = null;
  57. /**
  58. * constructor initializes the FSM to the first
  59. * state in the transitions table
  60. * @return void
  61. */
  62. public function __construct() {
  63. $this->checkTransitions();
  64. reset($this->fTransitions);
  65. $x = each($this->fTransitions);
  66. $x = $x[0];
  67. $this->fState = $x;
  68. }
  69. /**
  70. * Make sure a transitions graph has been defined
  71. *
  72. * @return void
  73. */
  74. protected function checkTransitions() {
  75. if (!isset($this->fTransitions))
  76. throw new Exception('No FSM processing is allowed without a transitions table\n');
  77. }
  78. /**
  79. * Getter for fState
  80. *
  81. * @return string
  82. */
  83. public function getState() {
  84. return $this->fState;
  85. }
  86. /**
  87. * return the list of events accepted in the current state
  88. * @return array
  89. */
  90. public function getAcceptedEvents() {
  91. $this->checkTransitions();
  92. try {
  93. $events = array_keys($this->fTransitions[$this->fState]);
  94. // echo func_name() . ": state $this->fState, accepted events are:\n" . print_r($events, true). "\n";
  95. }
  96. catch (Exception $e) {
  97. echo "Exception in getAcceptedEvents" . print_r($e);
  98. print_r(debug_backtrace());
  99. $events = array();
  100. }
  101. return $events;
  102. }
  103. /**
  104. * return the list of outcome accepted in the current state for the give event
  105. * @param string $eventName
  106. * @param mixed $outcome
  107. * @return array
  108. */
  109. public function getAcceptedOutcomes($eventName) {
  110. // echo func_name() . "\n";
  111. $this->checkTransitions();
  112. /**
  113. * Spare some paranioa
  114. *
  115. if (!$this->isEventAllowed($eventName))
  116. throw new Exception(func_name() . ": event \"$eventName\" not allowed in state \"$this->fState\"\n");
  117. */
  118. $outcomes = array_keys($this->fTransitions[$this->fState][$eventName]);
  119. // print_r($this->fTransitions[$this->fState][$eventName]);
  120. // echo "outcomes for event $eventName: " . var_dump($outcomes) . "\n";
  121. return $outcomes;
  122. }
  123. /**
  124. * is this event accepted in the current state
  125. * the FSM is in ?
  126. *
  127. * @param string $eventName
  128. * @return boolean
  129. */
  130. public function isEventAllowed($eventName) {
  131. // echo func_name() . "($eventName)";
  132. $this->checkTransitions();
  133. $ret = in_array($eventName, $this->getAcceptedEvents());
  134. // echo " in state $this->fState, result = <$ret>\n";
  135. return $ret;
  136. }
  137. /**
  138. * is a given outcome available for a given event,
  139. * considering the current context ?
  140. *
  141. * @param string $eventName
  142. * @param mixed $outcome
  143. * @return boolean
  144. */
  145. public function isOutcomeAllowed($eventName, $outcome) {
  146. $this->checkTransitions();
  147. if (!$this->isEventAllowed($eventName)) {
  148. return false;
  149. }
  150. $ret = array_key_exists($outcome, $this->getAcceptedOutcomes($eventName));
  151. return $ret;
  152. }
  153. /**
  154. * apply an event, and the resulting event chain if needed
  155. *
  156. * @param string $eventName
  157. * @param array $params the
  158. * @return Result resulting state
  159. */
  160. public function applyEvent($eventName) {
  161. // echo "Start of " . func_name() . "\n";
  162. do {
  163. $result = $this->applySimpleEvent($eventName);
  164. if ($this->allowActions) {
  165. $eventName = $result->fsmAction; // can be NULL
  166. } else {
  167. $eventName = NULL;
  168. }
  169. } while ($eventName);
  170. // echo "End of " . func_name() . "\n";
  171. return $result;
  172. }
  173. /**
  174. * Helper for applyEvent that does not implement the post-transition action
  175. *
  176. * @see applyEvent()
  177. *
  178. * @param string $eventName
  179. *
  180. * @return Result
  181. */
  182. private function applySimpleEvent($eventName) {
  183. // echo "Start of " . func_name() . ", event = $eventName\n";
  184. $currentState = $this->fState;
  185. if (($eventName == self::IDLE_EVENT) && !$this->idleWork) {
  186. return new Result();
  187. }
  188. if (!$this->isEventAllowed($eventName)) {
  189. die("Event $eventName not allowed in current state $currentState.\n");
  190. /* throw new Exception(func_name()
  191. . ": Event \"$eventName\" not accepted in current state \"$currentState\"");
  192. */
  193. }
  194. $outcomes = $this->getAcceptedOutcomes($eventName);
  195. $methodName = "event$eventName";
  196. if (!method_exists($this, $methodName)) {
  197. die (func_name() . ": missing method "
  198. . get_class($this) . "::$methodName. Aborting.\n");
  199. }
  200. $outcome = $this->$methodName();
  201. if (!in_array($outcome, $outcomes)) {
  202. throw new \Exception(func_name()
  203. . ": event guard. Transition on \"$eventName\" return invalid outcome: "
  204. . print_r($outcome, true)
  205. . " for state $this->fState\n");
  206. }
  207. $transition = &$this->fTransitions[$currentState][$eventName][$outcome];
  208. $result = new Result (
  209. $outcome,
  210. $transition[0],
  211. $transition[1]
  212. );
  213. if (isset($result->fsmState)) {
  214. $this->fState = $result->fsmState;
  215. }
  216. /* print_r($this->fTransitions[$currentState][$eventName]);
  217. var_dump($result); */
  218. if (!isset($outcome)) {
  219. $outcome = 'NULL';
  220. }
  221. /*
  222. echo func_name()
  223. . ": $currentState: " . $eventName . '[' . $outcome . ']'
  224. . " -> $this->fState / " . $result->fsmAction . PHP_EOL;
  225. */
  226. return $result;
  227. }
  228. /**
  229. * Default event.
  230. *
  231. * @return boolean
  232. */
  233. public function fIdle() {
  234. return TRUE;
  235. }
  236. /**
  237. * Apply an fsm::IDLE_EVENT event. Do not confuse with fIdle !
  238. *
  239. * @return Result
  240. */
  241. public function idle() {
  242. return $this->applyEvent(self::IDLE_EVENT);
  243. }
  244. /**
  245. * return the current operating mode of the FSM
  246. * @return int
  247. */
  248. public function getEventMode() {
  249. return $this->fEventMode;
  250. }
  251. /**
  252. * set the current operating mode for the FSM
  253. *
  254. * @param int $mode fsm::EVENT_* constants
  255. * @return int $mode fsm::EVENT_* constants
  256. */
  257. public function setEventMode($mode) {
  258. switch ($mode) {
  259. case self::EVENT_NORMAL :
  260. if (count($this->fQueue) > 0) {
  261. while (($event = array_shift($this->fQueue)) !== NULL) {
  262. $this->applyEvent($event);
  263. }
  264. }
  265. break;
  266. // Nothing special to do
  267. case self::EVENT_QUEUE:
  268. break;
  269. // Empty queue if needed
  270. case self::EVENT_SINK :
  271. if (count($this->fQueue) > 0) {
  272. $this->fQueue = array();
  273. }
  274. break;
  275. default:
  276. throw new Exception("Trying to set unknown FSM mode $mode");
  277. }
  278. $this->fEventMode = $mode;
  279. return $this->fEventMode;
  280. }
  281. /**
  282. * Load the fTransitions table from an external resource.
  283. *
  284. * @param string $url
  285. * Optional: defaults to the name of the class, . ".xml"
  286. */
  287. public function loadFsm($url = NULL) {
  288. $osd = FALSE; // on screen display (debug)
  289. if ($osd) {
  290. echo "Loading FSM from $url\n";
  291. }
  292. if (!isset($url)) {
  293. $url = get_class($this) . ".xml";
  294. }
  295. $fsm = simplexml_load_file($url);
  296. $fsmVersion = (string) $fsm['fsm_version'];
  297. if ($fsmVersion !== '1.3') {
  298. die("Revision $fsmVersion of schema is not supported.\n");
  299. }
  300. $this->idleWork = ($fsm['idle_work'] == 1);
  301. $this->allowActions = ($fsm['allow_actions'] == 1);
  302. $this->fTransitions = array();
  303. $t = &$this->fTransitions;
  304. // (string) casts in this loop are required: RHS is a SimpleXMLElement
  305. foreach ($fsm->state as $state) {
  306. $id = (string) $state['id'];
  307. if ($osd) {
  308. echo "State $id :\n";
  309. }
  310. $t[$id] = array();
  311. switch ($id) {
  312. case self::INIT_STATE :
  313. if ($osd) {
  314. echo " Initial state\n";
  315. }
  316. break;
  317. case self::FINAL_STATE :
  318. if ($osd) {
  319. echo " Final state\n";
  320. }
  321. break;
  322. }
  323. foreach ($state->event as $event) {
  324. $name = (string) $event['name'];
  325. if ($osd) {
  326. echo " Event $name";
  327. }
  328. if (!isset($event['type'])) {
  329. $event['type'] = 'void';
  330. }
  331. $eventType = (string) $event['type'];
  332. if ($osd) {
  333. echo ", type $eventType\n";
  334. }
  335. foreach ($event as $next) {
  336. if ($event['type'] == 'void') {
  337. $next['result'] = 'always';
  338. $result = null;
  339. }
  340. else {
  341. $result = (string) $next['result'];
  342. }
  343. if (!isset($next['state'])) {
  344. $next['state'] = (string) $state['id'];
  345. }
  346. if ($osd) {
  347. echo " Next(" . $next['result'] . ') = ' . $next['state'];
  348. if (isset($next['action'])) {
  349. echo " + event " . $next['action'];
  350. }
  351. echo PHP_EOL;
  352. }
  353. $t[$id][$name][$result] = array(
  354. (string) $next['state'],
  355. (string) $next['action']);
  356. }
  357. }
  358. }
  359. }
  360. }