Skip to content
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.idea
build
composer.lock
docs
Expand Down
1 change: 0 additions & 1 deletion .phpunit.result.cache

This file was deleted.

4 changes: 4 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ php:
- 7.3
- 7.4snapshot

+before_install:
- sudo apt-get update
- sudo apt-get -y install graphviz

before_script:
- travis_retry composer self-update
- travis_retry composer install --no-interaction --prefer-source
Expand Down
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,19 @@ $ php artisan winzou:state-machine:debug simple
+----------------------+--------------+------------------------------+---------------+
```

## Visualize command

An artisan command for generating an image of a given graph is included. It accepts the name of the graph as an argument.
It's taken from the corresponding bundle for Symfony: [https://github.com/MadMind/StateMachineVisualizationBundle](https://github.com/MadMind/StateMachineVisualizationBundle), so all credits goes to the original author.

If you want to run this command, you need to have installed **dot** - Part of graphviz package ([http://www.graphviz.org/](http://www.graphviz.org/)). In your mac, this is equal to having run ```brew install graphviz```

```bash
php artisan winzou:state-machine:visualize {graph? : A state machine graph} {--output=./graph.jpg} {--format=jpg} {--direction=TB} {--shape=circle} {--dot-path=/usr/local/bin/dot}
```

![test](https://user-images.githubusercontent.com/1104083/75524206-bcfd1a00-5a0d-11ea-9dce-aa0d61e46e75.jpg)

## Statable trait for Eloquent models

If you want to interact with the state machine directly within your models, you can install the [laravel-statable](https://github.com/iben12/laravel-statable) package by [iben12](https://github.com/iben12).
Expand Down
151 changes: 151 additions & 0 deletions src/Commands/Visualize.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
<?php

namespace Sebdesign\SM\Commands;

use Illuminate\Console\Command;
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\Process;

class Visualize extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'winzou:state-machine:visualize
{graph? : A state machine graph}
{--output=./graph.jpg}
{--format=jpg}
{--direction=TB}
{--shape=circle}
{--dot-path=}';

/**
* The console command description.
*
* @var string
*/
protected $description = 'Generates an image of the states and transitions of state machine graphs';

protected $config;

/**
* Create a new command instance.
*
* @param array $config
*/
public function __construct(array $config)
{
parent::__construct();

$this->config = $config;
}

/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
if (empty($this->config)) {
$this->error('There are no state machines configured.');

return 1;
}

if (! $this->argument('graph')) {
$this->askForGraph();
}

$graph = $this->argument('graph');

if (! array_key_exists($graph, $this->config)) {
$this->error('The provided state machine graph is not configured.');

return 1;
}

$config = $this->config[$graph];

$this->stateMachineInDotFormat($config);

return 0;
}

/**
* Ask for a graph name if one was not provided as argument.
*/
protected function askForGraph()
{
$choices = array_map(function ($name, $config) {
return $name."\t(".$config['class'].' - '.$config['graph'].')';
}, array_keys($this->config), $this->config);

$choice = $this->choice('Which state machine would you like to know about?', $choices, 0);

$choice = substr($choice, 0, strpos($choice, "\t"));

$this->info('You have just selected: '.$choice);

$this->input->setArgument('graph', $choice);
}

protected function stateMachineInDotFormat(array $config)
{
// Output image mime types.
$mimeTypes = [
'png' => 'image/png',
'jpg' => 'image/jpeg',
'gif' => 'image/gif',
'svg' => 'image/svg+xml',
];

$format = $this->option('format');

if (empty($mimeTypes[$format])) {
throw new \Exception(sprintf("Format '%s' is not supported", $format));
}

$dotPath = $this->option('dot-path') ?? 'dot';
$outputImage = $this->option('output');

$process = new Process([$dotPath, '-T', $format, '-o', $outputImage]);
$process->setInput($this->buildDotFile($config));
$process->run();

// executes after the command finishes
if (! $process->isSuccessful()) {
throw new ProcessFailedException($process);
}
}

protected function buildDotFile(array $config): string
{
// Display settings
$layout = $this->option('direction') === 'TB' ? 'TB' : 'LR';
$nodeShape = $this->option('shape');

// Build dot file content.
$result = [];
$result[] = 'digraph finite_state_machine {';
$result[] = "rankdir={$layout};";
$result[] = 'node [shape = point]; _start_'; // Input node

// Use first value from 'states' as start.
$start = $config['states'][0]['name'];
$result[] = "node [shape = {$nodeShape}];"; // Default nodes
$result[] = "_start_ -> \"{$start}\";"; // Input node -> starting node.

foreach ($config['transitions'] as $name => $transition) {
foreach ($transition['from'] as $from) {
$result[] = "\"{$from}\" -> \"{$transition['to']}\" [label = \"{$name}\"];";
}
}

$result[] = '}';

return implode(PHP_EOL, $result);
}
}
10 changes: 8 additions & 2 deletions src/ServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Sebdesign\SM\Callback\ContainerAwareCallback;
use Sebdesign\SM\Callback\ContainerAwareCallbackFactory;
use Sebdesign\SM\Commands\Debug;
use Sebdesign\SM\Commands\Visualize;
use Sebdesign\SM\Event\Dispatcher;
use Sebdesign\SM\Factory\Factory;
use SM\Callback\CallbackFactoryInterface;
Expand All @@ -32,8 +33,8 @@ public function boot()
if ($this->app->runningInConsole()) {
if ($this->app instanceof LaravelApplication) {
$this->publishes([
__DIR__.'/../config/state-machine.php' => config_path('state-machine.php'),
], 'config');
__DIR__.'/../config/state-machine.php' => config_path('state-machine.php'),
], 'config');
} elseif ($this->app instanceof LumenApplication) {
$this->app->configure('state-machine');
}
Expand Down Expand Up @@ -96,8 +97,13 @@ protected function registerCommands()
return new Debug($app->make('config')->get('state-machine', []));
});

$this->app->bind(Visualize::class, function ($app) {
return new Visualize($app->make('config')->get('state-machine', []));
});

$this->commands([
Debug::class,
Visualize::class,
]);
}

Expand Down
43 changes: 43 additions & 0 deletions tests/Commands/VisualizeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

namespace Sebdesign\SM\Test\Commands;

use Illuminate\Contracts\Console\Kernel;
use Sebdesign\SM\Test\ConsoleHelpers;
use Sebdesign\SM\Test\TestCase;

class VisualizeTest extends TestCase
{
use ConsoleHelpers;

/**
* @test
*/
public function it_generates_an_image()
{
if (! `which dot`) {
$this->markTestSkipped('Dot executable not found.');
}

// Arrange

$config = $this->app['config']->get('state-machine', []);
$command = \Mockery::spy('\Sebdesign\SM\Commands\Visualize[choice]', [$config]);

$this->app[Kernel::class]->registerCommand($command);

// Act

$outputImage = tempnam(sys_get_temp_dir(), 'smv');
$this->artisan('winzou:state-machine:visualize', [
'graph' => 'graphA',
'--no-interaction' => true,
'--output' => $outputImage,
]);

// Assert
$this->withSuccessCode();

$this->assertTrue(file_exists($outputImage));
}
}