Completed
Push — develop ( 0cc20e...dccdc9 )
by Mike
07:04
created

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

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\Router\RouterAbstract;
39
use phpDocumentor\Transformer\Transformation;
40
use phpDocumentor\Transformer\Transformer;
41
use phpDocumentor\Transformer\Writer\Xml\ArgumentConverter;
42
use phpDocumentor\Transformer\Writer\Xml\ConstantConverter;
43
use phpDocumentor\Transformer\Writer\Xml\DocBlockConverter;
44
use phpDocumentor\Transformer\Writer\Xml\InterfaceConverter;
45
use phpDocumentor\Transformer\Writer\Xml\MethodConverter;
46
use phpDocumentor\Transformer\Writer\Xml\PropertyConverter;
47
use phpDocumentor\Transformer\Writer\Xml\TagConverter;
48
use phpDocumentor\Transformer\Writer\Xml\TraitConverter;
49
50
/**
51
 * Converts the structural information of phpDocumentor into an XML file.
52
 */
53
class Xml extends WriterAbstract
54
{
55
    /** @var \DOMDocument $xml */
56
    protected $xml;
57
58
    protected $docBlockConverter;
59
60
    protected $argumentConverter;
61
62
    protected $methodConverter;
63
64
    protected $propertyConverter;
65
66
    protected $constantConverter;
67
68
    protected $interfaceConverter;
69
70
    protected $traitConverter;
71
72 2
    public function __construct(RouterAbstract $router)
73
    {
74 2
        $this->docBlockConverter = new DocBlockConverter(new TagConverter(), $router);
75 2
        $this->argumentConverter = new ArgumentConverter();
76 2
        $this->methodConverter = new MethodConverter($this->argumentConverter, $this->docBlockConverter);
77 2
        $this->propertyConverter = new PropertyConverter($this->docBlockConverter);
78 2
        $this->constantConverter = new ConstantConverter($this->docBlockConverter);
79 2
        $this->interfaceConverter = new InterfaceConverter(
80 2
            $this->docBlockConverter,
81 2
            $this->methodConverter,
82 2
            $this->constantConverter
83
        );
84 2
        $this->traitConverter = new TraitConverter(
85 2
            $this->docBlockConverter,
86 2
            $this->methodConverter,
87 2
            $this->propertyConverter
88
        );
89 2
    }
90
91
    /**
92
     * This method generates the AST output
93
     *
94
     * @param ProjectDescriptor $project        Document containing the structure.
95
     * @param Transformation    $transformation Transformation to execute.
96
     */
97 2
    public function transform(ProjectDescriptor $project, Transformation $transformation)
98
    {
99 2
        $artifact = $this->getDestinationPath($transformation);
100
101 2
        $this->checkForSpacesInPath($artifact);
102
103 2
        $this->xml = new \DOMDocument('1.0', 'utf-8');
104 2
        $this->xml->formatOutput = true;
105 2
        $document_element = new \DOMElement('project');
106 2
        $this->xml->appendChild($document_element);
107
108 2
        $document_element->setAttribute('title', $project->getName());
109 2
        $document_element->setAttribute('version', Application::VERSION());
110
111 2
        $this->buildPartials($document_element, $project);
112
113 2
        $transformer = $transformation->getTransformer();
114
115 2
        foreach ($project->getFiles() as $file) {
116 1
            $this->buildFile($document_element, $file, $transformer);
117
        }
118
119 2
        $this->finalize($project);
120 2
        file_put_contents($artifact, $this->xml->saveXML());
121 2
    }
122
123 2
    protected function buildPartials(\DOMElement $parent, ProjectDescriptor $project)
124
    {
125 2
        $child = new \DOMElement('partials');
126 2
        $parent->appendChild($child);
127 2
        foreach ($project->getPartials() as $name => $element) {
128
            $partial = new \DOMElement('partial');
129
            $child->appendChild($partial);
130
            $partial->setAttribute('name', $name);
131
            $partial->appendChild(new \DOMText($element));
132
        }
133 2
    }
134
135 1
    protected function buildFile(\DOMElement $parent, FileDescriptor $file, Transformer $transformer)
136
    {
137 1
        $child = new \DOMElement('file');
138 1
        $parent->appendChild($child);
139
140 1
        $path = ltrim($file->getPath(), './');
141 1
        $child->setAttribute('path', $path);
142 1
        $child->setAttribute(
143 1
            'generated-path',
144 1
            $transformer->generateFilename($path)
145
        );
146 1
        $child->setAttribute('hash', $file->getHash());
147
148 1
        $this->docBlockConverter->convert($child, $file);
149
150
        // add namespace aliases
151 1
        foreach ($file->getNamespaceAliases() as $alias => $namespace) {
152 1
            $alias_obj = new \DOMElement('namespace-alias', (string) $namespace);
153 1
            $child->appendChild($alias_obj);
154 1
            $alias_obj->setAttribute('name', (string) $alias);
155
        }
156
157
        /** @var ConstantDescriptor $constant */
158 1
        foreach ($file->getConstants() as $constant) {
159
            $this->constantConverter->convert($child, $constant);
160
        }
161
162
        /** @var FunctionDescriptor $function */
163 1
        foreach ($file->getFunctions() as $function) {
164
            $this->buildFunction($child, $function);
165
        }
166
167
        /** @var InterfaceDescriptor $interface */
168 1
        foreach ($file->getInterfaces() as $interface) {
169
            $this->interfaceConverter->convert($child, $interface);
170
        }
171
172
        /** @var ClassDescriptor $class */
173 1
        foreach ($file->getClasses() as $class) {
174
            $this->buildClass($child, $class);
175
        }
176
177
        /** @var TraitDescriptor $class */
178 1
        foreach ($file->getTraits() as $trait) {
179
            $this->traitConverter->convert($child, $trait);
180
        }
181
182
        // add markers
183 1
        if (count($file->getMarkers()) > 0) {
184
            $markers = new \DOMElement('markers');
185
            $child->appendChild($markers);
186
187
            foreach ($file->getMarkers() as $marker) {
188
                if (! $marker['type']) {
189
                    continue;
190
                }
191
192
                $type = preg_replace('/[^A-Za-z0-9\-]/', '', $marker['type']);
193
                $marker_obj = new \DOMElement(strtolower($type));
194
                $markers->appendChild($marker_obj);
195
196
                if (array_key_exists('message', $marker)) {
197
                    $marker_obj->appendChild(new \DOMText(trim((string) $marker['message'])));
198
                }
199
                $marker_obj->setAttribute('line', (string) $marker['line']);
200
            }
201
        }
202
203 1
        $errors = $file->getAllErrors();
204 1
        if (count($errors) > 0) {
205
            $parse_errors = new \DOMElement('parse_markers');
206
            $child->appendChild($parse_errors);
207
208
            /** @var Error $error */
209
            foreach ($errors as $error) {
210
                $this->createErrorEntry($error, $parse_errors);
211
            }
212
        }
213
214
        // if we want to include the source for each file; append a new
215
        // element 'source' which contains a compressed, encoded version
216
        // of the source
217 1
        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...
218
            $child->appendChild(new \DOMElement('source', base64_encode(gzcompress($file->getSource()))));
219
        }
220 1
    }
