Completed
Push — develop ( 80740b...61b5c3 )
by Mike
10:20
created

Xml::transform()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 2
dl 0
loc 25
rs 9.52
c 0
b 0
f 0
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 Mike van Riel
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