Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature visualisation #17

Open
wants to merge 23 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,40 @@ $sm->can('t34');

```


### Rendering State Machines as Graphs

You can render a state machine using Graphviz (see examples/rendered-graph.png).

To obtain the source code of a directed graph in dot simply initialize the graphviz renderer:

```php
use Finite\StateMachine\StateMachine;
use Finite\Visualisation\Graphviz;
use Finite\State\StateInterface;

$stateMachine = new StateMachine();
$renderer = new Graphviz();
$dotSource = $renderer->render($stateMachine);

```

To change how nodes are rendered, pass a configuration. The configuration accepts two parameters which control the generated output:

* An optional flag controlling whether the state properties shall be rendered within the label.
* The optional fill color (a dot color name or e.g. hex code) of init and end states.

```php
use Finite\StateMachine\StateMachine;
use Finite\Visualisation\Configuration;
use Finite\Visualisation\Graphviz;
use Finite\State\StateInterface;

$stateMachine = new StateMachine();
$config = new Configuration(true, 'red');
$renderer = new Graphviz($config);
$dotSource = $renderer->render($stateMachine);

```


3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
"symfony/event-dispatcher": "*",
"symfony/framework-bundle": "*",
"symfony/security": "*",
"twig/twig": "*"
"twig/twig": "*",
"alom/graphviz": "v1.0.0"
},
"suggest": {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should require dot if that is an executable no?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've thought about that, too, Imho "suggesting" it is better, since you're not forced to install dot if you don't need it. alom/graphviz doesn't require it, too.

"pimple/pimple": "Needed for use with PimpleFactory",
Expand Down
67 changes: 67 additions & 0 deletions examples/rendered-graph.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

require_once __DIR__ . '/../vendor/autoload.php';

use Finite\Loader\ArrayLoader;
use Finite\State\StateInterface;
use Finite\StatefulInterface;
use Finite\StateMachine\StateMachine;
use Finite\Visualisation\Configuration;
use Finite\Visualisation\Graphviz;

/**
* Example implementation
*/
class Document implements StatefulInterface
{
private $state;

public function getFiniteState()
{
return $this->state;
}

public function setFiniteState($state)
{
$this->state = $state;
}
}

// Configure your graph
$loader = new ArrayLoader([
'class' => 'Document',
'states' => [
'draft' => [
'type' => StateInterface::TYPE_INITIAL,
'properties' => ['deletable' => true, 'editable' => true],
],
'proposed' => [
'type' => StateInterface::TYPE_NORMAL,
'properties' => [],
],
'accepted' => [
'type' => StateInterface::TYPE_NORMAL,
'properties' => ['printable' => true],
],
'published' => [
'type' => StateInterface::TYPE_FINAL,
'properties' => ['printable' => true],
]
],
'transitions' => [
'propose' => ['from' => ['draft'], 'to' => 'proposed'],
'accept' => ['from' => ['proposed'], 'to' => 'accepted'],
'reject' => ['from' => ['proposed'], 'to' => 'draft'],
'publish' => ['from' => ['accepted'], 'to' => 'published'],
'cheat' => ['from' => ['draft'], 'to' => 'published'],
],
]);

$document = new Document;
$stateMachine = new StateMachine($document);
$loader->load($stateMachine);
$stateMachine->initialize();

$config = new Configuration(__DIR__ . '/rendered-graph.png', true, 'red');
$renderer = new Graphviz($config);
$renderer->render($stateMachine);
Binary file added examples/rendered-graph.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
47 changes: 47 additions & 0 deletions src/Finite/Visualisation/Configuration.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

namespace Finite\Visualisation;

/**
* Configuration value object.
*
* @author Daniel Pozzi <bonndan76@googlemail.com>
*/
class Configuration
{
private $renderProperties;
private $markCurrentState;

/**
* Constructor.
*
* @param boolean $renderProperties flag
* @param string|null $markCurrentState fillcolor
*/
public function __construct($renderProperties = false, $markCurrentState = null)
{
$this->renderProperties = (bool) $renderProperties;
$this->markCurrentState = $markCurrentState;
}

/**
* Returns whether state properties shall be rendered or not.
*
* @return boolean
*/
public function renderProperties()
{
return $this->renderProperties;
}

/**
* Returns in which color the current state shall be rendered (fillcolor) or null.
*
* @return string|null
*/
public function markCurrentState()
{
return $this->markCurrentState;
}

}
12 changes: 12 additions & 0 deletions src/Finite/Visualisation/Exception.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php
namespace Finite\Visualisation;

/**
* Visualisation exception.
*
* @author Daniel Pozzi <bonndan76@googlemail.com>
*/
class Exception extends \Finite\Exception\Exception
{
const CODE_DOT_ERROR = 1;
}
141 changes: 141 additions & 0 deletions src/Finite/Visualisation/Graphviz.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
<?php

namespace Finite\Visualisation;

use Finite\StateMachine\StateMachineInterface;
use Finite\State\StateInterface;
use Alom\Graphviz\Digraph;

/**
* Visualisation of a State machine using Graphviz
*

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove line break

* This class geneates dot source code which can be rendered
* by graphviz. Pass a configuration object to control how
* the nodes are rendered.
*
* @link http://www.graphviz.org/Gallery/directed/fsm.gv.txt
* @author Daniel Pozzi <bonndan76@googlemail.com>
*/
class Graphviz implements VisualisationInterface
{
/**
* the graphviz graph representation
*
* @var \Alom\Graphviz\Digraph

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove first \

*/
private $graph;

/**
* visualisation options
*
* @var \Finite\Visualisation\Configuration
*/
private $configuration;

/**
* Constructor.
*
* @param \Finite\Visualisation\Configuration $config
*/
public function __construct(Configuration $config = null)
{
if (null === $config) {
$config = new Configuration();
}
$this->configuration = $config;
}

/**
* Renders the state machine.
*
* @param \Finite\StateMachine\StateMachineInterface $stateMachine
* @param string $target
* @throws Exception
*/
public function render(StateMachineInterface $stateMachine)
{
$this->graph = new Digraph('state_machine');
$this->addNodes($stateMachine);
$this->addEdges($stateMachine);
$this->graph->end();

return $this->graph->render();
}

/**
* Adds the states as nodes.
*
* @param \Finite\StateMachine\StateMachineInterface $stateMachine
*/
private function addNodes(StateMachineInterface $stateMachine)
{
$states = $stateMachine->getStates();
foreach ($states as $name) {
$this->graph->beginNode($name, $this->getNodeAttributes($stateMachine, $name))->end();
}
}

/**
* Returns the node attributes.
*
* @param \Finite\StateMachine\StateMachineInterface $stateMachine
* @param string $name
* @return array
*/
private function getNodeAttributes(StateMachineInterface $stateMachine, $name)
{
$state = $stateMachine->getState($name); /* @var $state \Finite\State\StateInterface */
$data = array(
'shape' => $state->getType() != StateInterface::TYPE_NORMAL ? 'doublecircle' : 'circle',
'label' => $this->getNodeLabel($state),
);
if ($stateMachine->getCurrentState() == $state && $this->configuration->markCurrentState()) {
$data['fillcolor'] = $this->configuration->markCurrentState();
$data['style'] = 'filled';
}

return $data;
}

/**
* Returns the node label.
*
* @param \Finite\State\StateInterface $state
* @return string
*/
private function getNodeLabel(StateInterface $state)
{
$id = $state->getName();
$props = $state->getProperties();
if (count($props) > 0 && $this->configuration->renderProperties()) {
foreach (array_keys($props) as $prop) {
$id .= "\\n* " . $prop;
}
}

return $id;
}

/**
* Adds all transitions as edges.
*
* @param \Finite\StateMachine\StateMachineInterface $stateMachine
*/
private function addEdges(StateMachineInterface $stateMachine)
{
$states = $stateMachine->getStates();
foreach ($states as $name) {
$state = $stateMachine->getState($name);
/* @var $state \Finite\State\StateInterface */
$transitions = $state->getTransitions();
foreach ($transitions as $name) {
$trans = $stateMachine->getTransition($name);
/* @var $trans Finite\Transition\TransitionInterface */
$this->graph->beginEdge(
array($state->getName(), $trans->getState()), array('label' => $trans->getName()))
->end();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Weird indent

}
}
}

}
15 changes: 15 additions & 0 deletions src/Finite/Visualisation/VisualisationInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace Finite\Visualisation;

use Finite\StateMachine\StateMachineInterface;

/**
* Interface for state machine visualisation strategies.
*
* @author Daniel Pozzi <bonndan76@googlemail.com>
*/
interface VisualisationInterface
{
public function render(StateMachineInterface $stateMachine);
}
Loading