Graph::__get()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 1
cts 1
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 1
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 function array_merge;
18
use function escapeshellarg;
19
use function exec;
20
use function file_put_contents;
21
use function implode;
22
use function in_array;
23
use function realpath;
24
use function strtolower;
25
use function substr;
26
use function sys_get_temp_dir;
27
use function tempnam;
28
use function unlink;
29
use const DIRECTORY_SEPARATOR;
30
use const PHP_EOL;
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 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
118 1
        return $this;
119
    }
120
121
    /**
122
     * Returns the name for this Graph.
123
     */
124 1
    public function getName() : string
125
    {
126 1
        return $this->name;
127
    }
128
129
    /**
130
     * Sets the type for this graph.
131
     *
132
     * @param string $type Must be either "digraph", "graph" or "subgraph".
133
     *
134
     * @throws InvalidArgumentException If $type is not "digraph", "graph" or
135
     * "subgraph".
136
     */
137 2
    public function setType(string $type) : self
138
    {
139 2
        if (!in_array($type, ['digraph', 'graph', 'subgraph'], true)) {
140 1
            throw new InvalidArgumentException(
141
                'The type for a graph must be either "digraph", "graph" or '
142 1
                . '"subgraph"'
143
            );
144
        }
145
146 1
        $this->type = $type;
147
148 1
        return $this;
149
    }
150
151
    /**
152
     * Returns the type of this Graph.
153
     */
154 1
    public function getType() : string
155
    {
156 1
        return $this->type;
157
    }
158
159
    /**
160
     * Set if the Graph should be strict. If the graph is strict then
161
     * multiple edges are not allowed between the same pairs of nodes
162
     */
163 2
    public function setStrict(bool $isStrict) : self
164
    {
165 2
        $this->strict = $isStrict;
166
167 2
        return $this;
168
    }
169
170 1
    public function isStrict() : bool
171
    {
172 1
        return $this->strict;
173
    }
174
175
    /**
176
     * Magic method to provide a getter/setter to add attributes on the Graph.
177
     *
178
     * Using this method we make sure that we support any attribute without
179
     * too much hassle. If the name for this method does not start with get
180
     * or set we return null.
181
     *
182
     * Set methods return this graph (fluent interface) whilst get methods
183
     * return the attribute value.
184
     *
185
     * @param string  $name      Name of the method including get/set
186
     * @param mixed[] $arguments The arguments, should be 1: the value
187
     *
188
     * @return Attribute|Graph|null
189
     *
190
     * @throws AttributeNotFound
191
     */
192 1
    public function __call(string $name, array $arguments)
193
    {
194 1
        $key = strtolower(substr($name, 3));
195 1
        if (strtolower(substr($name, 0, 3)) === 'set') {
196 1
            return $this->setAttribute($key, (string) $arguments[0]);
197
        }
198
199 1
        if (strtolower(substr($name, 0, 3)) === 'get') {
200 1
            return $this->getAttribute($key);
201
        }
202
203 1
        return null;
204
    }
205
206
    /**
207
     * Adds a subgraph to this graph; automatically changes the type to subgraph.
208
     *
209
     * Please note that an index is maintained using the name of the subgraph.
210
     * Thus if you have 2 subgraphs with the same name that the first will be
211
     * overwritten by the latter.
212
     *
213
     * @see Graph::create()
214
     *
215
     * @param Graph $graph The graph to add onto this graph as
216
     * subgraph.
217
     */
218 1
    public function addGraph(self $graph) : self
219
    {
220 1
        $graph->setType('subgraph');
221 1
        $this->graphs[$graph->getName()] = $graph;
222
223 1
        return $this;
224
    }
225
226
    /**
227
     * Checks whether a graph with a certain name already exists.
228
     *
229
     * @param string $name Name of the graph to find.
230
     */
231 1
    public function hasGraph(string $name) : bool
232
    {
233 1
        return isset($this->graphs[$name]);
234
    }
235
236
    /**
237
     * Returns the subgraph with a given name.
238
     *
239
     * @param string $name Name of the requested graph.
240
     */
241 1
    public function getGraph(string $name) : self
242
    {
243 1
        return $this->graphs[$name];
244
    }
