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
![]() |
|||||||
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
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
![]() 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
![]() |
|||||||
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 |