Completed
Push — master ( 795a44...83526d )
by Sébastien
34:31 queued 19:33
created

Visualize::buildDotFile()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 26
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 14
c 1
b 0
f 0
nc 6
nop 1
dl 0
loc 26
rs 9.7998
1
<?php
2
3
namespace Sebdesign\SM\Commands;
4
5
use Illuminate\Console\Command;
6
use Symfony\Component\Process\Exception\ProcessFailedException;
7
use Symfony\Component\Process\Process;
8
9
class Visualize extends Command
10
{
11
    /**
12
     * The name and signature of the console command.
13
     *
14
     * @var string
15
     */
16
    protected $signature = 'winzou:state-machine:visualize
17
        {graph? : A state machine graph}
18
        {--output=./graph.jpg}
19
        {--format=jpg}
20
        {--direction=TB}
21
        {--shape=circle}
22
        {--dot-path=}';
23
24
    /**
25
     * The console command description.
26
     *
27
     * @var string
28
     */
29
    protected $description = 'Generates an image of the states and transitions of state machine graphs';
30
31
    protected $config;
32
33
    /**
34
     * Create a new command instance.
35
     *
36
     * @param array $config
37
     */
38
    public function __construct(array $config)
39
    {
40
        parent::__construct();
41
42
        $this->config = $config;
43
    }
44
45
    /**
46
     * Execute the console command.
47
     *
48
     * @return mixed
49
     */
50
    public function handle()
51
    {
52
        if (empty($this->config)) {
53
            $this->error('There are no state machines configured.');
54
55
            return 1;
56
        }
57
58
        if (! $this->argument('graph')) {
59
            $this->askForGraph();
60
        }
61
62
        $graph = $this->argument('graph');
63
64
        if (! array_key_exists($graph, $this->config)) {
65
            $this->error('The provided state machine graph is not configured.');
66
67
            return 1;
68
        }
69
70
        $config = $this->config[$graph];
71
72
        $this->stateMachineInDotFormat($config);
73
74
        return 0;
75
    }
76
77
    /**
78
     * Ask for a graph name if one was not provided as argument.
79
     */
80
    protected function askForGraph()
81
    {
82
        $choices = array_map(function ($name, $config) {
83
            return $name."\t(".$config['class'].' - '.$config['graph'].')';
84
        }, array_keys($this->config), $this->config);
85
86
        $choice = $this->choice('Which state machine would you like to know about?', $choices, 0);
87
88
        $choice = substr($choice, 0, strpos($choice, "\t"));
89
90
        $this->info('You have just selected: '.$choice);
91
92
        $this->input->setArgument('graph', $choice);
93
    }
94
95
    protected function stateMachineInDotFormat(array $config)
96
    {
97
        // Output image mime types.
98
        $mimeTypes = [
99
            'png' => 'image/png',
100
            'jpg' => 'image/jpeg',
101
            'gif' => 'image/gif',
102
            'svg' => 'image/svg+xml',
103
        ];
104
105
        $format = $this->option('format');
106
107
        if (empty($mimeTypes[$format])) {
108
            throw new \Exception(sprintf("Format '%s' is not supported", $format));
109
        }
110
111
        $dotPath = $this->option('dot-path') ?? 'dot';
112
        $outputImage = $this->option('output');
113
114
        $process = new Process([$dotPath, '-T', $format, '-o', $outputImage]);
115
        $process->setInput($this->buildDotFile($config));
116
        $process->run();
117
118
        // executes after the command finishes
119
        if (! $process->isSuccessful()) {
120
            throw new ProcessFailedException($process);
121
        }
122
    }
123
124
    protected function buildDotFile(array $config): string
125
    {
126
        // Display settings
127
        $layout = $this->option('direction') === 'TB' ? 'TB' : 'LR';
128
        $nodeShape = $this->option('shape');
129
130
        // Build dot file content.
131
        $result = [];
132
        $result[] = 'digraph finite_state_machine {';
133
        $result[] = "rankdir={$layout};";
134
        $result[] = 'node [shape = point]; _start_'; // Input node
135
136
        // Use first value from 'states' as start.
137
        $start = $config['states'][0]['name'];
138
        $result[] = "node [shape = {$nodeShape}];"; // Default nodes
139
        $result[] = "_start_ -> {$start};"; // Input node -> starting node.
140
141
        foreach ($config['transitions'] as $name => $transition) {
142
            foreach ($transition['from'] as $from) {
143
                $result[] = "{$from} -> {$transition['to']} [label = \"{$name}\"];";
144
            }
145
        }
146
147
        $result[] = '}';
148
149
        return implode(PHP_EOL, $result);
150
    }
151
}
152