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; |
|
|
|
|
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
|
|
|
*/ |
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
|
|
|
|
Let’s assume that you have a directory layout like this:
and let’s assume the following content of
Bar.php
:If both files
OtherDir/Foo.php
andSomeDir/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 beforeOtherDir/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: