Completed
Push — master ( dc2d5c...df2031 )
by Jaap
17:57 queued 16:30
created

Graph::setPath()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 9
ccs 5
cts 5
cp 1
rs 9.9666
c 0
b 0
f 0
cc 3
nc 2
nop 1
crap 3
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * phpDocumentor
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 *
11
 * @link      http://phpdoc.org
12
 */
13
14
namespace phpDocumentor\GraphViz;
15
16
use InvalidArgumentException;
17
use const DIRECTORY_SEPARATOR;
18
use const PHP_EOL;
19
use function array_merge;
20
use function escapeshellarg;
21
use function exec;
22
use function file_put_contents;
23
use function implode;
24
use function in_array;
25
use function realpath;
26
use function strtolower;
27
use function substr;
28
use function sys_get_temp_dir;
29
use function tempnam;
30
use function unlink;
31
32
/**
33
 * Class representing a graph; this may be a main graph but also a subgraph.
34
 *
35
 * In case of a subgraph:
36
 * When the name of the subgraph is prefixed with _cluster_ then the contents
37
 * of this graph will be grouped and a border will be added. Otherwise it is
38
 * used as logical container to place defaults in.
39
 *
40
 * @method Graph setRankSep(string $rankSep)
41
 * @method Graph setCenter(string $center)
42
 * @method Graph setRank(string $rank)
43
 * @method Graph setRankDir(string $rankDir)
44
 * @method Graph setSplines(string $splines)
45
 * @method Graph setConcentrate(string $concentrate)
46
 */