221
222
    /**
223
     * Creates an entry in the ParseErrors collection of a file for a given error.
224
     *
225
     * @param Error       $error
226
     * @param \DOMElement $parse_errors
227
     */
228
    protected function createErrorEntry($error, $parse_errors)
229
    {
230
        $marker_obj = new \DOMElement(strtolower($error->getSeverity()));
231
        $parse_errors->appendChild($marker_obj);
232
233
        $message = vsprintf($error->getCode(), $error->getContext());
234
235
        $marker_obj->appendChild(new \DOMText($message));
236
        $marker_obj->setAttribute('line', $error->getLine());
237
        $marker_obj->setAttribute('code', $error->getCode());
238
    }
239
240
    /**
241
     * Retrieves the destination location for this artifact.
242
     *
243
     * @return string
244
     */
245 2
    protected function getDestinationPath(Transformation $transformation)
246
    {
247 2
        return $transformation->getTransformer()->getTarget()
248 2
            . DIRECTORY_SEPARATOR . $transformation->getArtifact();
249
    }
250
251
    /**
252
     * Export this function definition to the given parent DOMElement.
253
     *
254
     * @param \DOMElement        $parent   Element to augment.
255
     * @param FunctionDescriptor $function Element to export.
256
     * @param \DOMElement        $child    if supplied this element will be augmented instead of freshly added.
257
     */
