Completed
Pull Request — master (#1148)
by Alexander
13:51
created

XmlSerializationVisitor::visitArray()   F

Complexity

Conditions 15
Paths 6272

Size

Total Lines 31
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 15.4394

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 15
eloc 19
c 1
b 0
f 0
nc 6272
nop 2
dl 0
loc 31
ccs 14
cts 16
cp 0.875
crap 15.4394
rs 1.7499

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace JMS\Serializer;
6
7
use JMS\Serializer\Exception\NotAcceptableException;
8
use JMS\Serializer\Exception\RuntimeException;
9
use JMS\Serializer\Metadata\ClassMetadata;
10
use JMS\Serializer\Metadata\PropertyMetadata;
11
use JMS\Serializer\Metadata\StaticPropertyMetadata;
12
use JMS\Serializer\Visitor\SerializationVisitorInterface;
13
14
/**
15
 * XmlSerializationVisitor.
16
 *
17
 * @author Johannes M. Schmitt <[email protected]>
18
 */
19
final class XmlSerializationVisitor extends AbstractVisitor implements SerializationVisitorInterface
20
{
21
    /**
22
     * @var \DOMDocument
23
     */
24
    private $document;
25
26
    /**
27
     * @var string
28
     */
29
    private $defaultRootName = 'result';
30
31
    /**
32
     * @var string|null
33
     */
34
    private $defaultRootNamespace;
35
36 121
    /**
37
     * @var string|null
38
     */
39
    private $defaultRootPrefix;
40
41
    /**
42
     * @var \SplStack
43
     */
44 121
    private $stack;
45 121
46 121
    /**
47
     * @var \SplStack
48 121
     */
49 121
    private $metadataStack;
50
51 121
    /**
52
     * @var \DOMNode|\DOMElement|null
53 121
     */
54 121
    private $currentNode;
55 121
56 121
    /**
57
     * @var ClassMetadata|PropertyMetadata|null
58 121
     */
59
    private $currentMetadata;
60 121
61 121
    /**
62
     * @var bool
63 121
     */
64
    private $hasValue;
65
66 111
    /**
67
     * @var bool
68 111
     */
69 20
    private $nullWasVisited;
70 20
71 20
    /**
72
     * @var \SplStack
73 91
     */
74 91
    private $objectMetadataStack;
75 91
76
    public function __construct(
77
        bool $formatOutput = true,
78 111
        string $defaultEncoding = 'UTF-8',
79 111
        string $defaultVersion = '1.0',
80 8
        string $defaultRootName = 'result',
81
        ?string $defaultRootNamespace = null,
82 103
        ?string $defaultRootPrefix = null
83
    ) {
84 111
        $this->objectMetadataStack = new \SplStack();
85 111
        $this->stack = new \SplStack();
86
        $this->metadataStack = new \SplStack();
87 111
88
        $this->currentNode = null;
89
        $this->nullWasVisited = false;
90 9
91
        $this->document = $this->createDocument($formatOutput, $defaultVersion, $defaultEncoding);
92 9
93 9
        $this->defaultRootName = $defaultRootName;
94 9
        $this->defaultRootNamespace = $defaultRootNamespace;
95
        $this->defaultRootPrefix = $defaultRootPrefix;
96 9
    }
97
98
    private function createDocument(bool $formatOutput, string $defaultVersion, string $defaultEncoding): \DOMDocument
99 80
    {
100
        $document = new \DOMDocument($defaultVersion, $defaultEncoding);
101 80
        $document->formatOutput = $formatOutput;
102
103 80
        return $document;
104
    }
105
106 2
    public function createRoot(?ClassMetadata $metadata = null, ?string $rootName = null, ?string $rootNamespace = null, ?string $rootPrefix = null): \DOMElement
107
    {
108 2
        if (null !== $metadata && !empty($metadata->xmlRootName)) {
109
            $rootPrefix = $metadata->xmlRootPrefix;
110
            $rootName = $metadata->xmlRootName;
111 5
            $rootNamespace = $metadata->xmlRootNamespace ?: $this->getClassDefaultNamespace($metadata);
112
        } else {
113 5
            $rootName = $rootName ?: $this->defaultRootName;
114
            $rootNamespace = $rootNamespace ?: $this->defaultRootNamespace;
115
            $rootPrefix = $rootPrefix ?: $this->defaultRootPrefix;
116 21
        }
117
118 21
        $document = $this->getDocument();
119
        if ($rootNamespace) {
120
            $rootNode = $document->createElementNS($rootNamespace, (null !== $rootPrefix ? ($rootPrefix . ':') : '') . $rootName);
121 9
        } else {
122
            $rootNode = $document->createElement($rootName);
123 9
        }
124 4
        $document->appendChild($rootNode);
125
        $this->setCurrentNode($rootNode);
126 6
127
        return $rootNode;
128
    }
129
    /**
130 32
     * {@inheritdoc}
131
     */
132 32
    public function visitNull($data, array $type)
133 11
    {
134
        $node = $this->document->createAttribute('xsi:nil');
135
        $node->value = 'true';
136 32
        $this->nullWasVisited = true;
137 32
138 32
        return $node;
139
    }
140 32
    /**
141 32
     * {@inheritdoc}
142
     */
143 31
    public function visitString(string $data, array $type)
144
    {
145 31
        $doCData = null !== $this->currentMetadata ? $this->currentMetadata->xmlElementCData : true;
146 31
147 31
        return $doCData ? $this->document->createCDATASection($data) : $this->document->createTextNode((string) $data);
148
    }
149 31
150 7
    /**
151
     * @param mixed $data
152
     * @param array $type
153
     */
154 31
    public function visitSimpleString($data, array $type): \DOMText
0 ignored issues
show
Unused Code introduced by
The parameter $type is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

154
    public function visitSimpleString($data, /** @scrutinizer ignore-unused */ array $type): \DOMText

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
155 28
    {
156
        return $this->document->createTextNode((string) $data);
157 5
    }
158 5
159
    /**
160
     * {@inheritdoc}
161 31
     */
162
    public function visitBoolean(bool $data, array $type)
163 32
    {
164
        return $this->document->createTextNode($data ? 'true' : 'false');
165 80
    }
166
167 80
    /**
168
     * {@inheritdoc}
169 80
     */
170 78
    public function visitInteger(int $data, array $type)
171
    {
172
        return $this->document->createTextNode((string) $data);
173 80
    }
174
175 80
    /**
176 80
     * {@inheritdoc}
177
     */
178 79
    public function visitDouble(float $data, array $type)
179
    {
180 79
        return $this->document->createTextNode(var_export((float) $data, true));
181 14
    }
182 14
183 14
    /**
184
     * {@inheritdoc}
185 14
     */
186
    public function visitArray(array $data, array $type): void
187
    {
188
        if (null === $this->currentNode) {
189 14
            $this->createRoot();
190
        }
191 14
192
        $entryName = null !== $this->currentMetadata && null !== $this->currentMetadata->xmlEntryName ? $this->currentMetadata->xmlEntryName : 'entry';
0 ignored issues
show
Bug introduced by
The property xmlEntryName does not seem to exist on JMS\Serializer\Metadata\ClassMetadata.
Loading history...
193
        $keyAttributeName = null !== $this->currentMetadata && null !== $this->currentMetadata->xmlKeyAttribute ? $this->currentMetadata->xmlKeyAttribute : null;
0 ignored issues
show
Bug introduced by
The property xmlKeyAttribute does not exist on JMS\Serializer\Metadata\ClassMetadata. Did you mean xmlAttribute?
Loading history...
194 76
        $namespace = null !== $this->currentMetadata && null !== $this->currentMetadata->xmlEntryNamespace ? $this->currentMetadata->xmlEntryNamespace : null;
0 ignored issues
show
Bug introduced by
The property xmlEntryNamespace does not exist on JMS\Serializer\Metadata\ClassMetadata. Did you mean xmlNamespace?
Loading history...
195 76
196
        $elType = $this->getElementType($type);
197 1
        foreach ($data as $k => $v) {
198
            $tagName = null !== $this->currentMetadata && $this->currentMetadata->xmlKeyValuePairs && $this->isElementNameValid((string) $k) ? $k : $entryName;
0 ignored issues
show
Bug introduced by
The property xmlKeyValuePairs does not seem to exist on JMS\Serializer\Metadata\ClassMetadata.
Loading history...
199
200 76
            $entryNode = $this->createElement($tagName, $namespace);
201 9
            $this->currentNode->appendChild($entryNode);
202
            $this->setCurrentNode($entryNode);
203 9
204 9
            if (null !== $keyAttributeName) {
205 9
                $entryNode->setAttribute($keyAttributeName, (string) $k);
206
            }
207 9
208
            try {
209
                if (null !== $node = $this->navigator->accept($v, $elType)) {
210
                    $this->currentNode->appendChild($node);
211 9
                }
212
            } catch (NotAcceptableException $e) {
213 9
                $this->currentNode->parentNode->removeChild($this->currentNode);
214
            }
215
216 72
            $this->revertCurrentNode();
217 2
        }
218 1
    }
219
220
    /**
221 1
     * {@inheritdoc}
222 1
     */
223 1
    public function startVisitingObject(ClassMetadata $metadata, object $data, array $type): void
224 1
    {
225
        $this->objectMetadataStack->push($metadata);
226 1
227
        if (null === $this->currentNode) {
228
            $this->createRoot($metadata);
229
        }
230 1
231
        $this->addNamespaceAttributes($metadata, $this->currentNode);
232
233 1
        $this->hasValue = false;
234
    }
235
236 70
    /**
237
     * {@inheritdoc}
238 69
     */
239 9
    public function visitProperty(PropertyMetadata $metadata, $v): void
240 69
    {
241
        if ($metadata->xmlAttribute) {
242 69
            $this->setCurrentMetadata($metadata);
243 69
            $node = $this->navigator->accept($v, $metadata->type);
244 69
            $this->revertCurrentMetadata();
245
246
            if (!$node instanceof \DOMCharacterData) {
247 70
                throw new RuntimeException(sprintf('Unsupported value for XML attribute for %s. Expected character data, but got %s.', $metadata->name, json_encode($v)));
248
            }
249
250 70
            $this->setAttributeOnNode($this->currentNode, $metadata->serializedName, $node->nodeValue, $metadata->xmlNamespace);
0 ignored issues
show
Bug introduced by
It seems like $this->currentNode can also be of type null; however, parameter $node of JMS\Serializer\XmlSerial...r::setAttributeOnNode() does only seem to accept DOMElement, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

250
            $this->setAttributeOnNode(/** @scrutinizer ignore-type */ $this->currentNode, $metadata->serializedName, $node->nodeValue, $metadata->xmlNamespace);
Loading history...
251 70
252
            return;
253 2
        }
254 2
255 2
        if (($metadata->xmlValue && $this->currentNode->childNodes->length > 0)
256 2
            || (!$metadata->xmlValue && $this->hasValue)
257 2
        ) {
258 2
            throw new RuntimeException(sprintf('If you make use of @XmlValue, all other properties in the class must have the @XmlAttribute annotation. Invalid usage detected in class %s.', $metadata->class));
259
        }
260
261 70
        if ($metadata->xmlValue) {
262
            $this->hasValue = true;
263 70
264 69
            $this->setCurrentMetadata($metadata);
265
            $node = $this->navigator->accept($v, $metadata->type);
266 69
            $this->revertCurrentMetadata();
267 3
268
            if (!$node instanceof \DOMCharacterData) {
269
                throw new RuntimeException(sprintf('Unsupported value for property %s::$%s. Expected character data, but got %s.', $metadata->reflection->class, $metadata->reflection->name, \is_object($node) ? \get_class($node) : \gettype($node)));
0 ignored issues
show
Bug introduced by
The property reflection does not seem to exist on JMS\Serializer\Metadata\PropertyMetadata.
Loading history...
270
            }
271 70
272 70
            $this->currentNode->appendChild($node);
0 ignored issues
show
Bug introduced by
The method appendChild() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

272
            $this->currentNode->/** @scrutinizer ignore-call */ 
273
                                appendChild($node);

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...
273
274 70
            return;
275
        }
276 70
277
        if ($metadata->xmlAttributeMap) {
278
            if (!\is_array($v)) {
279 6
                throw new RuntimeException(sprintf('Unsupported value type for XML attribute map. Expected array but got %s.', \gettype($v)));
280
            }
281 6
282
            foreach ($v as $key => $value) {
283
                $this->setCurrentMetadata($metadata);
284 7
                $node = $this->navigator->accept($value, null);
285
                $this->revertCurrentMetadata();
286 7
287
                if (!$node instanceof \DOMCharacterData) {
288
                    throw new RuntimeException(sprintf('Unsupported value for a XML attribute map value. Expected character data, but got %s.', json_encode($v)));
289 69
                }
290
291 69
                $this->setAttributeOnNode($this->currentNode, $key, $node->nodeValue, $metadata->xmlNamespace);
292
            }
293
294 77
            return;
295
        }
296 77
297 77
        if ($addEnclosingElement = !$this->isInLineCollection($metadata) && !$metadata->inline) {
0 ignored issues
show
Comprehensibility introduced by
Consider adding parentheses for clarity. Current Interpretation: $addEnclosingElement = (...&& ! $metadata->inline), Probably Intended Meaning: ($addEnclosingElement = ... && ! $metadata->inline
Loading history...
298
            $namespace = null !== $metadata->xmlNamespace
299 110
                ? $metadata->xmlNamespace
300
                : $this->getClassDefaultNamespace($this->objectMetadataStack->top());
301 110
302 22
            $element = $this->createElement($metadata->serializedName, $namespace);
303 1
            $this->currentNode->appendChild($element);
304
            $this->setCurrentNode($element);
305 21
        }
306 21
307 20
        $this->setCurrentMetadata($metadata);
308
309
        try {
310
            if (null !== $node = $this->navigator->accept($v, $metadata->type)) {
311
                $this->currentNode->appendChild($node);
312 110
            }
313 9
        } catch (NotAcceptableException $e) {
314 9
            $this->currentNode->parentNode->removeChild($this->currentNode);
0 ignored issues
show
Bug introduced by
It seems like $this->currentNode can also be of type null; however, parameter $oldnode of DOMElement::removeChild() does only seem to accept DOMNode, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

314
            $this->currentNode->parentNode->removeChild(/** @scrutinizer ignore-type */ $this->currentNode);
Loading history...
Bug introduced by
It seems like $this->currentNode can also be of type null; however, parameter $oldnode of DOMNode::removeChild() does only seem to accept DOMNode, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

314
            $this->currentNode->parentNode->removeChild(/** @scrutinizer ignore-type */ $this->currentNode);
Loading history...
315 9
            $this->revertCurrentMetadata();
316 9
            $this->revertCurrentNode();
317
            $this->hasValue = false;
318
            return;
319 110
        }
320
321
        $this->revertCurrentMetadata();
322 2
323
        if ($addEnclosingElement) {
324 2
            $this->revertCurrentNode();
325
326
            if ($this->isElementEmpty($element) && (null === $v || $this->isSkippableCollection($metadata) || $this->isSkippableEmptyObject($node, $metadata))) {
327
                $this->currentNode->removeChild($element);
328
            }
329
        }
330
331
        $this->hasValue = false;
332 113
    }
333
334 113
    /**
335
     * {@inheritdoc}
336
     */
337 113
    public function visitStaticProperty(string $name, $v): void
338
    {
339
        $this->visitProperty(new StaticPropertyMetadata('', $name, $v), $v);
340 78
    }
341
342 78
    private function isInLineCollection(PropertyMetadata $metadata): bool
343 78
    {
344 78
        return $metadata->xmlCollection && $metadata->xmlCollectionInline;
345
    }
346 112
347
    private function isSkippableEmptyObject(?\DOMElement $node, PropertyMetadata $metadata): bool
348 112
    {
349 112
        return null === $node && !$metadata->xmlCollection && $metadata->skipWhenEmpty;
350 112
    }
351
352 1
    private function isSkippableCollection(PropertyMetadata $metadata): bool
353
    {
354 1
        return $metadata->xmlCollection && $metadata->xmlCollectionSkipWhenEmpty;
355 1
    }
356 1
357
    private function isElementEmpty(\DOMElement $element): bool
358 79
    {
359
        return !$element->hasChildNodes() && !$element->hasAttributes();
360 79
    }
361
362
    public function endVisitingObject(ClassMetadata $metadata, object $data, array $type): void
363 78
    {
364
        $this->objectMetadataStack->pop();
365 78
    }
366
367
    /**
368 121
     * {@inheritdoc}
369
     */
370 121
    public function getResult($node)
371
    {
372 121
        if (null === $this->document->documentElement) {
373
            if ($node instanceof \DOMElement) {
374
                $this->document->appendChild($node);
375
            } else {
376
                $this->createRoot();
377
                if ($node) {
378
                    $this->document->documentElement->appendChild($node);
379
                }
380
            }
381
        }
382 2
383
        if ($this->nullWasVisited) {
384 2
            $this->document->documentElement->setAttributeNS(
385
                'http://www.w3.org/2000/xmlns/',
386
                'xmlns:xsi',
387
                'http://www.w3.org/2001/XMLSchema-instance'
388
            );
389
        }
390
        return $this->document->saveXML();
391
    }
392
393 80
    public function getCurrentNode(): ?\DOMNode
394
    {
395 80
        return $this->currentNode;
396 11
    }
397 11
398 11
    public function getCurrentMetadata(): ?PropertyMetadata
399 7
    {
400 5
        return $this->currentMetadata;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->currentMetadata could return the type JMS\Serializer\Metadata\ClassMetadata which is incompatible with the type-hinted return JMS\Serializer\Metadata\PropertyMetadata|null. Consider adding an additional type-check to rule them out.
Loading history...
401
    }
402 11
403
    public function getDocument(): \DOMDocument
404 80
    {
405
        if (null === $this->document) {
406 79
            $this->document = $this->createDocument();
0 ignored issues
show
Bug introduced by
The call to JMS\Serializer\XmlSerial...sitor::createDocument() has too few arguments starting with formatOutput. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

406
            /** @scrutinizer ignore-call */ 
407
            $this->document = $this->createDocument();

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
407
        }
408 79
        return $this->document;
409 73
    }
410
411 10
    public function setCurrentMetadata(PropertyMetadata $metadata): void
412 6
    {
413
        $this->metadataStack->push($this->currentMetadata);
414 9
        $this->currentMetadata = $metadata;
415 3
    }
416
417 9
    public function setCurrentNode(\DOMNode $node): void
418
    {
419
        $this->stack->push($this->currentNode);
420 15
        $this->currentNode = $node;
421
    }
422 15
423 5
    public function setCurrentAndRootNode(\DOMNode $node): void
424 2
    {
425
        $this->setCurrentNode($node);
426 5
        $this->document->appendChild($node);
427
    }
428 12
429
    public function revertCurrentNode(): ?\DOMNode
430 15
    {
431
        return $this->currentNode = $this->stack->pop();
432 68
    }
433
434 68
    public function revertCurrentMetadata(): ?PropertyMetadata
435
    {
436
        return $this->currentMetadata = $this->metadataStack->pop();
437
    }
438
439
    /**
440
     * {@inheritdoc}
441
     */
442
    public function prepare($data)
443
    {
444
        $this->nullWasVisited = false;
445
446
        return $data;
447
    }
448
449
    /**
450
     * Checks that the name is a valid XML element name.
451
     */
452
    private function isElementNameValid(string $name): bool
453
    {
454
        return $name && false === strpos($name, ' ') && preg_match('#^[\pL_][\pL0-9._-]*$#ui', $name);
455
    }
456
457
    /**
458
     * Adds namespace attributes to the XML root element
459
     */
460
    private function addNamespaceAttributes(ClassMetadata $metadata, \DOMElement $element): void
461
    {
462
        foreach ($metadata->xmlNamespaces as $prefix => $uri) {
463
            $attribute = 'xmlns';
464
            if ('' !== $prefix) {
465
                $attribute .= ':' . $prefix;
466
            } elseif ($element->namespaceURI === $uri) {
467
                continue;
468
            }
469
            $element->setAttributeNS('http://www.w3.org/2000/xmlns/', $attribute, $uri);
470
        }
471
    }
472
473
    private function createElement(string $tagName, ?string $namespace = null): \DOMElement
474
    {
475
        // See #1087 - element must be like: <element xmlns="" /> - https://www.w3.org/TR/REC-xml-names/#iri-use
476
        // Use of an empty string in a namespace declaration turns it into an "undeclaration".
477
        if ('' === $namespace) {
478
            // If we have a default namespace, we need to create namespaced.
479
            if ($this->parentHasNonEmptyDefaultNs()) {
480
                return $this->document->createElementNS($namespace, $tagName);
481
            }
482
            return $this->document->createElement($tagName);
483
        }
484
        if (null === $namespace) {
485
            return $this->document->createElement($tagName);
486
        }
487
        if ($this->currentNode->isDefaultNamespace($namespace)) {
488
            return $this->document->createElementNS($namespace, $tagName);
489
        }
490
        if (!($prefix = $this->currentNode->lookupPrefix($namespace)) && !($prefix = $this->document->lookupPrefix($namespace))) {
491
            $prefix = 'ns-' . substr(sha1($namespace), 0, 8);
492
        }
493
        return $this->document->createElementNS($namespace, $prefix . ':' . $tagName);
494
    }
495
496
    private function setAttributeOnNode(\DOMElement $node, string $name, string $value, ?string $namespace = null): void
497
    {
498
        if (null !== $namespace) {
499
            if (!$prefix = $node->lookupPrefix($namespace)) {
500
                $prefix = 'ns-' . substr(sha1($namespace), 0, 8);
501
            }
502
            $node->setAttributeNS($namespace, $prefix . ':' . $name, $value);
503
        } else {
504
            $node->setAttribute($name, $value);
505
        }
506
    }
507
508
    private function getClassDefaultNamespace(ClassMetadata $metadata): ?string
509
    {
510
        return $metadata->xmlNamespaces[''] ?? null;
511
    }
512
513
    private function parentHasNonEmptyDefaultNs(): bool
514
    {
515
        return null !== ($uri = $this->currentNode->lookupNamespaceUri(null)) && ('' !== $uri);
516
    }
517
}
518