Visualize::handle()   A
last analyzed

Complexity

Conditions 4
Paths 5

Size

Total Lines 25
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 4.9102

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 12
c 1
b 0
f 0
nc 5
nop 0
dl 0
loc 25
ccs 8
cts 13
cp 0.6153
crap 4.9102
rs 9.8666
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 15
    public function __construct(array $config)
39
    {
40 15
        parent::__construct();
41
42 15
        $this->config = $config;
43 5
    }
44
45
    /**
46
     * Execute the console command.
47
     *
48
     * @return mixed
49
     */
50 3
    public function handle()
51
    {
52 3
        if (empty($this->config)) {
53
            $this->error('There are no state machines configured.');
54
55
            return 1;
56
        }
57
58 3
        if (! $this->argument('graph')) {
59
            $this->askForGraph();
60
        }
61
62 3
        $graph = $this->argument('graph');
63
64 3
        if (! array_key_exists($graph, $this->config)) {
0 ignored issues
show
Bug introduced by
It seems like $graph can also be of type array; however, parameter $key of array_key_exists() does only seem to accept integer|string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

64
        if (! array_key_exists(/** @scrutinizer ignore-type */ $graph, $this->config)) {
Loading history...
65
            $this->error('The provided state machine graph is not configured.');
66
67
            return 1;
68
        }
69
70 3
        $config = $this->config[$graph];
71
72 3
        $this->stateMachineInDotFormat($config);
73
74 3
        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"));
0 ignored issues
show
Bug introduced by
It seems like $choice can also be of type array; however, parameter $haystack of strpos() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

88
        $choice = substr($choice, 0, strpos(/** @scrutinizer ignore-type */ $choice, "\t"));
Loading history...
Bug introduced by
It seems like $choice can also be of type array; however, parameter $string of substr() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

88
        $choice = substr(/** @scrutinizer ignore-type */ $choice, 0, strpos($choice, "\t"));
Loading history...
89
90
        $this->info('You have just selected: '.$choice);
91
92
        $this->input->setArgument('graph', $choice);
93
    }
94
95 3
    protected function stateMachineInDotFormat(array $config)
96
    {
97
        // Output image mime types.
98 2
        $mimeTypes = [
99 3
            'png' => 'image/png',
100 2
            'jpg' => 'image/jpeg',
101 2
            'gif' => 'image/gif',
102 2
            'svg' => 'image/svg+xml',
103 2
        ];
104
105 3
        $format = $this->option('format');
106
107 3
        if (empty($mimeTypes[$format])) {
108
            throw new \Exception(sprintf("Format '%s' is not supported", $format));
109
        }
110
111 3
        $dotPath = $this->option('dot-path') ?? 'dot';
112 3
        $outputImage = $this->option('output');
113
114 3
        $process = new Process([$dotPath, '-T', $format, '-o', $outputImage]);
115 3
        $process->setInput($this->buildDotFile($config));
116 3
        $process->run();
117
118
        // executes after the command finishes
119 3
        if (! $process->isSuccessful()) {
120
            throw new ProcessFailedException($process);
121
        }
122 1
    }
123
124 3
    protected function buildDotFile(array $config): string
125
    {
126
        // Display settings
127 3
        $layout = $this->option('direction') === 'TB' ? 'TB' : 'LR';
128 3
        $nodeShape = $this->option('shape');
129
130
        // Build dot file content.
131 3
        $result = [];
132 3
        $result[] = 'digraph finite_state_machine {';
133 3
        $result[] = "rankdir={$layout};";
134 3
        $result[] = 'node [shape = point]; _start_'; // Input node
135
136
        // Use first value from 'states' as start.
137 3
        if (is_array($config['states'][0])) {
138 3
            $start = $config['states'][0]['name'] ?? 'null';
139
        } else {
140
            $start = $config['states'][0] ?? 'null';
141
        }
142 3
        $result[] = "node [shape = {$nodeShape}];"; // Default nodes
143 3
        $result[] = "_start_ -> \"{$start}\";"; // Input node -> starting node.
144
145 3
        foreach ($config['transitions'] as $name => $transition) {
146 3
            foreach ($transition['from'] as $from) {
147 3
                $result[] = "\"{$from}\" -> \"{$transition['to']}\" [label = \"{$name}\"];";
148
            }
149
        }
150
151 3
        $result[] = '}';
152
153 3
        return implode(PHP_EOL, $result);
154
    }
155
}
156