Completed
Pull Request — master (#1148)
by Alexander
12:41
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
    public function visitStaticProperty(string $name, $v): void
335
    {
336
        $this->visitProperty(new StaticPropertyMetadata('', $name, $v), $v);
337 113
    }
338
339
    private function isInLineCollection(PropertyMetadata $metadata): bool
340 78
    {
341
        return $metadata->xmlCollection && $metadata->xmlCollectionInline;
342 78
    }
343 78
344 78
    private function isSkippableEmptyObject(?\DOMElement $node, PropertyMetadata $metadata): bool
345
    {
346 112
        return null === $node && !$metadata->xmlCollection && $metadata->skipWhenEmpty;
347
    }
348 112
349 112
    private function isSkippableCollection(PropertyMetadata $metadata): bool
350 112
    {
351
        return $metadata->xmlCollection && $metadata->xmlCollectionSkipWhenEmpty;
352 1
    }
353
354 1
    private function isElementEmpty(\DOMElement $element): bool
355 1
    {
356 1
        return !$element->hasChildNodes() && !$element->hasAttributes();
357
    }
358 79
359
    public function endVisitingObject(ClassMetadata $metadata, object $data, array $type): void
360 79
    {
361
        $this->objectMetadataStack->pop();
362
    }
363 78
364
    /**
365 78
     * {@inheritdoc}
366
     */
367
    public function getResult($node)
368 121
    {
369
        if (null === $this->document->documentElement) {
370 121
            if ($node instanceof \DOMElement) {
371
                $this->document->appendChild($node);
372 121
            } else {
373
                $this->createRoot();
374
                if ($node) {
375
                    $this->document->documentElement->appendChild($node);
376
                }
377
            }
378
        }
379
380
        if ($this->nullWasVisited) {
381
            $this->document->documentElement->setAttributeNS(
382 2
                'http://www.w3.org/2000/xmlns/',
383
                'xmlns:xsi',
384 2
                'http://www.w3.org/2001/XMLSchema-instance'
385
            );
386
        }
387
        return $this->document->saveXML();
388
    }
389
390
    public function getCurrentNode(): ?\DOMNode
391
    {
392
        return $this->currentNode;
393 80
    }
394
395 80
    public function getCurrentMetadata(): ?PropertyMetadata
396 11
    {
397 11
        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...
398 11
    }
399 7
400 5
    public function getDocument(): \DOMDocument
401
    {
402 11
        if (null === $this->document) {
403
            $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

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