258
    public function buildFunction(\DOMElement $parent, FunctionDescriptor $function, \DOMElement $child = null)
259
    {
260
        if (!$child) {
261
            $child = new \DOMElement('function');
262
            $parent->appendChild($child);
263
        }
264
265
        $namespace = $function->getNamespace()
266
            ?: $parent->getAttribute('namespace');
267
        $child->setAttribute('namespace', ltrim((string) $namespace, '\\'));
268
        $child->setAttribute('line', (string) $function->getLine());
269
270
        $child->appendChild(new \DOMElement('name', $function->getName()));
271
        $child->appendChild(new \DOMElement('full_name', (string) $function->getFullyQualifiedStructuralElementName()));
272
273
        $this->docBlockConverter->convert($child, $function);
274
275
        foreach ($function->getArguments() as $argument) {
276
            $this->argumentConverter->convert($child, $argument);
277
        }
278
    }
279
280
    /**
281
     * Exports the given reflection object to the parent XML element.
282
     *
283
     * This method creates a new child element on the given parent XML element
284
     * and takes the properties of the Reflection argument and sets the
285
     * elements and attributes on the child.
286
     *
287
     * If a child DOMElement is provided then the properties and attributes are
288
     * set on this but the child element is not appended onto the parent. This
289
     * is the responsibility of the invoker. Essentially this means that the
290
     * $parent argument is ignored in this case.
291
     *
292
     * @param \DOMElement     $parent The parent element to augment.
293
     * @param ClassDescriptor $class  The data source.
294
     * @param \DOMElement     $child  Optional: child element to use instead of creating a
295
     *      new one on the $parent.
296
     */
297
    public function buildClass(\DOMElement $parent, ClassDescriptor $class, \DOMElement $child = null)
298
    {
299
        if (!$child) {
300
            $child = new \DOMElement('class');
301
            $parent->appendChild($child);
302
        }
303
304
        $child->setAttribute('final', $class->isFinal() ? 'true' : 'false');
305
        $child->setAttribute('abstract', $class->isAbstract() ? 'true' : 'false');
306
307
        if ($class->getParent() !== null) {
308
            $parentFqcn = $class->getParent() instanceof ClassDescriptor
309
                ? (string) $class->getParent()->getFullyQualifiedStructuralElementName()
310
                : (string) $class->getParent();
311
            $child->appendChild(new \DOMElement('extends', $parentFqcn));
312
        }
313
314
        /** @var InterfaceDescriptor $interface */
315
        foreach ($class->getInterfaces() as $interface) {
316
            $interfaceFqcn = $interface instanceof InterfaceDescriptor
317
                ? (string) $interface->getFullyQualifiedStructuralElementName()
318
                : (string) $interface;
319
            $child->appendChild(new \DOMElement('implements', $interfaceFqcn));
320
        }
321
322
        if ($child === null) {
323
            $child = new \DOMElement('interface');
324
            $parent->appendChild($child);
325
        }
326
327
        $namespace = (string) $class->getNamespace()->getFullyQualifiedStructuralElementName();
328
        $child->setAttribute('namespace', ltrim($namespace, '\\'));
329
        $child->setAttribute('line', (string) $class->getLine());
330
331
        $child->appendChild(new \DOMElement('name', $class->getName()));
332
        $child->appendChild(new \DOMElement('full_name', (string) $class->getFullyQualifiedStructuralElementName()));
333
334
        $this->docBlockConverter->convert($child, $class);
335
336
        foreach ($class->getConstants() as $constant) {
337
            // TODO #840: Workaround; for some reason there are NULLs in the constants array.
338
            if ($constant) {
339
                $this->constantConverter->convert($child, $constant);
340
            }
341
        }
342
343
        foreach ($class->getInheritedConstants() as $constant) {
344
            // TODO #840: Workaround; for some reason there are NULLs in the constants array.
345
            if ($constant) {
346
                $this->constantConverter->convert($child, $constant);
347
            }
348
        }
349
350
        foreach ($class->getProperties() as $property) {
351
            // TODO #840: Workaround; for some reason there are NULLs in the properties array.
352
            if ($property) {
353
                $this->propertyConverter->convert($child, $property);
354
            }
355
        }
356
357
        foreach ($class->getInheritedProperties() as $property) {
358
            // TODO #840: Workaround; for some reason there are NULLs in the properties array.
359
            if ($property) {
360
                $this->propertyConverter->convert($child, $property);
361
            }
362
        }
363
364
        foreach ($class->getMethods() as $method) {
365
            // TODO #840: Workaround; for some reason there are NULLs in the methods array.
366
            if ($method) {
367
                $this->methodConverter->convert($child, $method);
368
            }
369
        }
370
371
        foreach ($class->getInheritedMethods() as $method) {
372
            // TODO #840: Workaround; for some reason there are NULLs in the methods array.
373
            if ($method) {
374
                $methodElement = $this->methodConverter->convert($child, $method);
375
                $methodElement->appendChild(
376
                    new \DOMElement(
377
                        'inherited_from',
378
                        (string) $method->getParent()->getFullyQualifiedStructuralElementName()
379
                    )
380
                );
381
            }
382
        }
383
    }