245
246
    /**
247
     * Sets a node in the $nodes array; uses the name of the node as index.
248
     *
249
     * Nodes can be retrieved by retrieving the property with the same name.
250
     * Thus 'node1' can be retrieved by invoking: $graph->node1
251
     *
252
     * @see Node::create()
253
     *
254
     * @param Node $node The node to set onto this Graph.
255
     */
256 1
    public function setNode(Node $node) : self
257
    {
258 1
        $this->nodes[$node->getName()] = $node;
259
260 1
        return $this;
261
    }
262
263
    /**
264
     * Finds a node in this graph or any of its subgraphs.
265
     *
266
     * @param string $name Name of the node to find.
267
     */
268 1
    public function findNode(string $name) : ?Node
269
    {
270 1
        if (isset($this->nodes[$name])) {
271 1
            return $this->nodes[$name];
272
        }
273
274 1
        foreach ($this->graphs as $graph) {
275 1
            $node = $graph->findNode($name);
276 1
            if ($node) {
277 1
                return $node;
278
            }
279
        }
280
281 1
        return null;
282
    }
283
284
    /**
285
     * Sets a node using a custom name.
286
     *
287
     * @see Graph::setNode()
288
     *
289
     * @param string $name  Name of the node.
290
     * @param Node   $value Node to set on the given name.
291
     */
292 1
    public function __set(string $name, Node $value) : void
293
    {
294 1
        $this->nodes[$name] = $value;
295
    }
296 1
297
    /**
298
     * Returns the requested node by its name.
299
     *
300
     * @see Graph::setNode()
301
     *
302
     * @param string $name The name of the node to retrieve.
303
     */
304
    public function __get(string $name) : ?Node
305
    {
306 1
        return $this->nodes[$name] ?? null;
307
    }
308 1
309
    /**
310
     * Links two nodes to eachother and registers the Edge onto this graph.
311
     *
312
     * @see Edge::create()
313
     *
314
     * @param Edge $edge The link between two classes.
315
     */
316
    public function link(Edge $edge) : self
317
    {
318 1
        $this->edges[] = $edge;
319
320 1
        return $this;
321
    }
322 1
323
    /**
324
     * Exports this graph to a generated image.
325
     *
326
     * This is the only method that actually requires GraphViz.
327
     *
328
     * @link http://www.graphviz.org/content/output-formats
329
     * @uses GraphViz/dot
330
     *
331
     * @param string $type     The type to export to; see the link above for a
332
     *     list of supported types.
333
     * @param string $filename The path to write to.
334
     *
335
     * @throws Exception If an error occurred in GraphViz.
336
     */
337
    public function export(string $type, string $filename) : self
338
    {
339 2
        $type     = escapeshellarg($type);
340
        $filename = escapeshellarg($filename);
341 2
342 2
        // write the dot file to a temporary file
343
        $tmpfile = (string) tempnam(sys_get_temp_dir(), 'gvz');
344
        file_put_contents($tmpfile, (string) $this);
345 2
346 2
        // escape the temp file for use as argument
347
        $tmpfileArg = escapeshellarg($tmpfile);
348
349 2
        // create the dot output
350
        $output = [];
351
        $code   = 0;
352 2
        exec($this->path . "dot -T${type} -o${filename} < ${tmpfileArg} 2>&1", $output, $code);
353 2
        unlink($tmpfile);
354 2
355 2
        if ($code !== 0) {
356
            throw new Exception(
357 2
                'An error occurred while creating the graph; GraphViz returned: '
358 1
                . implode(PHP_EOL, $output)
359
            );
360 1
        }
361
362
        return $this;
363
    }
364 1
365
    /**
366
     * Generates a DOT file for use with GraphViz.
367
     *
368
     * GraphViz is not used in this method; it is safe to call it even without
369
     * GraphViz installed.
370
     */
371
    public function __toString() : string
372
    {
373 1
        $elements = array_merge(
374
            $this->graphs,
375 1
            $this->attributes,
376 1
            $this->edges,
377 1
            $this->nodes
378 1
        );
379 1
380
        $attributes = [];
381
        foreach ($elements as $value) {
382 1
            $attributes[] = (string) $value;
383 1
        }
384 1
385
        $attributes = implode(PHP_EOL, $attributes);
386
387 1
        $strict = ($this->isStrict() ? 'strict ' : '');
388
389 1
        return <<<DOT
390
{$strict}{$this->getType()} "{$this->getName()}" {
391
${attributes}
392 1
}
393 1
DOT;
394
    }
395
}
396