Completed
Push — develop ( 80740b...61b5c3 )
by Mike
10:20
created

src/phpDocumentor/Transformer/Writer/Graph.php (2 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
declare(strict_types=1);
3
4
/**
5
 * This file is part of phpDocumentor.
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 *
10
 * @author    Mike van Riel <[email protected]>
11
 * @copyright 2010-2018 Mike van Riel / Naenius (http://www.naenius.com)
12
 * @license   http://www.opensource.org/licenses/mit-license.php MIT
13
 * @link      http://phpdoc.org
14
 */
15
16
namespace phpDocumentor\Transformer\Writer;
17
18
use phpDocumentor\Descriptor\ClassDescriptor;
19
use phpDocumentor\Descriptor\Collection;
0 ignored issues
show
This use statement conflicts with another class in this namespace, phpDocumentor\Transformer\Writer\Collection.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
20
use phpDocumentor\Descriptor\DescriptorAbstract;
21
use phpDocumentor\Descriptor\InterfaceDescriptor;
22
use phpDocumentor\Descriptor\NamespaceDescriptor;
23
use phpDocumentor\Descriptor\ProjectDescriptor;
24
use phpDocumentor\Descriptor\TraitDescriptor;
25
use phpDocumentor\GraphViz\Edge;
26
use phpDocumentor\GraphViz\Graph as GraphVizGraph;
27
use phpDocumentor\GraphViz\Node;
28
use phpDocumentor\Transformer\Transformation;
29
use phpDocumentor\Transformer\Writer\WriterAbstract;
30
use Zend\Stdlib\Exception\ExtensionNotLoadedException;
31
32
/**
33
 * Writer responsible for generating various graphs.
34
 *
35
 * The Graph writer is capable of generating a Graph (as provided using the 'source' parameter) at the location provided
36
 * using the artifact parameter.
37
 *
38
 * Currently supported:
39
 *
40
 * * 'class', a Class Diagram Generated using GraphViz
41
 *
42
 * @todo Fix this class
43
 */
44
class Graph extends WriterAbstract
45
{
46
    /** @var string Name of the font to use to display the node labels with */
47
    protected $nodeFont = 'Courier';
48
49
    /** @var Node[] a cache where nodes for classes, interfaces and traits are stored for reference */
50
    protected $nodeCache = [];
51
52
    /** @var GraphVizGraph[] */
53
    protected $namespaceCache = [];
54
55
    /**
56
     * Invokes the query method contained in this class.
57
     *
58
     * @param ProjectDescriptor $project        Document containing the structure.
59
     * @param Transformation    $transformation Transformation to execute.
60
     */
61
    public function transform(ProjectDescriptor $project, Transformation $transformation)
62
    {
63
        $type_method = 'process' . ucfirst($transformation->getSource());
64
        $this->{$type_method}($project, $transformation);
65
    }
66
67
    /**
68
     * Creates a class inheritance diagram.
69
     */
70
    public function processClass(ProjectDescriptor $project, Transformation $transformation)
71
    {
72
        try {
73
            $this->checkIfGraphVizIsInstalled();
74
        } catch (\Exception $e) {
75
            echo $e->getMessage();
76
77
            return;
78
        }
79
80
        if ($transformation->getParameter('font') !== null && $transformation->getParameter('font')->getValue()) {
81
            $this->nodeFont = $transformation->getParameter('font')->getValue();
82
        } else {
83
            $this->nodeFont = 'Courier';
84
        }
85
86
        $filename = $this->getDestinationPath($transformation);
87
88
        $graph = GraphVizGraph::create()
89
            ->setRankSep('1.0')
90
            ->setCenter('true')
91
            ->setRank('source')
92
            ->setRankDir('RL')
93
            ->setSplines('true')
94
            ->setConcentrate('true');
95
96
        $this->buildNamespaceTree($graph, $project->getNamespace());
97
98
        $classes = $project->getIndexes()->get('classes', new Collection())->getAll();
99
        $interfaces = $project->getIndexes()->get('interfaces', new Collection())->getAll();
100
        $traits = $project->getIndexes()->get('traits', new Collection())->getAll();
101
102
        /** @var ClassDescriptor[]|InterfaceDescriptor[]|TraitDescriptor[] $containers */
103
        $containers = array_merge($classes, $interfaces, $traits);
104
105
        foreach ($containers as $container) {
106
            $from_name = (string) $container->getFullyQualifiedStructuralElementName();
107
108
            $parents = [];
109
            $implemented = [];
110
            if ($container instanceof ClassDescriptor) {
111
                if ($container->getParent()) {
112
                    $parents[] = $container->getParent();
113
                }
114
115
                $implemented = $container->getInterfaces()->getAll();
116
            }
117
118
            if ($container instanceof InterfaceDescriptor) {
119
                $parents = $container->getParent()->getAll();
120
            }
121
122
            /** @var string|ClassDescriptor|InterfaceDescriptor $parent */
123
            foreach ($parents as $parent) {
124
                $edge = $this->createEdge($graph, $from_name, $parent);
125
                if ($edge !== null) {
126
                    $edge->setArrowHead('empty');
127
                    $graph->link($edge);
128
                }
129
            }
130
131
            /** @var string|ClassDescriptor|InterfaceDescriptor $parent */
132
            foreach ($implemented as $parent) {
133
                $edge = $this->createEdge($graph, $from_name, $parent);
134
                if ($edge !== null) {
135
                    $edge->setStyle('dotted');
136
                    $edge->setArrowHead('empty');
137
                    $graph->link($edge);
138
                }
139
            }
140
        }
141
142
        $graph->export('svg', $filename);
143
    }
144
145
    /**
146
     * Creates a GraphViz Edge between two nodes.
147
     *
148
     * @param Graph  $graph
149
     * @param string $from_name
150
     * @param string|ClassDescriptor|InterfaceDescriptor|TraitDescriptor $to
151
     *
152
     * @return Edge|null
153
     */
154
    protected function createEdge($graph, $from_name, $to)
155
    {
156
        $to_name = (string) ($to instanceof DescriptorAbstract ? $to->getFullyQualifiedStructuralElementName() : $to);
157
158
        if (!isset($this->nodeCache[$from_name])) {
159
            $namespaceParts = explode('\\', $from_name);
160
            $this->nodeCache[$from_name] = $this->createEmptyNode(
161
                array_pop($namespaceParts),
162
                $this->createNamespaceGraph($from_name)
163
            );
164
        }
165
166
        if (!isset($this->nodeCache[$to_name])) {
167
            $namespaceParts = explode('\\', $to_name);
168
            $this->nodeCache[$to_name] = $this->createEmptyNode(
169
                array_pop($namespaceParts),
170
                $this->createNamespaceGraph($to_name)
171
            );
172
        }
173
174
        $fromNode = $this->nodeCache[$from_name];
175
        $toNode = $this->nodeCache[$to_name];
176
        if ($fromNode !== null && $toNode !== null) {
177
            return Edge::create($fromNode, $toNode);
178
        }
179
180
        return null;
181
    }
182
183
    protected function createNamespaceGraph($fqcn)
184
    {
185
        $namespaceParts = explode('\\', $fqcn);
186
187
        // push the classname off the stack
188
        array_pop($namespaceParts);
189
190
        $graph = null;
191
        $reassembledFqnn = '';
192
        foreach ($namespaceParts as $part) {
193
            if ($part === '\\' || $part === '') {
194
                $part = 'Global';
195
                $reassembledFqnn = 'Global';
196
            } else {
197
                $reassembledFqnn = $reassembledFqnn . '\\' . $part;
198
            }
199
200
            if (isset($this->namespaceCache[$part])) {
201
                $graph = $this->namespaceCache[$part];
202
            } else {
203
                $subgraph = $this->createGraphForNamespace($reassembledFqnn, $part);
204
                $graph->addGraph($subgraph);
205
                $graph = $subgraph;
206
            }
207
        }
208
209
        return $graph;
210
    }
211
212
    /**
213
     * @param string $name
214
     * @param Graph $graph
215
     *
216
     * @return Node
217
     */
218
    protected function createEmptyNode($name, $graph)
219
    {
220
        if ($graph === null) {
221
            return null;
222
        }
223
224
        $node = Node::create($name);
225
        $node->setFontColor('gray');
226
        $node->setLabel($name);
227
        $graph->setNode($node);
0 ignored issues
show
The method setNode() does not seem to exist on object<phpDocumentor\Transformer\Writer\Graph>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
228
229
        return $node;
230
    }
231
232
    /**
233
     * Builds a tree of namespace subgraphs with their classes associated.
234
     */
235
    protected function buildNamespaceTree(GraphVizGraph $graph, NamespaceDescriptor $namespace)
236
    {
237
        $full_namespace_name = (string) $namespace->getFullyQualifiedStructuralElementName();
238
        if ($full_namespace_name === '\\') {
239
            $full_namespace_name = 'Global';
240
        }
241
242
        $label = $namespace->getName() === '\\' ? 'Global' : $namespace->getName();
243
        $sub_graph = $this->createGraphForNamespace($full_namespace_name, $label);
244
        $this->namespaceCache[$full_namespace_name] = $sub_graph;
245
246
        $elements = array_merge(
247
            $namespace->getClasses()->getAll(),
248
            $namespace->getInterfaces()->getAll(),
249
            $namespace->getTraits()->getAll()
250
        );
251
252
        /** @var ClassDescriptor|InterfaceDescriptor|TraitDescriptor $sub_element */
253
        foreach ($elements as $sub_element) {
254
            $node = Node::create(
255
                (string) $sub_element->getFullyQualifiedStructuralElementName(),
256
                $sub_element->getName()
257
            )
258
                ->setShape('box')
259
                ->setFontName($this->nodeFont)
260
                ->setFontSize('11');
261
262
            if ($sub_element instanceof ClassDescriptor && $sub_element->isAbstract()) {
263
                $node->setLabel('<«abstract»<br/>' . $sub_element->getName() . '>');
264
            }
265
266
            //$full_name = $sub_element->getFullyQualifiedStructuralElementName();
267
            //$node->setURL($this->class_paths[$full_name]);
268
            //$node->setTarget('_parent');
269
270
            $this->nodeCache[(string) $sub_element->getFullyQualifiedStructuralElementName()] = $node;
271
            $sub_graph->setNode($node);
272
        }
273
274
        foreach ($namespace->getChildren()->getAll() as $element) {
275
            $this->buildNamespaceTree($sub_graph, $element);
276
        }
277
278
        $graph->addGraph($sub_graph);
279
    }
280
281
    protected function getDestinationPath(Transformation $transformation)
282
    {
283
        return $transformation->getTransformer()->getTarget()
284
            . DIRECTORY_SEPARATOR . $transformation->getArtifact();
285
    }
286
287
    /**
288
     * Checks whether GraphViz is installed and throws an Exception otherwise.
289
     *
290
     * @throws ExtensionNotLoadedException if graphviz is not found.
291
     */
292
    protected function checkIfGraphVizIsInstalled()
293
    {
294
        // NOTE: the -V flag sends output using STDERR and STDOUT
295
        exec('dot -V 2>&1', $output, $error);
296
        if ($error !== 0) {
297
            throw new ExtensionNotLoadedException(
298
                'Unable to find the `dot` command of the GraphViz package. '
299
                . 'Is GraphViz correctly installed and present in your path?'
300
            );
301
        }
302
    }
303
304
    /**
305
     * @param string $full_namespace_name
306
     * @param string $label
307
     *
308
     * @return mixed
309
     */
310
    protected function createGraphForNamespace($full_namespace_name, $label)
311
    {
312
        return GraphVizGraph::create('cluster_' . $full_namespace_name)
313
            ->setLabel($label)
314
            ->setStyle('rounded')
315
            ->setColor('gray')
316
            ->setFontColor('gray')
317
            ->setFontSize('11')
318
            ->setRankDir('LR');
319
    }
320
}
321