Completed
Pull Request — master (#1148)
by Alexander
12:41
created

XmlSerializationVisitor::visitStaticProperty()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1.125

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
nc 1
nop 2
dl 0
loc 3
ccs 1
cts 2
cp 0.5
crap 1.125
rs 10
c 1
b 0
f 0
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