Finite_State_Machine.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456
  1. <?php
  2. /**
  3. * Finite state machine skeleton
  4. *
  5. * (c) 2006 Ouest Systèmes Informatiques (OSI)
  6. * Licensed under the CeCILL 2.0 license
  7. *
  8. * $Id: Finite_State_Machine.php,v 1.6 2007-05-08 21:47:28 marand Exp $
  9. */
  10. require_once('misc.php'); // for func_name()
  11. error_reporting(E_ALL|E_STRICT);
  12. /**
  13. * This class defines a possible outcome for a given FSM transition
  14. *
  15. */
  16. class fsm_result
  17. {
  18. /**
  19. * The return value of the event handler
  20. * @var mixed
  21. */
  22. public $fsm_return;
  23. /**
  24. * The name of the state to which the FSM must change. If NULL, do not change
  25. * the current state.
  26. *
  27. * @var string
  28. */
  29. public $fsm_state;
  30. /**
  31. * The name of an event to be fired after the state change has been applied
  32. *
  33. * @var string
  34. */
  35. public $fsm_action;
  36. /**
  37. * @param string $state
  38. * @param string $action
  39. * @return void
  40. */
  41. public function __construct($return = NULL, $state = NULL, $action = NULL)
  42. {
  43. $this->fsm_return = $return;
  44. $this->fsm_state = $state;
  45. $this->fsm_action = $action;
  46. }
  47. }
  48. /**
  49. * This class must be inherited by code implementing actual FSMs.
  50. *
  51. * Applications must create a fsm descendant in which they will:
  52. *
  53. * - define the f_transitions table, usually in their constructor
  54. * - invoke the parent constructor to set up the FSM from the transitions table
  55. * - create "f_foo" functions for each "foo" event, except "idle", which is builtin.
  56. *
  57. * Applications may:
  58. * - disable event processing by setting $this->event to one of the fsm::EVENT_* constants
  59. * - disable post-event actions by setting $this->allows actions to false
  60. * - disable the builtin idle event by setting $this->idle_work to false
  61. * - query the current state by using $this->get_state()
  62. * - send an idle event by using $this->idle()
  63. * - submit any event (including idle) by using $this->apply_event($event_name)
  64. *
  65. */
  66. abstract class Finite_State_Machine
  67. {
  68. const IDLE_EVENT = 'idle'; // must not change name: method f_idle depends on it
  69. const INIT_STATE = 'init';
  70. const FINAL_STATE = 'final';
  71. const EVENT_NORMAL = 1; // processes events
  72. const EVENT_QUEUE = 2; // queue events for later use
  73. const EVENT_SINK = 3; // throw away events
  74. public $idle_work = TRUE;
  75. public $allow_actions = TRUE;
  76. protected $f_event_mode = Finite_State_Machine::EVENT_NORMAL;
  77. protected $f_queue = array(); // event queue for EVENT_QUEUE mode
  78. /**
  79. * the current state of the object
  80. * @var string
  81. */
  82. protected $f_state;
  83. /**
  84. * Transitions holds the transitions table
  85. * state1
  86. * event1
  87. * result1
  88. * state_name|fsm_result
  89. * event2
  90. * ...
  91. * ..
  92. * ..
  93. * @var array
  94. */
  95. protected $f_transitions = null;
  96. /**
  97. * constructor initializes the FSM to the first
  98. * state in the transitions table
  99. * @return void
  100. */
  101. public function __construct()
  102. {
  103. $this->_check_transitions();
  104. reset($this->f_transitions);
  105. $x = each($this->f_transitions);
  106. $x = $x[0];
  107. $this->f_state = $x;
  108. }
  109. /**
  110. * make sure a transitions graph has been defined
  111. * @return void
  112. */
  113. private function _check_transitions()
  114. {
  115. if (!isset($this->f_transitions))
  116. throw new Exception('No FSM processing is allowed without a transitions table\n');
  117. }
  118. /**
  119. * getter for f_state
  120. */
  121. public function get_state()
  122. {
  123. return $this->f_state;
  124. }
  125. /**
  126. * return the list of events accepted in the current state
  127. * @return array
  128. */
  129. public function get_accepted_events()
  130. {
  131. $this->_check_transitions();
  132. try
  133. {
  134. $events = array_keys($this->f_transitions[$this->f_state]);
  135. // echo func_name() . ": state $this->f_state, accepted events are:\n" . print_r($events, true). "\n";
  136. }
  137. catch (Exception $e)
  138. {
  139. echo "Exception in get_accepted_events" . print_r($e);
  140. print_r(debug_backtrace());
  141. $events = array();
  142. }
  143. return $events;
  144. }
  145. /**
  146. * return the list of outcome accepted in the current state for the give event
  147. * @param string $event_name
  148. * @param mixed $outcome
  149. * @return array
  150. */
  151. public function get_accepted_outcomes($event_name)
  152. {
  153. // echo func_name() . "\n";
  154. $this->_check_transitions();
  155. /**
  156. * Spare some paranioa
  157. *
  158. if (!$this->is_event_allowed($event_name))
  159. throw new Exception(func_name() . ": event \"$event_name\" not allowed in state \"$this->f_state\"\n");
  160. */
  161. $outcomes = array_keys($this->f_transitions[$this->f_state][$event_name]);
  162. // print_r($this->f_transitions[$this->f_state][$event_name]);
  163. // echo "outcomes for event $event_name: " . var_dump($outcomes) . "\n";
  164. return $outcomes;
  165. }
  166. /**
  167. * is this event accepted in the current state
  168. * the FSM is in ?
  169. *
  170. * @param string $event_name
  171. * @return boolean
  172. */
  173. public function is_event_allowed($event_name)
  174. {
  175. // echo func_name() . "($event_name)";
  176. $this->_check_transitions();
  177. $ret = in_array($event_name, $this->get_accepted_events());
  178. // echo " in state $this->f_state, result = <$ret>\n";
  179. return $ret;
  180. }
  181. /**
  182. * is a given outcome available for a given event,
  183. * considering the current context ?
  184. *
  185. * @param string $event_name
  186. * @param mixed $outcome
  187. * @return boolean
  188. */
  189. public function is_outcome_allowed($event_name, $outcome)
  190. {
  191. $this->_check_transitions();
  192. if (!$this->is_event_allowed($event_name))
  193. return false;
  194. $ret = array_key_exists($outcome, $this->get_accepted_outcomes($event_name));
  195. return $ret;
  196. }
  197. /**
  198. * apply an event, and the resulting event chain if needed
  199. *
  200. * @param string $event_name
  201. * @param array $params the
  202. * @return string resulting state
  203. */
  204. public function apply_event($event_name)
  205. {
  206. //echo func_name() . "\n";
  207. do {
  208. $result = $this->apply_simple_event($event_name);
  209. if ($this->allow_actions)
  210. {
  211. $event_name = $result->fsm_action; // can be NULL
  212. }
  213. else
  214. {
  215. $event_name = NULL;
  216. }
  217. } while($event_name);
  218. return $result;
  219. }
  220. /**
  221. * Helper for apply_event that does not implement the post-transition action
  222. *
  223. * @param string $event_name
  224. * @return fsm_result
  225. * @see apply_event()
  226. */
  227. private function apply_simple_event($event_name)
  228. {
  229. // echo func_name() . ", event = $event_name";
  230. $current_state = $this->f_state;
  231. if (($event_name == Finite_State_Machine::IDLE_EVENT) && !$this->idle_work)
  232. {
  233. return new fsm_result();
  234. }
  235. if (! $this->is_event_allowed($event_name))
  236. throw new Exception(func_name()
  237. . ": Event \"$event_name\" not accepted in current state \"$current_state\"");
  238. $outcomes = $this->get_accepted_outcomes($event_name);
  239. $method_name = "f_$event_name";
  240. if (!method_exists($this, $method_name))
  241. {
  242. die (func_name() . ": missing method "
  243. . get_class($this) . "::$method_name. Aborting.\n");
  244. }
  245. $outcome = $this->$method_name();
  246. if (!in_array($outcome, $outcomes))
  247. {
  248. throw new Exception(func_name()
  249. . ": event guard. Transition on \"$event_name\" return invalid outcome: "
  250. . print_r($outcome, true)
  251. . " for state $this->f_state\n");
  252. }
  253. $transition = &$this->f_transitions[$current_state][$event_name][$outcome];
  254. $result = new fsm_result
  255. (
  256. $outcome,
  257. $transition[0],
  258. $transition[1]
  259. );
  260. if (isset($result->fsm_state))
  261. {
  262. $this->f_state = $result->fsm_state;
  263. }
  264. /* print_r($this->f_transitions[$current_state][$event_name]);
  265. var_dump($result); */
  266. if (!isset($outcome))
  267. $outcome = 'NULL';
  268. /* echo func_name()
  269. . ": $current_state: " . $event_name . '[' . $outcome . ']'
  270. . " -> $this->f_state / " . $result->fsm_action . PHP_EOL;
  271. */
  272. return $result;
  273. }
  274. /**
  275. * Default event
  276. * @return boolean
  277. */
  278. public function f_idle()
  279. {
  280. return TRUE;
  281. }
  282. /**
  283. * Apply an fsm::IDLE_EVENT event. Do not confuse with f_idle !
  284. *
  285. * @return fsm_result
  286. */
  287. public function idle()
  288. {
  289. return $this->apply_event(Finite_State_Machine::IDLE_EVENT);
  290. }
  291. /**
  292. * return the current operating mode of the FSM
  293. * @return int
  294. */
  295. public function get_event_mode()
  296. {
  297. return $this->f_event_mode;
  298. }
  299. /**
  300. * set the current operating mode for the FSM
  301. *
  302. * @param int $mode fsm::EVENT_* constants
  303. */
  304. public function set_event_mode($mode)
  305. {
  306. switch ($mode)
  307. {
  308. case Finite_State_Machine::EVENT_NORMAL :
  309. if (count($this->f_queue) > 0)
  310. {
  311. while ($event = array_shift($this->f_queue))
  312. {
  313. $this->apply_event($event);
  314. }
  315. }
  316. break;
  317. case Finite_State_Machine::EVENT_QUEUE : // nothing special to do
  318. break;
  319. case Finite_State_Machine::EVENT_SINK : // empty queue if needed
  320. if (count($this->f_queue) > 0)
  321. {
  322. $this->f_queue = array();
  323. }
  324. break;
  325. default:
  326. throw new Exception("Trying to set unknown FSM mode $mode");
  327. }
  328. $this->f_event_mode = $mode;
  329. return $this->f_event_mode;
  330. }
  331. /**
  332. * Load the f_transitions table from an external resource.
  333. *
  334. * @param string $url Optional: defaults to the name of the class, . ".xml"
  335. */
  336. public function load_fsm($url = NULL)
  337. {
  338. $osd = FALSE; // on screen display (debug)
  339. if ($osd)
  340. echo "Loading FSM from $url\n";
  341. if (!isset($url))
  342. {
  343. $url = get_class($this) . ".xml";
  344. }
  345. $fsm = simplexml_load_file($url);
  346. $fsm_version = (string) $fsm['fsm_version'];
  347. if ($fsm_version !== '1.3')
  348. {
  349. die("Revision $fsm_version of schema is not supported.\n");
  350. }
  351. $this->idle_work = ($fsm['idle_work'] == 1);
  352. $this->allow_actions = ($fsm['allow_actions'] == 1);
  353. $this->f_transitions = array();
  354. $t = &$this->f_transitions;
  355. // (string) casts in this loop are required: RHS is a SimpleXMLElement
  356. foreach ($fsm->state as $state)
  357. {
  358. $id = (string) $state['id'];
  359. if ($osd)
  360. echo "State $id :\n";
  361. $t[$id] = array();
  362. switch ($id)
  363. {
  364. case Finite_State_Machine::INIT_STATE :
  365. if ($osd)
  366. echo " Initial state\n";
  367. break;
  368. case Finite_State_Machine::FINAL_STATE :
  369. if ($osd)
  370. echo " Final state\n";
  371. break;
  372. }
  373. foreach ($state->event as $event)
  374. {
  375. $ename = (string) $event['name'];
  376. if ($osd)
  377. echo " Event $ename";
  378. if (!isset($event['type']))
  379. $event['type'] = 'void';
  380. $etype = (string) $event['type'];
  381. if ($osd)
  382. echo ", type $etype\n";
  383. foreach ($event as $next)
  384. {
  385. if ($event['type'] == 'void')
  386. {
  387. $next['result'] = 'always';
  388. $eresult = null;
  389. }
  390. else
  391. $eresult = (string) $next['result'];
  392. if (!isset($next['state']))
  393. $next['state'] = (string) $state['id'];
  394. if ($osd)
  395. echo " Next(" . $next['result'] . ') = ' . $next['state'];
  396. if ($osd)
  397. if (isset($next['action']))
  398. echo " + event " . $next['action'];
  399. if ($osd)
  400. echo PHP_EOL;
  401. $t[$id][$ename][$eresult] = array(
  402. (string) $next['state'],
  403. (string) $next['action']);
  404. }
  405. }
  406. }
  407. }
  408. }