384
385
    /**
386
     * Finalizes the processing and executing all post-processing actions.
387
     *
388
     * This method is responsible for extracting and manipulating the data that
389
     * is global to the project, such as:
390
     *
391
     * - Package tree
392
     * - Namespace tree
393
     * - Marker list
394
     * - Deprecated elements listing
395
     * - Removal of objects related to visibility
396
     */
397 2
    protected function finalize(ProjectDescriptor $projectDescriptor)
398
    {
399
        // TODO: move all these behaviours to a central location for all template parsers
400 2
        $behaviour = new AuthorTag();
401 2
        $behaviour->process($this->xml);
402 2
        $behaviour = new CoversTag();
403 2
        $behaviour->process($this->xml);
404 2
        $behaviour = new IgnoreTag();
405 2
        $behaviour->process($this->xml);
406 2
        $behaviour = new InternalTag(
407 2
            $projectDescriptor->isVisibilityAllowed(ProjectDescriptor\Settings::VISIBILITY_INTERNAL)
408
        );
409 2
        $behaviour->process($this->xml);
410 2
        $behaviour = new LicenseTag();
411 2
        $behaviour->process($this->xml);
412 2
        $behaviour = new MethodTag();
413 2
        $behaviour->process($this->xml);
414 2
        $behaviour = new ParamTag();
415 2
        $behaviour->process($this->xml);
416 2
        $behaviour = new PropertyTag();
417 2
        $behaviour->process($this->xml);
418 2
        $behaviour = new ReturnTag();
419 2
        $behaviour->process($this->xml);
420 2
        $behaviour = new UsesTag();
421 2
        $behaviour->process($this->xml);
422 2
        $behaviour = new VarTag();
423 2
        $behaviour->process($this->xml);
424 2
        $this->buildPackageTree($this->xml);
425 2
        $this->buildNamespaceTree($this->xml);
426 2
        $this->buildDeprecationList($this->xml);
427 2
    }
428
429
    /**
430
     * Collects all packages and subpackages, and adds a new section in the
431
     * DOM to provide an overview.
432
     *
433
     * @param \DOMDocument $dom Packages are extracted and a summary inserted
434
     *     in this object.
435
     */
436 2
    protected function buildPackageTree(\DOMDocument $dom)
437
    {
438 2
        $xpath = new \DOMXPath($dom);
439 2
        $packages = ['global' => true];
440 2
        $qry = $xpath->query('//@package');
441 2
        for ($i = 0; $i < $qry->length; ++$i) {
442 1
            if (isset($packages[$qry->item($i)->nodeValue])) {
443
                continue;
444
            }
445
446 1
            $packages[$qry->item($i)->nodeValue] = true;
447
        }
448
449 2
        $packages = $this->generateNamespaceTree(array_keys($packages));
450 2
        $this->generateNamespaceElements($packages, $dom->documentElement, 'package');
451 2
    }
452
453
    /**
454
     * Collects all namespaces and sub-namespaces, and adds a new section in
455
     * the DOM to provide an overview.
456
     *
457
     * @param \DOMDocument $dom Namespaces are extracted and a summary inserted
458
     *     in this object.
459
     */
460 2
    protected function buildNamespaceTree(\DOMDocument $dom)
