PageRenderTime 63ms CodeModel.GetById 35ms RepoModel.GetById 0ms app.codeStats 0ms

/FSM/GraphViz.php

https://github.com/pear/FSM
PHP | 331 lines | 146 code | 29 blank | 156 comment | 31 complexity | 3780af3bb51a6722fcd2682af612e0d4 MD5 | raw file
  1. <?php
  2. /* vim: set expandtab softtabstop=4 tabstop=4 shiftwidth=4: */
  3. /**
  4. * Copyright (c) 2007-2008 Philippe Jausions / 11abacus
  5. *
  6. * Permission is hereby granted, free of charge, to any person obtaining a copy
  7. * of this software and associated documentation files (the "Software"), to
  8. * deal in the Software without restriction, including without limitation the
  9. * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
  10. * sell copies of the Software, and to permit persons to whom the Software is
  11. * furnished to do so, subject to the following conditions:
  12. *
  13. * The above copyright notice and this permission notice shall be included in
  14. * all copies or substantial portions of the Software.
  15. *
  16. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  21. * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
  22. * IN THE SOFTWARE.
  23. *
  24. * @package FSM
  25. * @author Philippe Jausions <jausions@php.net>
  26. * @copyright 2007 Philippe Jausions / 11abacus
  27. * @license http://www.11abacus.com/license/NewBSD.php New BSD License
  28. * @version CVS: $Id$
  29. */
  30. /**
  31. * Requires PEAR packages
  32. */
  33. require_once 'FSM.php';
  34. require_once 'Image/GraphViz.php';
  35. /**
  36. * FSM to Image_GraphViz converter
  37. *
  38. * This class extends the FSM class to have access to private properties.
  39. * It is not intended to be used as a FSM instance.
  40. *
  41. * PHP 5 or later is recommended to be able to handle action that return a new
  42. * state.
  43. *
  44. * @package FSM
  45. * @author Philippe Jausions <jausions@php.net>
  46. * @copyright (c) 2007 by Philippe Jausions / 11abacus
  47. * @since 1.3.0
  48. */
  49. class FSM_GraphViz extends FSM
  50. {
  51. /**
  52. * Machine instance
  53. *
  54. * @var FSM
  55. * @access protected
  56. */
  57. var $_fsm;
  58. /**
  59. * Action name callback
  60. *
  61. * @var string
  62. * @access protected
  63. */
  64. var $_actionNameCallback;
  65. /**
  66. * Constructor
  67. *
  68. * @param FSM &$machine instance to convert
  69. *
  70. * @access public
  71. */
  72. function FSM_GraphViz(&$machine)
  73. {
  74. $this->_fsm =& $machine;
  75. $this->_actionNameCallback = array(&$this, '_getActionName');
  76. }
  77. /**
  78. * Sets the callback for the action name
  79. *
  80. * @param mixed $callback
  81. *
  82. * @return boolean TRUE on success, PEAR_Error on error
  83. * @access public
  84. */
  85. function setActionNameCallback($callback)
  86. {
  87. if (!is_callable($callback)) {
  88. return PEAR::raiseError('Not a valid callback');
  89. }
  90. $this->_actionNameCallback = $callback;
  91. return true;
  92. }
  93. /**
  94. * Converts an FSM to an instance of Image_GraphViz
  95. *
  96. * @param string $name Name for the graph
  97. * @param boolean $strict Whether to collapse multiple edges between
  98. * same nodes.
  99. *
  100. * @return Image_GraphViz instance or PEAR_Error on failure
  101. * @access public
  102. */
  103. function &export($name = 'FSM', $strict = true)
  104. {
  105. if (!is_a($this->_fsm, 'FSM')) {
  106. $error = PEAR::raiseError('Not a FSM instance');
  107. return $error;
  108. }
  109. $g = new Image_GraphViz(true, null, $name, $strict);
  110. // Initial state
  111. $attr = array('shape' => 'invhouse');
  112. $g->addNode($this->_fsm->_initialState, $attr);
  113. $nodes = array($this->_fsm->_initialState => $this->_fsm->_initialState);
  114. $_t = '_transitions';
  115. do {
  116. foreach ($this->_fsm->$_t as $input => $t) {
  117. if ($_t == '_transitions') {
  118. list($symbol, $state) = explode(',', $input, 2);
  119. } else {
  120. $state = $input;
  121. $symbol = '';
  122. }
  123. list($nextState, $action) = $t;
  124. if (!array_key_exists($nextState, $nodes)) {
  125. $g->addNode($nextState);
  126. $nodes[$nextState] = $nextState;
  127. }
  128. if (!array_key_exists($state, $nodes)) {
  129. $g->addNode($state);
  130. $nodes[$state] = $state;
  131. }
  132. if (strlen($symbol)) {
  133. $g->addEdge(array($state => $nextState),
  134. array('label' => $symbol));
  135. } else {
  136. $g->addEdge(array($state => $nextState));
  137. }
  138. $this->_addAction($g, $nodes, $action, $nextState);
  139. }
  140. if ($_t == '_transitions') {
  141. $_t = '_transitionsAny';
  142. } else {
  143. $_t = false;
  144. }
  145. } while ($_t);
  146. // Add default transition
  147. if ($this->_defaultTransition) {
  148. list($nextState, $action) = $this->_defaultTransition;
  149. if (!array_key_exists($nextState, $nodes)) {
  150. $g->addNode($nextState, array('style' => 'dotted'));
  151. $nodes[$nextState] = $nextState;
  152. }
  153. $this->_addAction($g, $nodes, $action, $nextState, true);
  154. }
  155. return $g;
  156. }
  157. /**
  158. * Adds an action into the graph
  159. *
  160. * @param Image_GraphViz &$graph instance to add the action to
  161. * @param array &$nodes list of nodes
  162. * @param mixed $action callback
  163. * @param string $state start state
  164. * @param boolean $default whether this is the action tied to the default
  165. * transition
  166. *
  167. * @return void
  168. * @access protected
  169. */
  170. function _addAction(&$graph, &$nodes, $action, $state, $default = false)
  171. {
  172. $actionName = call_user_func($this->_actionNameCallback, $action);
  173. if (strlen($actionName)) {
  174. $attr = array();
  175. if ($default) {
  176. $attr['style'] = 'dotted';
  177. }
  178. if (!array_key_exists($actionName, $nodes)) {
  179. $graph->addNode($actionName,
  180. array_merge($attr, array('shape' => 'box')));
  181. $nodes[$actionName] = $actionName;
  182. }
  183. $graph->addEdge(array($state => $actionName), $attr);
  184. // Any new states out of action?
  185. $states = $this->_getStatesReturnedByAction($action);
  186. foreach ($states as $state) {
  187. if (!array_key_exists($state, $nodes)) {
  188. $graph->addNode($state, $attr);
  189. $nodes[$state] = $state;
  190. }
  191. $graph->addEdge(array($actionName => $state), $attr);
  192. }
  193. }
  194. }
  195. /**
  196. * Returns an symbol-node name
  197. *
  198. * @param string $symbol
  199. * @param string $state
  200. *
  201. * @return string
  202. * @access protected
  203. */
  204. function _getSymbolName($symbol, $state)
  205. {
  206. return $symbol.', '.$state;
  207. }
  208. /**
  209. * Returns an action as string
  210. *
  211. * @param mixed $callback action
  212. *
  213. * @return string
  214. * @access protected
  215. */
  216. function _getActionName($callback)
  217. {
  218. if (!is_callable($callback)) {
  219. return null;
  220. }
  221. if (!is_array($callback)) {
  222. return $callback.'()';
  223. }
  224. if (is_object($callback[0])) {
  225. return get_class($callback[0]).'::'.$callback[1].'()';
  226. }
  227. return $callback[0].'::'.$callback[1].'()';
  228. }
  229. /**
  230. * Analyzes callback for possible new state(s) returned
  231. *
  232. * PHP version 5
  233. *
  234. * This methods requires the use of the Reflection API to parse the
  235. * doc block and looks for the @return declaration.
  236. *
  237. * If the callback shall return new states, @return should specify a string
  238. * followed by a <ul></ul> containing a list of new states inside <li></li>
  239. * tags.
  240. *
  241. * Example of doc block for action callback:
  242. * <code>
  243. * /**
  244. * * This method does something then returns a new status
  245. * *
  246. * * \@param string $symbol
  247. * * \@param mixed $payload
  248. * *
  249. * * \@return string One of
  250. * * <ul>
  251. * * <li>RIPE</li>
  252. * * <li>NOT_RIPE</li>
  253. * * <ul>
  254. * * \@access public
  255. * {@*}
  256. * function checkRipeness($symbol, $payload)
  257. * {
  258. * return ($symbol == 'Orange') ? 'RIPE' : 'NOT_RIPE';
  259. * }
  260. * </code>
  261. *
  262. * @param mixed $callback callback to analyze
  263. *
  264. * @return array a list of possible new states returned by the callback
  265. * @access protected
  266. */
  267. function _getStatesReturnedByAction($callback)
  268. {
  269. if (version_compare(PHP_VERSION, '5.1.0') < 0
  270. || !is_callable($callback)) {
  271. return array();
  272. }
  273. if (!is_array($callback)) {
  274. strstr($callback, '::')
  275. ? $reflector = new ReflectionMethod($callback)
  276. : $reflector = new ReflectionFunction($callback);
  277. } else {
  278. $reflector = new ReflectionMethod($callback[0], $callback[1]);
  279. }
  280. $doc = $reflector->getDocComment();
  281. // We're only interested in the docBlock from @return and on
  282. $returnPos = strpos($doc, '* @return');
  283. if ($returnPos === false) {
  284. return array();
  285. }
  286. $returnDoc = trim(substr($doc, $returnPos + 9));
  287. // Returning a string (i.e. new state name)?
  288. if (strncasecmp($returnDoc, 'string', 6) != 0) {
  289. return array();
  290. }
  291. // Get the list of possible new states
  292. $length = strpos($returnDoc, '* @');
  293. if (!$length) {
  294. $length = strlen($returnDoc);
  295. }
  296. $listDoc = substr($returnDoc, 0, $length);
  297. if (!preg_match_all('~<li>([^\s<]+?).*</li>~Uis', $listDoc, $list)) {
  298. return array();
  299. }
  300. return $list[1];
  301. }
  302. }