* @copyright 2007 Philippe Jausions / 11abacus * @license http://www.11abacus.com/license/NewBSD.php New BSD License * @version CVS: $Id: GraphViz.php 252630 2008-02-10 22:07:09Z jon $ */ /** * Requires PEAR packages */ require_once 'FSM.php'; require_once 'Image/GraphViz.php'; /** * FSM to Image_GraphViz converter * * This class extends the FSM class to have access to private properties. * It is not intended to be used as a FSM instance. * * PHP 5 or later is recommended to be able to handle action that return a new * state. * * @package FSM * @author Philippe Jausions * @copyright (c) 2007 by Philippe Jausions / 11abacus * @since 1.3.0 */ class FSM_GraphViz extends FSM { /** * Machine instance * * @var FSM * @access protected */ var $_fsm; /** * Action name callback * * @var string * @access protected */ var $_actionNameCallback; /** * Constructor * * @param FSM &$machine instance to convert * * @access public */ function FSM_GraphViz(&$machine) { $this->_fsm =& $machine; $this->_actionNameCallback = array(&$this, '_getActionName'); } /** * Sets the callback for the action name * * @param mixed $callback * * @return boolean TRUE on success, PEAR_Error on error * @access public */ function setActionNameCallback($callback) { if (!is_callable($callback)) { return PEAR::raiseError('Not a valid callback'); } $this->_actionNameCallback = $callback; return true; } /** * Converts an FSM to an instance of Image_GraphViz * * @param string $name Name for the graph * @param boolean $strict Whether to collapse multiple edges between * same nodes. * * @return Image_GraphViz instance or PEAR_Error on failure * @access public */ function &export($name = 'FSM', $strict = true) { if (!is_a($this->_fsm, 'FSM')) { $error = PEAR::raiseError('Not a FSM instance'); return $error; } $g = new Image_GraphViz(true, null, $name, $strict); // Initial state $attr = array('shape' => 'invhouse'); $g->addNode($this->_fsm->_initialState, $attr); $nodes = array($this->_fsm->_initialState => $this->_fsm->_initialState); $_t = '_transitions'; do { foreach ($this->_fsm->$_t as $input => $t) { if ($_t == '_transitions') { list($symbol, $state) = explode(',', $input, 2); } else { $state = $input; $symbol = ''; } list($nextState, $action) = $t; if (!array_key_exists($nextState, $nodes)) { $g->addNode($nextState); $nodes[$nextState] = $nextState; } if (!array_key_exists($state, $nodes)) { $g->addNode($state); $nodes[$state] = $state; } if (strlen($symbol)) { $g->addEdge(array($state => $nextState), array('label' => $symbol)); } else { $g->addEdge(array($state => $nextState)); } $this->_addAction($g, $nodes, $action, $nextState); } if ($_t == '_transitions') { $_t = '_transitionsAny'; } else { $_t = false; } } while ($_t); // Add default transition if ($this->_defaultTransition) { list($nextState, $action) = $this->_defaultTransition; if (!array_key_exists($nextState, $nodes)) { $g->addNode($nextState, array('style' => 'dotted')); $nodes[$nextState] = $nextState; } $this->_addAction($g, $nodes, $action, $nextState, true); } return $g; } /** * Adds an action into the graph * * @param Image_GraphViz &$graph instance to add the action to * @param array &$nodes list of nodes * @param mixed $action callback * @param string $state start state * @param boolean $default whether this is the action tied to the default * transition * * @return void * @access protected */ function _addAction(&$graph, &$nodes, $action, $state, $default = false) { $actionName = call_user_func($this->_actionNameCallback, $action); if (strlen($actionName)) { $attr = array(); if ($default) { $attr['style'] = 'dotted'; } if (!array_key_exists($actionName, $nodes)) { $graph->addNode($actionName, array_merge($attr, array('shape' => 'box'))); $nodes[$actionName] = $actionName; } $graph->addEdge(array($state => $actionName), $attr); // Any new states out of action? $states = $this->_getStatesReturnedByAction($action); foreach ($states as $state) { if (!array_key_exists($state, $nodes)) { $graph->addNode($state, $attr); $nodes[$state] = $state; } $graph->addEdge(array($actionName => $state), $attr); } } } /** * Returns an symbol-node name * * @param string $symbol * @param string $state * * @return string * @access protected */ function _getSymbolName($symbol, $state) { return $symbol.', '.$state; } /** * Returns an action as string * * @param mixed $callback action * * @return string * @access protected */ function _getActionName($callback) { if (!is_callable($callback)) { return null; } if (!is_array($callback)) { return $callback.'()'; } if (is_object($callback[0])) { return get_class($callback[0]).'::'.$callback[1].'()'; } return $callback[0].'::'.$callback[1].'()'; } /** * Analyzes callback for possible new state(s) returned * * PHP version 5 * * This methods requires the use of the Reflection API to parse the * doc block and looks for the @return declaration. * * If the callback shall return new states, @return should specify a string * followed by a containing a list of new states inside
  • * tags. * * Example of doc block for action callback: * * /** * * This method does something then returns a new status * * * * \@param string $symbol * * \@param mixed $payload * * * * \@return string One of * *