1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* phpDocumentor |
4
|
|
|
* |
5
|
|
|
* PHP Version 5.3 |
6
|
|
|
* |
7
|
|
|
* @copyright 2010-2014 Mike van Riel / Naenius (http://www.naenius.com) |
8
|
|
|
* @license http://www.opensource.org/licenses/mit-license.php MIT |
9
|
|
|
* @link http://phpdoc.org |
10
|
|
|
*/ |
11
|
|
|
|
12
|
|
|
namespace phpDocumentor\Plugin\Graphs\Writer; |
13
|
|
|
|
14
|
|
|
use phpDocumentor\Descriptor\ClassDescriptor; |
15
|
|
|
use phpDocumentor\Descriptor\Collection; |
16
|
|
|
use phpDocumentor\Descriptor\DescriptorAbstract; |
17
|
|
|
use phpDocumentor\Descriptor\InterfaceDescriptor; |
18
|
|
|
use phpDocumentor\Descriptor\NamespaceDescriptor; |
19
|
|
|
use phpDocumentor\Descriptor\ProjectDescriptor; |
20
|
|
|
use phpDocumentor\Descriptor\TraitDescriptor; |
21
|
|
|
use phpDocumentor\GraphViz\Edge; |
22
|
|
|
use phpDocumentor\GraphViz\Graph as GraphVizGraph; |
23
|
|
|
use phpDocumentor\GraphViz\Node; |
24
|
|
|
use phpDocumentor\Reflection\Fqsen; |
25
|
|
|
use phpDocumentor\Transformer\Transformation; |
26
|
|
|
use phpDocumentor\Transformer\Writer\WriterAbstract; |
27
|
|
|
use Zend\Stdlib\Exception\ExtensionNotLoadedException; |
28
|
|
|
|
29
|
|
|
/** |
30
|
|
|
* Writer responsible for generating various graphs. |
31
|
|
|
* |
32
|
|
|
* The Graph writer is capable of generating a Graph (as provided using the 'source' parameter) at the location provided |
33
|
|
|
* using the artifact parameter. |
34
|
|
|
* |
35
|
|
|
* Currently supported: |
36
|
|
|
* |
37
|
|
|
* * 'class', a Class Diagram Generated using GraphViz |
38
|
|
|
* |
39
|
|
|
* @todo Fix this class |
40
|
|
|
*/ |
41
|
|
|
class Graph extends WriterAbstract |
|
|
|
|
42
|
|
|
{ |
43
|
|
|
/** @var string Name of the font to use to display the node labels with */ |
44
|
|
|
protected $nodeFont = 'Courier'; |
45
|
|
|
|
46
|
|
|
/** @var Node[] a cache where nodes for classes, interfaces and traits are stored for reference */ |
47
|
|
|
protected $nodeCache = array(); |
48
|
|
|
|
49
|
|
|
/** @var GraphVizGraph[] */ |
50
|
|
|
protected $namespaceCache = array(); |
51
|
|
|
|
52
|
|
|
/** |
53
|
|
|
* Invokes the query method contained in this class. |
54
|
|
|
* |
55
|
|
|
* @param ProjectDescriptor $project Document containing the structure. |
56
|
|
|
* @param Transformation $transformation Transformation to execute. |
57
|
|
|
* |
58
|
|
|
* @return void |
59
|
|
|
*/ |
60
|
|
|
public function transform(ProjectDescriptor $project, Transformation $transformation) |
61
|
|
|
{ |
62
|
|
|
$type_method = 'process' . ucfirst($transformation->getSource()); |
63
|
|
|
$this->$type_method($project, $transformation); |
64
|
|
|
} |
65
|
|
|
|
66
|
|
|
/** |
67
|
|
|
* Creates a class inheritance diagram. |
68
|
|
|
* |
69
|
|
|
* @param ProjectDescriptor $project |
70
|
|
|
* @param Transformation $transformation |
71
|
|
|
* |
72
|
|
|
* @return void |
73
|
|
|
*/ |
74
|
|
|
public function processClass(ProjectDescriptor $project, Transformation $transformation) |
|
|
|
|
75
|
|
|
{ |
76
|
|
|
try { |
77
|
|
|
$this->checkIfGraphVizIsInstalled(); |
78
|
|
|
} catch (\Exception $e) { |
79
|
|
|
echo $e->getMessage(); |
80
|
|
|
|
81
|
|
|
return; |
82
|
|
|
} |
83
|
|
|
|
84
|
|
|
if ($transformation->getParameter('font') !== null && $transformation->getParameter('font')->getValue()) { |
85
|
|
|
$this->nodeFont = $transformation->getParameter('font')->getValue(); |
86
|
|
|
} else { |
87
|
|
|
$this->nodeFont = 'Courier'; |
88
|
|
|
} |
89
|
|
|
|
90
|
|
|
$filename = $this->getDestinationPath($transformation); |
91
|
|
|
|
92
|
|
|
$graph = GraphVizGraph::create() |
93
|
|
|
->setRankSep('1.0') |
94
|
|
|
->setCenter('true') |
95
|
|
|
->setRank('source') |
96
|
|
|
->setRankDir('RL') |
97
|
|
|
->setSplines('true') |
98
|
|
|
->setConcentrate('true'); |
99
|
|
|
|
100
|
|
|
$this->buildNamespaceTree($graph, $project->getNamespace()); |
101
|
|
|
|
102
|
|
|
$classes = $project->getIndexes()->get('classes', new Collection())->getAll(); |
103
|
|
|
$interfaces = $project->getIndexes()->get('interfaces', new Collection())->getAll(); |
104
|
|
|
$traits = $project->getIndexes()->get('traits', new Collection())->getAll(); |
105
|
|
|
|
106
|
|
|
/** @var ClassDescriptor[]|InterfaceDescriptor[]|TraitDescriptor[] $containers */ |
107
|
|
|
$containers = array_merge($classes, $interfaces, $traits); |
108
|
|
|
|
109
|
|
|
foreach ($containers as $container) { |
110
|
|
|
$from_name = (string)$container->getFullyQualifiedStructuralElementName(); |
111
|
|
|
|
112
|
|
|
$parents = array(); |
113
|
|
|
$implemented = array(); |
114
|
|
|
if ($container instanceof ClassDescriptor) { |
115
|
|
|
if ($container->getParent()) { |
116
|
|
|
$parents[] = $container->getParent(); |
117
|
|
|
} |
118
|
|
|
$implemented = $container->getInterfaces()->getAll(); |
119
|
|
|
} |
120
|
|
|
if ($container instanceof InterfaceDescriptor) { |
121
|
|
|
$parents = $container->getParent()->getAll(); |
|
|
|
|
122
|
|
|
} |
123
|
|
|
|
124
|
|
|
/** @var string|ClassDescriptor|InterfaceDescriptor $parent */ |
125
|
|
|
foreach ($parents as $parent) { |
126
|
|
|
$edge = $this->createEdge($graph, $from_name, $parent); |
127
|
|
|
if ($edge !== null) { |
128
|
|
|
$edge->setArrowHead('empty'); |
129
|
|
|
$graph->link($edge); |
130
|
|
|
} |
131
|
|
|
} |
132
|
|
|
|
133
|
|
|
/** @var string|ClassDescriptor|InterfaceDescriptor $parent */ |
134
|
|
|
foreach ($implemented as $parent) { |
135
|
|
|
$edge = $this->createEdge($graph, $from_name, $parent); |
136
|
|
|
if ($edge !== null) { |
137
|
|
|
$edge->setStyle('dotted'); |
138
|
|
|
$edge->setArrowHead('empty'); |
139
|
|
|
$graph->link($edge); |
140
|
|
|
} |
141
|
|
|
} |
142
|
|
|
} |
143
|
|
|
|
144
|
|
|
$graph->export('svg', $filename); |
145
|
|
|
} |
146
|
|
|
|
147
|
|
|
/** |
148
|
|
|
* Creates a GraphViz Edge between two nodes. |
149
|
|
|
* |
150
|
|
|
* @param Graph $graph |
151
|
|
|
* @param string $from_name |
152
|
|
|
* @param string|ClassDescriptor|InterfaceDescriptor|TraitDescriptor $to |
153
|
|
|
* |
154
|
|
|
* @return Edge|null |
155
|
|
|
*/ |
156
|
|
|
protected function createEdge($graph, $from_name, $to) |
|
|
|
|
157
|
|
|
{ |
158
|
|
|
$to_name = (string)($to instanceof DescriptorAbstract ? $to->getFullyQualifiedStructuralElementName() : $to); |
159
|
|
|
|
160
|
|
|
if (!isset($this->nodeCache[$from_name])) { |
161
|
|
|
$namespaceParts = explode('\\', $from_name); |
162
|
|
|
$this->nodeCache[$from_name] = $this->createEmptyNode(array_pop($namespaceParts), $this->createNamespaceGraph($from_name)); |
163
|
|
|
} |
164
|
|
|
|
165
|
|
|
if (!isset($this->nodeCache[$to_name])) { |
166
|
|
|
$namespaceParts = explode('\\', $to_name); |
167
|
|
|
$this->nodeCache[$to_name] = $this->createEmptyNode(array_pop($namespaceParts), $this->createNamespaceGraph($to_name)); |
168
|
|
|
} |
169
|
|
|
|
170
|
|
|
$fromNode = $this->nodeCache[$from_name]; |
171
|
|
|
$toNode = $this->nodeCache[$to_name]; |
172
|
|
|
if ($fromNode !== null && $toNode !== null) { |
173
|
|
|
return Edge::create($fromNode, $toNode); |
174
|
|
|
} |
175
|
|
|
|
176
|
|
|
return null; |
177
|
|
|
} |
178
|
|
|
|
179
|
|
|
protected function createNamespaceGraph($fqcn) |
180
|
|
|
{ |
181
|
|
|
$namespaceParts = explode('\\', $fqcn); |
182
|
|
|
|
183
|
|
|
// push the classname off the stack |
184
|
|
|
array_pop($namespaceParts); |
185
|
|
|
|
186
|
|
|
$graph = null; |
187
|
|
|
$reassembledFqnn = ''; |
188
|
|
|
foreach ($namespaceParts as $part) { |
189
|
|
|
if ($part == '\\' || $part == '') { |
190
|
|
|
$part = 'Global'; |
191
|
|
|
$reassembledFqnn = 'Global'; |
192
|
|
|
} else { |
193
|
|
|
$reassembledFqnn = $reassembledFqnn . '\\' . $part; |
194
|
|
|
} |
195
|
|
|
if (isset($this->namespaceCache[$part])) { |
196
|
|
|
$graph = $this->namespaceCache[$part]; |
197
|
|
|
} else { |
198
|
|
|
$subgraph = $this->createGraphForNamespace($reassembledFqnn, $part); |
199
|
|
|
$graph->addGraph($subgraph); |
200
|
|
|
$graph = $subgraph; |
201
|
|
|
} |
202
|
|
|
} |
203
|
|
|
|
204
|
|
|
return $graph; |
205
|
|
|
} |
206
|
|
|
|
207
|
|
|
/** |
208
|
|
|
* @param string $name |
209
|
|
|
* @param Graph $graph |
210
|
|
|
* |
211
|
|
|
* @return Node |
212
|
|
|
*/ |
213
|
|
|
protected function createEmptyNode($name, $graph) |
214
|
|
|
{ |
215
|
|
|
if ($graph === null) { |
216
|
|
|
return null; |
217
|
|
|
} |
218
|
|
|
|
219
|
|
|
$node = Node::create($name); |
220
|
|
|
$node->setFontColor('gray'); |
221
|
|
|
$node->setLabel($name); |
222
|
|
|
$graph->setNode($node); |
|
|
|
|
223
|
|
|
|
224
|
|
|
return $node; |
225
|
|
|
} |
226
|
|
|
|
227
|
|
|
/** |
228
|
|
|
* Builds a tree of namespace subgraphs with their classes associated. |
229
|
|
|
* |
230
|
|
|
* @param GraphVizGraph $graph |
231
|
|
|
* @param NamespaceDescriptor $namespace |
232
|
|
|
* |
233
|
|
|
* @return void |
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((string)$sub_element->getFullyQualifiedStructuralElementName(), $sub_element->getName()) |
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
|
|
|
/** |
279
|
|
|
* @param \phpDocumentor\Transformer\Transformation $transformation |
280
|
|
|
* @return string |
281
|
|
|
*/ |
282
|
|
|
protected function getDestinationPath(Transformation $transformation) |
283
|
|
|
{ |
284
|
|
|
$filename = $transformation->getTransformer()->getTarget() |
285
|
|
|
. DIRECTORY_SEPARATOR . $transformation->getArtifact(); |
286
|
|
|
|
287
|
|
|
return $filename; |
288
|
|
|
} |
289
|
|
|
|
290
|
|
|
/** |
291
|
|
|
* Checks whether GraphViz is installed and throws an Exception otherwise. |
292
|
|
|
* |
293
|
|
|
* @throws ExtensionNotLoadedException if graphviz is not found. |
294
|
|
|
* |
295
|
|
|
* @return void |
296
|
|
|
*/ |
297
|
|
|
protected function checkIfGraphVizIsInstalled() |
298
|
|
|
{ |
299
|
|
|
// NOTE: the -V flag sends output using STDERR and STDOUT |
300
|
|
|
exec('dot -V 2>&1', $output, $error); |
301
|
|
|
if ($error != 0) { |
|
|
|
|
302
|
|
|
throw new ExtensionNotLoadedException( |
303
|
|
|
'Unable to find the `dot` command of the GraphViz package. ' |
304
|
|
|
. 'Is GraphViz correctly installed and present in your path?' |
305
|
|
|
); |
306
|
|
|
} |
307
|
|
|
} |
308
|
|
|
|
309
|
|
|
/** |
310
|
|
|
* @param $full_namespace_name |
311
|
|
|
* @param $label |
312
|
|
|
* |
313
|
|
|
* @return mixed |
314
|
|
|
*/ |
315
|
|
|
protected function createGraphForNamespace($full_namespace_name, $label) |
316
|
|
|
{ |
317
|
|
|
$sub_graph = GraphVizGraph::create('cluster_' . $full_namespace_name) |
318
|
|
|
->setLabel($label) |
319
|
|
|
->setStyle('rounded') |
320
|
|
|
->setColor('gray') |
321
|
|
|
->setFontColor('gray') |
322
|
|
|
->setFontSize('11') |
323
|
|
|
->setRankDir('LR') |
324
|
|
|
; |
325
|
|
|
|
326
|
|
|
return $sub_graph; |
327
|
|
|
} |
328
|
|
|
} |
329
|
|
|
|