47
class Graph
48
{
49
    use Attributes;
50
51
    /** @var string Name of this graph */
52
    protected $name = 'G';
53
54
    /** @var string Type of this graph; may be digraph, graph or subgraph */
55
    protected $type = 'digraph';
56
57
    /** @var bool If the graph is strict then multiple edges are not allowed between the same pairs of nodes */
58
    protected $strict = false;
59
60
    /** @var Graph[] A list of subgraphs for this Graph */
61
    protected $graphs = [];
62
63
    /** @var Node[] A list of nodes for this Graph */
64
    protected $nodes = [];
65
66
    /** @var Edge[] A list of edges / arrows for this Graph */
67
    protected $edges = [];
68
69
    /** @var string The path to execute dot from */
70
    protected $path = '';
71
72
    /**
73
     * Factory method to instantiate a Graph so that you can use fluent coding
74
     * to chain everything.
75
     *
76
     * @param string $name        The name for this graph.
77
     * @param bool   $directional Whether this is a directed or undirected graph.
78
     *
79
     * @return \phpDocumentor\GraphViz\Graph
80
     */
81 1
    public static function create(string $name = 'G', bool $directional = true) : self
82
    {
83 1
        $graph = new self();
84
        $graph
85 1
            ->setName($name)
86 1
            ->setType($directional ? 'digraph' : 'graph');
87
88 1
        return $graph;
89
    }
90
91
    /**
92
     * Sets the path for the execution. Only needed if it is not in the PATH env.
93
     *
94
     * @param string $path The path to execute dot from
95
     */
96 1
    public function setPath(string $path) : self
97
    {
98 1
        $realpath = realpath($path);
99 1
        if ($path && $path === $realpath) {
100 1
            $this->path = $path . DIRECTORY_SEPARATOR;
101
        }
102
103 1
        return $this;
104
    }
105
106
    /**
107
     * Sets the name for this graph.
108
     *
109
     * If this is a subgraph you can prefix the name with _cluster_ to group all
110
     * contained nodes and add a border.
111
     *
112
     * @param string $name The new name for this graph.
113
     */
114 1
    public function setName(string $name) : self
115
    {
116 1
        $this->name = $name;
117 1
        return $this;
118
    }
119
120
    /**
121
     * Returns the name for this Graph.
122
     */
123 1
    public function getName() : string
124
    {
125 1
        return $this->name;
126
    }
127
128
    /**
129
     * Sets the type for this graph.
130
     *
131
     * @param string $type Must be either "digraph", "graph" or "subgraph".
132
     *
133
     * @throws InvalidArgumentException If $type is not "digraph", "graph" or
134
     * "subgraph".
135
     */
136 2
    public function setType(string $type) : self
137
    {
138 2
        if (!in_array($type, ['digraph', 'graph', 'subgraph'], true)) {
139 1
            throw new InvalidArgumentException(
140
                'The type for a graph must be either "digraph", "graph" or '
141 1
                . '"subgraph"'
142
            );
143
        }
144
145 1
        $this->type = $type;
146 1
        return $this;
147
    }
148
149
    /**
150
     * Returns the type of this Graph.
151
     */
152 1
    public function getType() : string
153
    {
154 1
        return $this->type;
155
    }
156
157
    /**
158
     * Set if the Graph should be strict. If the graph is strict then
159
     * multiple edges are not allowed between the same pairs of nodes
160
     */
161 2
    public function setStrict(bool $isStrict) : self
162
    {
163 2
        $this->strict = $isStrict;
164 2
        return $this;
165
    }
166
167 1
    public function isStrict() : bool
168
    {
169 1
        return $this->strict;
170
    }
171
172
    /**
173
     * Magic method to provide a getter/setter to add attributes on the Graph.
174
     *
175
     * Using this method we make sure that we support any attribute without
176
     * too much hassle. If the name for this method does not start with get
177
     * or set we return null.
178
     *
179
     * Set methods return this graph (fluent interface) whilst get methods
180
     * return the attribute value.
181
     *
182
     * @param string  $name      Name of the method including get/set
183
     * @param mixed[] $arguments The arguments, should be 1: the value
184
     *
185
     * @return Attribute|Graph|null
186
     *
187
     * @throws AttributeNotFound
188
     */
189 1
    public function __call(string $name, array $arguments)
190
    {
191 1
        $key = strtolower(substr($name, 3));
192 1
        if (strtolower(substr($name, 0, 3)) === 'set') {
193 1
            return $this->setAttribute($key, (string) $arguments[0]);
194
        }
195
196 1
        if (strtolower(substr($name, 0, 3)) === 'get') {
197 1
            return $this->getAttribute($key);
198
        }
199
200 1
        return null;
201
    }
202
203
    /**
204
     * Adds a subgraph to this graph; automatically changes the type to subgraph.
205
     *
206
     * Please note that an index is maintained using the name of the subgraph.
207
     * Thus if you have 2 subgraphs with the same name that the first will be
208
     * overwritten by the latter.
209
     *
210
     * @see Graph::create()
211
     *
212
     * @param Graph $graph The graph to add onto this graph as
213
     * subgraph.
214
     */
215 1
    public function addGraph(self $graph) : self
216
    {
217 1
        $graph->setType('subgraph');
218 1
        $this->graphs[$graph->getName()] = $graph;
219 1
        return $this;
220
    }
221
222
    /**
223
     * Checks whether a graph with a certain name already exists.
224
     *
225
     * @param string $name Name of the graph to find.
226
     */
227 1
    public function hasGraph(string $name) : bool
228
    {
229 1
        return isset($this->graphs[$name]);
230
    }
231
232
    /**
233
     * Returns the subgraph with a given name.
234
     *
235
     * @param string $name Name of the requested graph.
236
     */
237 1
    public function getGraph(string $name) : self
238
    {
239 1
        return $this->graphs[$name];
240
    }
241
242
    /**
243
     * Sets a node in the $nodes array; uses the name of the node as index.
244
     *
245
     * Nodes can be retrieved by retrieving the property with the same name.
246
     * Thus 'node1' can be retrieved by invoking: $graph->node1
247
     *
248
     * @see Node::create()
249
     *
250
     * @param Node $node The node to set onto this Graph.
251
     */
252 1
    public function setNode(Node $node) : self
253
    {
254 1
        $this->nodes[$node->getName()] = $node;
255 1
        return $this;
256
    }
257
258
    /**
259
     * Finds a node in this graph or any of its subgraphs.
260
     *
261
     * @param string $name Name of the node to find.
262
     */
263 1
    public function findNode(string $name) : ?Node
264
    {
265 1
        if (isset($this->nodes[$name])) {
266 1
            return $this->nodes[$name];
267
        }
268
269 1
        foreach ($this->graphs as $graph) {
270 1
            $node = $graph->findNode($name);
271 1
            if ($node) {
272 1
                return $node;
273
            }
274
        }
275
276 1
        return null;
277
    }
278
279
    /**
280
     * Sets a node using a custom name.
281
     *
282
     * @see Graph::setNode()
283
     *
284
     * @param string $name  Name of the node.
285
     * @param Node   $value Node to set on the given name.
286
     */
287 1
    public function __set(string $name, Node $value) : self
288
    {
289 1
        $this->nodes[$name] = $value;
290 1
        return $this;
291
    }
292
293
    /**
294
     * Returns the requested node by its name.
295
     *
296
     * @see Graph::setNode()
297
     *
298
     * @param string $name The name of the node to retrieve.
299
     */
300 1
    public function __get(string $name) : ?Node
301
    {
302 1
        return $this->nodes[$name] ?? null;
303
    }
304
305
    /**
306
     * Links two nodes to eachother and registers the Edge onto this graph.
307
     *
308
     * @see Edge::create()
309
     *
310
     * @param Edge $edge The link between two classes.
311
     */
312 1
    public function link(Edge $edge) : self
313
    {
314 1
        $this->edges[] = $edge;
315 1
        return $this;
316
    }
317
318
    /**
319
     * Exports this graph to a generated image.
320
     *
321
     * This is the only method that actually requires GraphViz.
322
     *
323
     * @link http://www.graphviz.org/content/output-formats
324
     * @uses GraphViz/dot
325
     *
326
     * @param string $type     The type to export to; see the link above for a
327
     *     list of supported types.
328
     * @param string $filename The path to write to.
329
     *
330
     * @throws Exception If an error occurred in GraphViz.
331
     */
332 2
    public function export(string $type, string $filename) : self
333
    {
334 2
        $type     = escapeshellarg($type);
335 2
        $filename = escapeshellarg($filename);
336
337
        // write the dot file to a temporary file
338 2
        $tmpfile = (string) tempnam(sys_get_temp_dir(), 'gvz');
339 2
        file_put_contents($tmpfile, (string) $this);
340
341
        // escape the temp file for use as argument
342 2
        $tmpfileArg = escapeshellarg($tmpfile);
343
344
        // create the dot output
345 2
        $output = [];
346 2
        $code   = 0;
347 2
        exec($this->path . "dot -T${type} -o${filename} < ${tmpfileArg} 2>&1", $output, $code);
348 2
        unlink($tmpfile);
349
350 2
        if ($code !== 0) {
351 1
            throw new Exception(
352
                'An error occurred while creating the graph; GraphViz returned: '
353 1
                . implode(PHP_EOL, $output)
354
            );
355
        }
356
357 1
        return $this;
358
    }
359
360
    /**
361
     * Generates a DOT file for use with GraphViz.
362
     *
363
     * GraphViz is not used in this method; it is safe to call it even without
364
     * GraphViz installed.
365
     */
366 1
    public function __toString() : string
367
    {
368 1
        $elements = array_merge(
369 1
            $this->graphs,
370 1
            $this->attributes,
371 1
            $this->edges,
372 1
            $this->nodes
373
        );
374
375 1
        $attributes = [];
376 1
        foreach ($elements as $value) {
377 1
            $attributes[] = (string) $value;
378
        }
379
380 1
        $attributes = implode(PHP_EOL, $attributes);
381
382 1
        $strict = ($this->isStrict() ? 'strict ' : '');
383
384
        return <<<DOT
385 1
{$strict}{$this->getType()} "{$this->getName()}" {
386 1
${attributes}
387
}
388
DOT;
389
    }
390
}
391