u_fsm.php 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  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: u_fsm.php,v 1.3 2007-04-29 15:40:04 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 name of the state to which the FSM must change. If NULL, do not change
  20. * the current state.
  21. *
  22. * @var string
  23. */
  24. public $fsm_state;
  25. /**
  26. * The name of an event to be fired after the state change has been applied
  27. *
  28. * @var string
  29. */
  30. public $fsm_action;
  31. /**
  32. * @param string $state
  33. * @param string $action
  34. * @return void
  35. */
  36. public function __construct($state = NULL, $action = NULL)
  37. {
  38. $this->fsm_state = $state;
  39. $this->fsm_action = $action;
  40. }
  41. }
  42. /**
  43. * This class must be inherited by code implementing actual FSMs.
  44. *
  45. * Applications must create a fsm descendant in which they will:
  46. *
  47. * - define the f_transitions table, usually in their constructor
  48. * - create "f_foo" functions for each "foo" event, except "idle", which is builtin.
  49. *
  50. * Applications may:
  51. * - disable event processing by setting $this->event to one of the fsm::EVENT_* constants
  52. * - disable post-event actions by setting $this->allows actions to false
  53. * - disable the builtin idle event by setting $this->idle_work to false
  54. * - query the current state by using $this->get_state()
  55. * - send an idle event by using $this->idle()
  56. * - submit any event (including idle) by using $this->apply_event($event_name)
  57. *
  58. */
  59. abstract class fsm
  60. {
  61. const IDLE_EVENT = 'idle'; // must not change name: method f_idle depends on it
  62. const EVENT_NORMAL = 1; // processes events
  63. const EVENT_QUEUE = 2; // queue events for later use
  64. const EVENT_SINK = 3; // throw away events
  65. public $idle_work = TRUE;
  66. public $allow_actions = TRUE;
  67. protected $f_event_mode = fsm::EVENT_NORMAL;
  68. protected $f_queue = array(); // event queue for EVENT_QUEUE mode
  69. /**
  70. * the current state of the object
  71. * @var string
  72. */
  73. protected $f_state;
  74. /**
  75. * Transitions holds the transitions table
  76. * state1
  77. * event1
  78. * result1
  79. * state_name|fsm_result
  80. * event2
  81. * ...
  82. * ..
  83. * ..
  84. * @var array
  85. */
  86. protected $f_transitions = null;
  87. /**
  88. * constructor initializes the FSM to the first
  89. * state in the transitions table
  90. * @return void
  91. */
  92. public function __construct()
  93. {
  94. $this->_check_transitions();
  95. reset($this->f_transitions);
  96. $x = each($this->f_transitions);
  97. $x = $x[0];
  98. $this->f_state = $x;
  99. }
  100. /**
  101. * make sure a transitions graph has been defined
  102. * @return void
  103. */
  104. private function _check_transitions()
  105. {
  106. if (!isset($this->f_transitions))
  107. throw new Exception('No FSM processing is allowed without a transitions table');
  108. }
  109. /**
  110. * getter for f_state
  111. */
  112. public function get_state()
  113. {
  114. return $this->f_state;
  115. }
  116. /**
  117. * return the list of events accepted in the current state
  118. * @return array
  119. */
  120. public function get_accepted_events()
  121. {
  122. $this->_check_transitions();
  123. $events = array_keys($this->f_transitions[$this->f_state]);
  124. // echo func_name() . ": " . print_r($events, true). "\n";
  125. return $events;
  126. }
  127. /**
  128. * return the list of outcome accepted in the current state for the give event
  129. * @param string $event_name
  130. * @param mixed $outcome
  131. * @return array
  132. */
  133. public function get_accepted_outcomes($event_name)
  134. {
  135. // echo func_name() . "\n";
  136. $this->_check_transitions();
  137. if (!$this->is_event_allowed($event_name))
  138. throw new Exception(func_name() . ": event \"$event_name\" not allowed in state \"$this->f_state\"\n");
  139. $outcomes = array_keys($this->f_transitions[$this->f_state][$event_name]);
  140. //echo "outcomes for event $event_name: " . print_r($outcomes, true) . "\n";
  141. return $outcomes;
  142. }
  143. /**
  144. * is this event accepted in the current state
  145. * the FSM is in ?
  146. *
  147. * @param string $event_name
  148. * @return boolean
  149. */
  150. public function is_event_allowed($event_name)
  151. {
  152. // echo func_name() . "($event_name)";
  153. $this->_check_transitions();
  154. $ret = in_array($event_name, $this->get_accepted_events());
  155. // echo ", result = <$ret>\n";
  156. return $ret;
  157. }
  158. /**
  159. * is a given outcome available for a given event,
  160. * considering the current context ?
  161. *
  162. * @param string $event_name
  163. * @param mixed $outcome
  164. * @return boolean
  165. */
  166. public function is_outcome_allowed($event_name, $outcome)
  167. {
  168. $this->_check_transitions();
  169. if (!$this->is_event_allowed($event_name))
  170. return false;
  171. $ret = array_key_exists($outcome, $this->get_accepted_outcomes($event_name));
  172. return $ret;
  173. }
  174. /**
  175. * apply an event, and the resulting event chain if needed
  176. *
  177. * @param string $event_name
  178. * @param array $params the
  179. * @return string resulting state
  180. */
  181. public function apply_event($event_name)
  182. {
  183. //echo func_name() . "\n";
  184. do {
  185. $result = $this->apply_simple_event($event_name);
  186. if ($this->allow_actions)
  187. {
  188. $event_name = $result->fsm_action; // can be NULL
  189. }
  190. else
  191. {
  192. $event_name = NULL;
  193. }
  194. } while($event_name);
  195. return $result;
  196. }
  197. /**
  198. * Helper for apply_event that does not implement the post-transition action
  199. *
  200. * @param string $event_name
  201. * @return fsm_result
  202. * @see apply_event()
  203. */
  204. private function apply_simple_event($event_name)
  205. {
  206. //echo func_name() . "\n";
  207. if (($event_name == fsm::IDLE_EVENT) && !$this->idle_work)
  208. {
  209. return new fsm_result();
  210. }
  211. if (! $this->is_event_allowed($event_name))
  212. throw new Exception(func_name()
  213. . ": Event \"$event_name\" not accepted in current state \"$this->f_state\"");
  214. $method_name = "f_$event_name";
  215. $outcomes = $this->get_accepted_outcomes($event_name);
  216. $result = $this->$method_name();
  217. if (!is_object($result))
  218. {
  219. $result = new fsm_result($result, NULL);
  220. }
  221. if (!in_array($result->fsm_state, $outcomes))
  222. throw new Exception(func_name()
  223. . ": event guard. Transition on \"$event_name\" return invalid result: "
  224. . var_dump($result)
  225. . "\n");
  226. $this->f_state = $this->f_transitions[$this->f_state][$event_name][$result->fsm_state];
  227. // echo func_name() . ", new state: $this->f_state, action: $result->fsm_action\n";
  228. return $result;
  229. }
  230. /**
  231. * Default event
  232. * @return boolean
  233. */
  234. public function f_idle()
  235. {
  236. return TRUE;
  237. }
  238. /**
  239. * Apply an fsm::IDLE_EVENT event. Do not confuse with f_idle !
  240. *
  241. * @return fsm_result
  242. */
  243. public function idle()
  244. {
  245. return $this->apply_event(fsm::IDLE_EVENT);
  246. }
  247. /**
  248. * return the current operating mode of the FSM
  249. * @return int
  250. */
  251. public function get_event_mode()
  252. {
  253. return $this->f_event_mode;
  254. }
  255. /**
  256. * set the current operating mode for the FSM
  257. *
  258. * @param int $mode fsm::EVENT_* constants
  259. */
  260. public function set_event_mode($mode)
  261. {
  262. switch ($mode)
  263. {
  264. case fsm::EVENT_NORMAL :
  265. if (count($this->f_queue) > 0)
  266. {
  267. while ($event = array_shift($this->f_queue))
  268. {
  269. $this->apply_event($event);
  270. }
  271. }
  272. break;
  273. case fsm::EVENT_QUEUE : // nothing special to do
  274. break;
  275. case fsm::EVENT_SINK : // empty queue if needed
  276. if (count($this->f_queue) > 0)
  277. {
  278. $this->f_queue = array();
  279. }
  280. break;
  281. default:
  282. throw new Exception("Trying to set unknown FSM mode $mode");
  283. }
  284. $this->f_event_mode = $mode;
  285. return $this->f_event_mode;
  286. }
  287. }