Graph::buildNamespaceTree()   B
last analyzed

Complexity

Conditions 7
Paths 24

Size

Total Lines 45

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 56

Importance

Changes 0
Metric Value
cc 7
nc 24
nop 2
dl 0
loc 45
ccs 0
cts 33
cp 0
crap 56
rs 8.2666
c 0
b 0
f 0
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
Bug introduced by
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();
0 ignored issues
show
Documentation Bug introduced by
The method getAll does not exist on object<phpDocumentor\Descriptor\ClassDescriptor>? 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...
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)
0 ignored issues
show
Unused Code introduced by
The parameter $graph is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

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