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
|
|||
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
The method
getAll does not exist on object<phpDocumentor\Descriptor\ClassDescriptor> ? Since you implemented __call , maybe consider adding a @method annotation.
If you implement This is often the case, when 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
|
|||
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(); |
||
0 ignored issues
–
show
Unused Code
Comprehensibility
introduced
by
60% of this comment could be valid code. Did you maybe forget this after debugging?
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it. The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production. This check looks for comments that seem to be mostly valid code and reports them.
Loading history...
|
|||
267 | //$node->setURL($this->class_paths[$full_name]); |
||
0 ignored issues
–
show
Unused Code
Comprehensibility
introduced
by
84% of this comment could be valid code. Did you maybe forget this after debugging?
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it. The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production. This check looks for comments that seem to be mostly valid code and reports them.
Loading history...
|
|||
268 | //$node->setTarget('_parent'); |
||
0 ignored issues
–
show
Unused Code
Comprehensibility
introduced
by
86% of this comment could be valid code. Did you maybe forget this after debugging?
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it. The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production. This check looks for comments that seem to be mostly valid code and reports them.
Loading history...
|
|||
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 |
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: