Completed
Pull Request — master (#32)
by
unknown
07:51 queued 26s
created

Visualize::stateMachineInDotFormat()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 29

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 29
rs 9.456
c 0
b 0
f 0
cc 3
nc 3
nop 1
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 {graph? : A state machine graph} {--output=./graph.jpg} {--format=jpg} {--direction=TB} {--shape=circle} {--dot-path=/usr/local/bin/dot}';
17
18
    /**
19
     * The console command description.
20
     *
21
     * @var string
22
     */
23
    protected $description = 'Generates an image of the states and transitions of state machine graphs';
24
25
    protected $config;
26
27
    /**
28
     * Create a new command instance.
29
     *
30
     * @param array $config
31
     */
32
    public function __construct(array $config)
33
    {
34
        parent::__construct();
35
36
        $this->config = $config;
37
    }
38
39
    /**
40
     * Execute the console command.
41
     *
42
     * @return mixed
43
     */
44
    public function handle()
45
    {
46
        if (empty($this->config)) {
47
            $this->error('There are no state machines configured.');
48
49
            return 1;
50
        }
51
52
        if (!$this->argument('graph')) {
53
            $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...
54
        }
55
56
        $graph = $this->argument('graph');
57
58
        if (!array_key_exists($graph, $this->config)) {
59
            $this->error('The provided state machine graph is not configured.');
60
61
            return 1;
62
        }
63
64
        $config = $this->config[$graph];
65
66
        $this->stateMachineInDotFormat($config);
67
68
        return 0;
69
    }
70
71
    protected function stateMachineInDotFormat(array $config)
72
    {
73
        // Output image mime types.
74
        $mimeTypes = [
75
            'png' => 'image/png',
76
            'jpg' => 'image/jpeg',
77
            'gif' => 'image/gif',
78
            'svg' => 'image/svg+xml',
79
        ];
80
81
        $format = $this->option('format');
82
83
        if (empty($mimeTypes[$format])) {
84
            throw new \Exception(sprintf("Format '%s' is not supported", $format));
85
        }
86
87
        $dotFile = $this->buildDotFile($config);
88
89
        $dotPath = $this->option('dot-path');
90
        $outputImage = $this->option('output');
91
92
        $process = new Process([$dotPath, '-T' . $format, '-o', $outputImage, $dotFile]);
93
        $process->run();
94
95
        // executes after the command finishes
96
        if (!$process->isSuccessful()) {
97
            throw new ProcessFailedException($process);
98
        }
99
    }
100
101
    protected function buildDotFile(array $config): string
102
    {
103
        // Temporary files.
104
        $dotFile = tempnam(sys_get_temp_dir(), 'smv');
105
106
        // Display settings
107
        $layout = $this->option('direction');
108
        $layout = $layout === 'TB' ? 'TB' : 'LR';
109
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
        $result = implode(PHP_EOL, $result);
132
133
        // Save dot file for input.
134
        file_put_contents($dotFile, $result);
135
136
        return $dotFile;
137
    }
138
}
139