Completed
Push — develop ( 8eb671...133594 )
by Mike
19:30 queued 09:24
created

src/phpDocumentor/Transformer/Writer/Xml.php (1 issue)

Check for loose comparison of strings.

Best Practice Bug Major

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\Application;
19
use phpDocumentor\Descriptor\ClassDescriptor;
20
use phpDocumentor\Descriptor\ConstantDescriptor;
21
use phpDocumentor\Descriptor\FileDescriptor;
22
use phpDocumentor\Descriptor\FunctionDescriptor;
23
use phpDocumentor\Descriptor\InterfaceDescriptor;
24
use phpDocumentor\Descriptor\ProjectDescriptor;
25
use phpDocumentor\Descriptor\TraitDescriptor;
26
use phpDocumentor\Descriptor\Validator\Error;
27
use phpDocumentor\Transformer\Behaviour\Tag\AuthorTag;
28
use phpDocumentor\Transformer\Behaviour\Tag\CoversTag;
29
use phpDocumentor\Transformer\Behaviour\Tag\IgnoreTag;
30
use phpDocumentor\Transformer\Behaviour\Tag\InternalTag;
31
use phpDocumentor\Transformer\Behaviour\Tag\LicenseTag;
32
use phpDocumentor\Transformer\Behaviour\Tag\MethodTag;
33
use phpDocumentor\Transformer\Behaviour\Tag\ParamTag;
34
use phpDocumentor\Transformer\Behaviour\Tag\PropertyTag;
35
use phpDocumentor\Transformer\Behaviour\Tag\ReturnTag;
36
use phpDocumentor\Transformer\Behaviour\Tag\UsesTag;
37
use phpDocumentor\Transformer\Behaviour\Tag\VarTag;
38
use phpDocumentor\Transformer\Writer\Xml\ArgumentConverter;
39
use phpDocumentor\Transformer\Writer\Xml\ConstantConverter;
40
use phpDocumentor\Transformer\Writer\Xml\DocBlockConverter;
41
use phpDocumentor\Transformer\Writer\Xml\InterfaceConverter;
42
use phpDocumentor\Transformer\Writer\Xml\MethodConverter;
43
use phpDocumentor\Transformer\Writer\Xml\PropertyConverter;
44
use phpDocumentor\Transformer\Writer\Xml\TagConverter;
45
use phpDocumentor\Transformer\Writer\Xml\TraitConverter;
46
use phpDocumentor\Transformer\Router\RouterAbstract;
47
use phpDocumentor\Transformer\Transformation;
48
use phpDocumentor\Transformer\Transformer;
49
use phpDocumentor\Transformer\Writer\WriterAbstract;
50
51
/**
52
 * Converts the structural information of phpDocumentor into an XML file.
53
 */