461
    {
462 2
        $xpath = new \DOMXPath($dom);
463 2
        $namespaces = [];
464 2
        $qry = $xpath->query('//@namespace');
465 2
        for ($i = 0; $i < $qry->length; ++$i) {
466
            if (isset($namespaces[$qry->item($i)->nodeValue])) {
467
                continue;
468
            }
469
470
            $namespaces[$qry->item($i)->nodeValue] = true;
471
        }
472
473 2
        $namespaces = $this->generateNamespaceTree(array_keys($namespaces));
474 2
        $this->generateNamespaceElements($namespaces, $dom->documentElement);
475 2
    }
476
477
    /**
478
     * Adds a node to the xml for deprecations and the count value
479
     *
480
     * @param \DOMDocument $dom Markers are extracted and a summary inserted in this object.
481
     */
482 2
    protected function buildDeprecationList(\DOMDocument $dom)
483
    {
484 2
        $nodes = $this->getNodeListForTagBasedQuery($dom, 'deprecated');
485
486 2
        $node = new \DOMElement('deprecated');
487 2
        $dom->documentElement->appendChild($node);
488 2
        $node->setAttribute('count', (string) $nodes->length);
489 2
    }
490
491
    /**
492
     * Build a tag based query string and return result
493
     *
494
     * @param \DOMDocument $dom    Markers are extracted and a summary inserted
495
     *      in this object.
496
     * @param string       $marker The marker we're searching for throughout xml
497
     *
498
     * @return \DOMNodeList
499
     */
500 2
    protected function getNodeListForTagBasedQuery($dom, $marker)
501
    {
502 2
        $xpath = new \DOMXPath($dom);
503
504 2
        $query = '/project/file/markers/' . $marker . '|';
505 2
        $query .= '/project/file/docblock/tag[@name="' . $marker . '"]|';
506 2
        $query .= '/project/file/class/docblock/tag[@name="' . $marker . '"]|';
507 2
        $query .= '/project/file/class/*/docblock/tag[@name="' . $marker . '"]|';
508 2
        $query .= '/project/file/interface/docblock/tag[@name="' . $marker . '"]|';
509 2
        $query .= '/project/file/interface/*/docblock/tag[@name="' . $marker . '"]|';
510 2
        $query .= '/project/file/function/docblock/tag[@name="' . $marker . '"]|';
511 2
        $query .= '/project/file/constant/docblock/tag[@name="' . $marker . '"]';
512
513 2
        return $xpath->query($query);
514
    }
515
516
    /**
517
     * Generates a hierarchical array of namespaces with their singular name
518
     * from a single level list of namespaces with their full name.
519
     *
520
     * @param array $namespaces the list of namespaces as retrieved from the xml.
521
     *
522
     * @return array
523
     */
524 2
    protected function generateNamespaceTree($namespaces)
525
    {
526 2
        sort($namespaces);
527
528 2
        $result = [];
529 2
        foreach ($namespaces as $namespace) {
530 2
            if (!$namespace) {
531
                $namespace = 'global';
532
            }
533
534 2
            $namespace_list = explode('\\', $namespace);
535
536 2
            $node = &$result;
537 2
            foreach ($namespace_list as $singular) {
538 2
                if (!isset($node[$singular])) {
539 2
                    $node[$singular] = [];
540
                }
541
542 2
                $node = &$node[$singular];
543
            }
544
        }
545
546 2
        return $result;
547
    }
548
549
    /**
550
     * Recursive method to create a hierarchical set of nodes in the dom.
551
     *
552
     * @param array[]     $namespaces     the list of namespaces to process.
553
     * @param \DOMElement $parent_element the node to receive the children of
554
     *                                    the above list.
555
     * @param string      $node_name      the name of the summary element.
556
     */
557 2
    protected function generateNamespaceElements($namespaces, $parent_element, $node_name = 'namespace')
558
    {
559 2
        foreach ($namespaces as $name => $sub_namespaces) {
560 2
            $node = new \DOMElement($node_name);
561 2
            $parent_element->appendChild($node);
562 2
            $node->setAttribute('name', $name);
563 2
            $fullName = $parent_element->nodeName === $node_name
564
                ? $parent_element->getAttribute('full_name') . '\\' . $name
565 2
                : $name;
566 2
            $node->setAttribute('full_name', $fullName);
567 2
            $this->generateNamespaceElements($sub_namespaces, $node, $node_name);
568
        }
569 2
    }
570
}
571