Completed
Push — master ( a1bc75...7865af )
by Sébastien
11:42 queued 11s
created

Visualize::handle()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 26
rs 9.504
c 0
b 0
f 0
cc 4
nc 5
nop 0
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=/usr/local/bin/dot}';
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();
0 ignored issues
show
Documentation Bug introduced by
The method askForGraph does not exist on object<Sebdesign\SM\Commands\Visualize>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
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
    protected function stateMachineInDotFormat(array $config)
78
    {
79
        // Output image mime types.
80
        $mimeTypes = [
81
            'png' => 'image/png',
82
            'jpg' => 'image/jpeg',
83
            'gif' => 'image/gif',
84
            'svg' => 'image/svg+xml',
85
        ];
86
87
        $format = $this->option('format');
88
89
        if (empty($mimeTypes[$format])) {
90
            throw new \Exception(sprintf("Format '%s' is not supported", $format));
91
        }
92
93
        $dotPath = $this->option('dot-path');
94
        $outputImage = $this->option('output');
95
96
        $process = new Process([$dotPath, '-T', $format, '-o', $outputImage]);
97
        $process->setInput($this->buildDotFile($config));
98
        $process->run();
99
100
        // executes after the command finishes
101
        if (! $process->isSuccessful()) {
102
            throw new ProcessFailedException($process);
103
        }
104
    }
105
106
    protected function buildDotFile(array $config): string
107
    {
108
        // Display settings
109
        $layout = $this->option('direction') === 'TB' ? 'TB' : 'LR';
110
        $nodeShape = $this->option('shape');
111
112
        // Build dot file content.
113
        $result = [];
114
        $result[] = 'digraph finite_state_machine {';
115
        $result[] = "rankdir={$layout};";
116
        $result[] = 'node [shape = point]; _start_'; // Input node
117
118
        // Use first value from 'states' as start.
119
        $start = $config['states'][0]['name'];
120
        $result[] = "node [shape = {$nodeShape}];"; // Default nodes
121
        $result[] = "_start_ -> {$start};"; // Input node -> starting node.
122
123
        foreach ($config['transitions'] as $name => $transition) {
124
            foreach ($transition['from'] as $from) {
125
                $result[] = "{$from} -> {$transition['to']}[label = \"{$name}\"];";
126
            }
127
        }
128
129
        $result[] = '}';
130
131
        return implode(PHP_EOL, $result);
132
    }
133
}
134