54
class Xml extends WriterAbstract
55
{
56
    /** @var \DOMDocument $xml */
57
    protected $xml;
58
59
    protected $docBlockConverter;
60
61
    protected $argumentConverter;
62
63
    protected $methodConverter;
64
65
    protected $propertyConverter;
66
67
    protected $constantConverter;
68
69
    protected $interfaceConverter;
70
71
    protected $traitConverter;
72
73
    public function __construct(RouterAbstract $router)
74
    {
75
        $this->docBlockConverter = new DocBlockConverter(new TagConverter(), $router);
76
        $this->argumentConverter = new ArgumentConverter();
77
        $this->methodConverter = new MethodConverter($this->argumentConverter, $this->docBlockConverter);
78
        $this->propertyConverter = new PropertyConverter($this->docBlockConverter);
79
        $this->constantConverter = new ConstantConverter($this->docBlockConverter);
80
        $this->interfaceConverter = new InterfaceConverter(
81
            $this->docBlockConverter,
82
            $this->methodConverter,
83
            $this->constantConverter
84
        );
85
        $this->traitConverter = new TraitConverter(
86
            $this->docBlockConverter,
87
            $this->methodConverter,
88
            $this->propertyConverter
89
        );
90
    }
91
92
    /**
93
     * This method generates the AST output
94
     *
95
     * @param ProjectDescriptor $project        Document containing the structure.
96
     * @param Transformation    $transformation Transformation to execute.
97
     */
98
    public function transform(ProjectDescriptor $project, Transformation $transformation)
99
    {
100
        $artifact = $this->getDestinationPath($transformation);
101
102
        $this->checkForSpacesInPath($artifact);
103
104
        $this->xml = new \DOMDocument('1.0', 'utf-8');
105
        $this->xml->formatOutput = true;
106
        $document_element = new \DOMElement('project');
107
        $this->xml->appendChild($document_element);
108
109
        $document_element->setAttribute('title', $project->getName());
110
        $document_element->setAttribute('version', Application::VERSION());
111
112
        $this->buildPartials($document_element, $project);
113
114
        $transformer = $transformation->getTransformer();
115
116
        foreach ($project->getFiles() as $file) {
117
            $this->buildFile($document_element, $file, $transformer);
118
        }
119
120
        $this->finalize($project);
121
        file_put_contents($artifact, $this->xml->saveXML());
122
    }
123
124
    protected function buildPartials(\DOMElement $parent, ProjectDescriptor $project)
125
    {
126
        $child = new \DOMElement('partials');
127
        $parent->appendChild($child);
128
        foreach ($project->getPartials() as $name => $element) {
129
            $partial = new \DOMElement('partial');
130
            $child->appendChild($partial);
131
            $partial->setAttribute('name', $name);
132
            $partial->appendChild(new \DOMText($element));
133
        }
134
    }
135
136
    protected function buildFile(\DOMElement $parent, FileDescriptor $file, Transformer $transformer)
137
    {
138
        $child = new \DOMElement('file');
139
        $parent->appendChild($child);
140
141
        $path = ltrim($file->getPath(), './');
142
        $child->setAttribute('path', $path);
143
        $child->setAttribute(
144
            'generated-path',
145
            $transformer->generateFilename($path)
146
        );
147
        $child->setAttribute('hash', $file->getHash());
148
149
        $this->docBlockConverter->convert($child, $file);
150
151
        // add namespace aliases
152
        foreach ($file->getNamespaceAliases() as $alias => $namespace) {
153
            $alias_obj = new \DOMElement('namespace-alias', (string) $namespace);
154
            $child->appendChild($alias_obj);
155
            $alias_obj->setAttribute('name', (string) $alias);
156
        }
157
158
        /** @var ConstantDescriptor $constant */
159
        foreach ($file->getConstants() as $constant) {
160
            $this->constantConverter->convert($child, $constant);
161
        }
162
163
        /** @var FunctionDescriptor $function */
164
        foreach ($file->getFunctions() as $function) {
165
            $this->buildFunction($child, $function);
166
        }
167
168
        /** @var InterfaceDescriptor $interface */
169
        foreach ($file->getInterfaces() as $interface) {
170
            $this->interfaceConverter->convert($child, $interface);
171
        }
172
173
        /** @var ClassDescriptor $class */
174
        foreach ($file->getClasses() as $class) {
175
            $this->buildClass($child, $class);
176
        }
177
178
        /** @var TraitDescriptor $class */
179
        foreach ($file->getTraits() as $trait) {
180
            $this->traitConverter->convert($child, $trait);
181
        }
182
183
        // add markers
184
        if (count($file->getMarkers()) > 0) {
185
            $markers = new \DOMElement('markers');
186
            $child->appendChild($markers);
187
188
            foreach ($file->getMarkers() as $marker) {
189
                if (! $marker['type']) {
190
                    continue;
191
                }
192
193
                $type = preg_replace('/[^A-Za-z0-9\-]/', '', $marker['type']);
194
                $marker_obj = new \DOMElement(strtolower($type));
195
                $markers->appendChild($marker_obj);
196
197
                if (array_key_exists('message', $marker)) {
198
                    $marker_obj->appendChild(new \DOMText(trim((string) $marker['message'])));
199
                }
200
                $marker_obj->setAttribute('line', (string) $marker['line']);
201
            }
202
        }
203
204
        $errors = $file->getAllErrors();
205
        if (count($errors) > 0) {
206
            $parse_errors = new \DOMElement('parse_markers');
207
            $child->appendChild($parse_errors);
208
209
            /** @var Error $error */
210
            foreach ($errors as $error) {
211
                $this->createErrorEntry($error, $parse_errors);
212
            }
213
        }
214
215
        // if we want to include the source for each file; append a new
216
        // element 'source' which contains a compressed, encoded version
217
        // of the source
218
        if ($file->getSource()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $file->getSource() of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
219
            $child->appendChild(new \DOMElement('source', base64_encode(gzcompress($file->getSource()))));
220
        }
221
    }
222
223
    /**
224
     * Creates an entry in the ParseErrors collection of a file for a given error.
225
     *
226
     * @param Error       $error
227
     * @param \DOMElement $parse_errors
228
     */
229
    protected function createErrorEntry($error, $parse_errors)
230
    {
231
        $marker_obj = new \DOMElement(strtolower($error->getSeverity()));
232
        $parse_errors->appendChild($marker_obj);
233
234
        $message = vsprintf($error->getCode(), $error->getContext());
235
236
        $marker_obj->appendChild(new \DOMText($message));
237
        $marker_obj->setAttribute('line', $error->getLine());
238
        $marker_obj->setAttribute('code', $error->getCode());
239
    }
240
241
    /**
242
     * Retrieves the destination location for this artifact.
243
     *
244
     * @return string
245
     */
246
    protected function getDestinationPath(Transformation $transformation)
247
    {
248
        return $transformation->getTransformer()->getTarget()
249
            . DIRECTORY_SEPARATOR . $transformation->getArtifact();
250
    }
251
252
    /**
253
     * Export this function definition to the given parent DOMElement.
254
     *
255
     * @param \DOMElement        $parent   Element to augment.
256
     * @param FunctionDescriptor $function Element to export.
257
     * @param \DOMElement        $child    if supplied this element will be augmented instead of freshly added.
258
     */
259
    public function buildFunction(\DOMElement $parent, FunctionDescriptor $function, \DOMElement $child = null)
260
    {
261
        if (!$child) {
262
            $child = new \DOMElement('function');
263
            $parent->appendChild($child);
264
        }
265
266
        $namespace = $function->getNamespace()
267
            ?: $parent->getAttribute('namespace');
268
        $child->setAttribute('namespace', ltrim((string) $namespace, '\\'));
269
        $child->setAttribute('line', (string) $function->getLine());
270
271
        $child->appendChild(new \DOMElement('name', $function->getName()));
272
        $child->appendChild(new \DOMElement('full_name', (string) $function->getFullyQualifiedStructuralElementName()));
273
274
        $this->docBlockConverter->convert($child, $function);
275
276
        foreach ($function->getArguments() as $argument) {
277
            $this->argumentConverter->convert($child, $argument);
278
        }
279
    }
280
281
    /**
282
     * Exports the given reflection object to the parent XML element.
283
     *
284
     * This method creates a new child element on the given parent XML element
285
     * and takes the properties of the Reflection argument and sets the
286
     * elements and attributes on the child.
287
     *
288
     * If a child DOMElement is provided then the properties and attributes are
289
     * set on this but the child element is not appended onto the parent. This
290
     * is the responsibility of the invoker. Essentially this means that the
291
     * $parent argument is ignored in this case.
292
     *
293
     * @param \DOMElement     $parent The parent element to augment.
294
     * @param ClassDescriptor $class  The data source.
295
     * @param \DOMElement     $child  Optional: child element to use instead of creating a
296
     *      new one on the $parent.
297
     */
298
    public function buildClass(\DOMElement $parent, ClassDescriptor $class, \DOMElement $child = null)
299
    {
300
        if (!$child) {
301
            $child = new \DOMElement('class');
302
            $parent->appendChild($child);
303
        }
304
305
        $child->setAttribute('final', $class->isFinal() ? 'true' : 'false');
306
        $child->setAttribute('abstract', $class->isAbstract() ? 'true' : 'false');
307
308
        if ($class->getParent() !== null) {
309
            $parentFqcn = $class->getParent() instanceof ClassDescriptor
310
                ? (string) $class->getParent()->getFullyQualifiedStructuralElementName()
311
                : (string) $class->getParent();
312
            $child->appendChild(new \DOMElement('extends', $parentFqcn));
313
        }
314
315
        /** @var InterfaceDescriptor $interface */
316
        foreach ($class->getInterfaces() as $interface) {
317
            $interfaceFqcn = $interface instanceof InterfaceDescriptor
318
                ? (string) $interface->getFullyQualifiedStructuralElementName()
319
                : (string) $interface;
320
            $child->appendChild(new \DOMElement('implements', $interfaceFqcn));
321
        }
322
323
        if ($child === null) {
324
            $child = new \DOMElement('interface');
325
            $parent->appendChild($child);
326
        }
327
328
        $namespace = (string) $class->getNamespace()->getFullyQualifiedStructuralElementName();
329
        $child->setAttribute('namespace', ltrim($namespace, '\\'));
330
        $child->setAttribute('line', (string) $class->getLine());
331
332
        $child->appendChild(new \DOMElement('name', $class->getName()));
333
        $child->appendChild(new \DOMElement('full_name', (string) $class->getFullyQualifiedStructuralElementName()));
334
335
        $this->docBlockConverter->convert($child, $class);
336
337
        foreach ($class->getConstants() as $constant) {
338
            // TODO #840: Workaround; for some reason there are NULLs in the constants array.
339
            if ($constant) {
340
                $this->constantConverter->convert($child, $constant);
341
            }
342
        }
343
344
        foreach ($class->getInheritedConstants() as $constant) {
345
            // TODO #840: Workaround; for some reason there are NULLs in the constants array.
346
            if ($constant) {
347
                $this->constantConverter->convert($child, $constant);
348
            }
349
        }
350
351
        foreach ($class->getProperties() as $property) {
352
            // TODO #840: Workaround; for some reason there are NULLs in the properties array.
353
            if ($property) {
354
                $this->propertyConverter->convert($child, $property);
355
            }
356
        }
357
358
        foreach ($class->getInheritedProperties() as $property) {
359
            // TODO #840: Workaround; for some reason there are NULLs in the properties array.
360
            if ($property) {
361
                $this->propertyConverter->convert($child, $property);
362
            }
363
        }
364
365
        foreach ($class->getMethods() as $method) {
366
            // TODO #840: Workaround; for some reason there are NULLs in the methods array.
367
            if ($method) {
368
                $this->methodConverter->convert($child, $method);
369
            }
370
        }
371
372
        foreach ($class->getInheritedMethods() as $method) {
373
            // TODO #840: Workaround; for some reason there are NULLs in the methods array.
374
            if ($method) {
375
                $methodElement = $this->methodConverter->convert($child, $method);
376
                $methodElement->appendChild(
377
                    new \DOMElement(
378
                        'inherited_from',
379
                        (string) $method->getParent()->getFullyQualifiedStructuralElementName()
380
                    )
381
                );
382
            }
383
        }
384
    }
385
386
    /**
387
     * Finalizes the processing and executing all post-processing actions.
388
     *
389
     * This method is responsible for extracting and manipulating the data that
390
     * is global to the project, such as:
391
     *
392
     * - Package tree
393
     * - Namespace tree
394
     * - Marker list
395
     * - Deprecated elements listing
396
     * - Removal of objects related to visibility
397
     */
398
    protected function finalize(ProjectDescriptor $projectDescriptor)
399
    {
400
        // TODO: move all these behaviours to a central location for all template parsers
401
        $behaviour = new AuthorTag();
402
        $behaviour->process($this->xml);
403
        $behaviour = new CoversTag();
404
        $behaviour->process($this->xml);
405
        $behaviour = new IgnoreTag();
406
        $behaviour->process($this->xml);
407
        $behaviour = new InternalTag(
408
            $projectDescriptor->isVisibilityAllowed(ProjectDescriptor\Settings::VISIBILITY_INTERNAL)
409
        );
410
        $behaviour->process($this->xml);
411
        $behaviour = new LicenseTag();
412
        $behaviour->process($this->xml);
413
        $behaviour = new MethodTag();
414
        $behaviour->process($this->xml);
415
        $behaviour = new ParamTag();
416
        $behaviour->process($this->xml);
417
        $behaviour = new PropertyTag();
418
        $behaviour->process($this->xml);
419
        $behaviour = new ReturnTag();
420
        $behaviour->process($this->xml);
421
        $behaviour = new UsesTag();
422
        $behaviour->process($this->xml);
423
        $behaviour = new VarTag();
424
        $behaviour->process($this->xml);
425
        $this->buildPackageTree($this->xml);
426
        $this->buildNamespaceTree($this->xml);
427
        $this->buildDeprecationList($this->xml);
428
    }
429
430
    /**
431
     * Collects all packages and subpackages, and adds a new section in the
432
     * DOM to provide an overview.
433
     *
434
     * @param \DOMDocument $dom Packages are extracted and a summary inserted
435
     *     in this object.
436
     */
437
    protected function buildPackageTree(\DOMDocument $dom)
438
    {
439
        $xpath = new \DOMXPath($dom);
440
        $packages = ['global' => true];
441
        $qry = $xpath->query('//@package');
442
        for ($i = 0; $i < $qry->length; ++$i) {
443
            if (isset($packages[$qry->item($i)->nodeValue])) {
444
                continue;
445
            }
446
447
            $packages[$qry->item($i)->nodeValue] = true;
448
        }
449
450
        $packages = $this->generateNamespaceTree(array_keys($packages));
451
        $this->generateNamespaceElements($packages, $dom->documentElement, 'package');
452
    }
453
454
    /**
455
     * Collects all namespaces and sub-namespaces, and adds a new section in
456
     * the DOM to provide an overview.
457
     *
458
     * @param \DOMDocument $dom Namespaces are extracted and a summary inserted
459
     *     in this object.
460
     */
461
    protected function buildNamespaceTree(\DOMDocument $dom)
462
    {
463
        $xpath = new \DOMXPath($dom);
464
        $namespaces = [];
465
        $qry = $xpath->query('//@namespace');
466
        for ($i = 0; $i < $qry->length; ++$i) {
467
            if (isset($namespaces[$qry->item($i)->nodeValue])) {
468
                continue;
469
            }
470
471
            $namespaces[$qry->item($i)->nodeValue] = true;
472
        }
473
474
        $namespaces = $this->generateNamespaceTree(array_keys($namespaces));
475
        $this->generateNamespaceElements($namespaces, $dom->documentElement);
476
    }
477
478
    /**
479
     * Adds a node to the xml for deprecations and the count value
480
     *
481
     * @param \DOMDocument $dom Markers are extracted and a summary inserted in this object.
482
     */
483
    protected function buildDeprecationList(\DOMDocument $dom)
484
    {
485
        $nodes = $this->getNodeListForTagBasedQuery($dom, 'deprecated');
486
487
        $node = new \DOMElement('deprecated');
488
        $dom->documentElement->appendChild($node);
489
        $node->setAttribute('count', (string) $nodes->length);
490
    }
491
492
    /**
493
     * Build a tag based query string and return result
494
     *
495
     * @param \DOMDocument $dom    Markers are extracted and a summary inserted
496
     *      in this object.
497
     * @param string       $marker The marker we're searching for throughout xml
498
     *
499
     * @return \DOMNodeList
500
     */
501
    protected function getNodeListForTagBasedQuery($dom, $marker)
502
    {
503
        $xpath = new \DOMXPath($dom);
504
505
        $query = '/project/file/markers/' . $marker . '|';
506
        $query .= '/project/file/docblock/tag[@name="' . $marker . '"]|';
507
        $query .= '/project/file/class/docblock/tag[@name="' . $marker . '"]|';
508
        $query .= '/project/file/class/*/docblock/tag[@name="' . $marker . '"]|';
509
        $query .= '/project/file/interface/docblock/tag[@name="' . $marker . '"]|';
510
        $query .= '/project/file/interface/*/docblock/tag[@name="' . $marker . '"]|';
511
        $query .= '/project/file/function/docblock/tag[@name="' . $marker . '"]|';
512
        $query .= '/project/file/constant/docblock/tag[@name="' . $marker . '"]';
513
514
        return $xpath->query($query);
515
    }
516
517
    /**
518
     * Generates a hierarchical array of namespaces with their singular name
519
     * from a single level list of namespaces with their full name.
520
     *
521
     * @param array $namespaces the list of namespaces as retrieved from the xml.
522
     *
523
     * @return array
524
     */
525
    protected function generateNamespaceTree($namespaces)
526
    {
527
        sort($namespaces);
528
529
        $result = [];
530
        foreach ($namespaces as $namespace) {
531
            if (!$namespace) {
532
                $namespace = 'global';
533
            }
534
535
            $namespace_list = explode('\\', $namespace);
536
537
            $node = &$result;
538
            foreach ($namespace_list as $singular) {
539
                if (!isset($node[$singular])) {
540
                    $node[$singular] = [];
541
                }
542
543
                $node = &$node[$singular];
544
            }
545
        }
546
547
        return $result;
548
    }
549
550
    /**
551
     * Recursive method to create a hierarchical set of nodes in the dom.
552
     *
553
     * @param array[]     $namespaces     the list of namespaces to process.
554
     * @param \DOMElement $parent_element the node to receive the children of
555
     *                                    the above list.
556
     * @param string      $node_name      the name of the summary element.
557
     */
558
    protected function generateNamespaceElements($namespaces, $parent_element, $node_name = 'namespace')
559
    {
560
        foreach ($namespaces as $name => $sub_namespaces) {
561
            $node = new \DOMElement($node_name);
562
            $parent_element->appendChild($node);
563
            $node->setAttribute('name', $name);
564
            $fullName = $parent_element->nodeName === $node_name
565
                ? $parent_element->getAttribute('full_name') . '\\' . $name
566
                : $name;
567
            $node->setAttribute('full_name', $fullName);
568
            $this->generateNamespaceElements($sub_namespaces, $node, $node_name);
569
        }
570
    }
571
}
572