Passed
Push — master ( 87567e...de6cb3 )
by Asmir
25:58 queued 23:08
created

XmlSerializationVisitor::getDocument()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2.0625

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 0
dl 0
loc 6
ccs 3
cts 4
cp 0.75
crap 2.0625
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * Copyright 2016 Johannes M. Schmitt <[email protected]>
7
 *
8
 * Licensed under the Apache License, Version 2.0 (the "License");
9
 * you may not use this file except in compliance with the License.
10
 * You may obtain a copy of the License at
11
 *
12
 *     http://www.apache.org/licenses/LICENSE-2.0
13
 *
14
 * Unless required by applicable law or agreed to in writing, software
15
 * distributed under the License is distributed on an "AS IS" BASIS,
16
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
 * See the License for the specific language governing permissions and
18
 * limitations under the License.
19
 */
20
21
namespace JMS\Serializer;
22
23
use JMS\Serializer\Exception\NotAcceptableException;
24
use JMS\Serializer\Exception\RuntimeException;
25
use JMS\Serializer\Metadata\ClassMetadata;
26
use JMS\Serializer\Metadata\PropertyMetadata;
27
use JMS\Serializer\Visitor\SerializationVisitorInterface;
28
29
/**
30
 * XmlSerializationVisitor.
31
 *
32
 * @author Johannes M. Schmitt <[email protected]>
33
 */
34
final class XmlSerializationVisitor extends AbstractVisitor implements SerializationVisitorInterface
35
{
36
    /**
37
     * @var \DOMDocument
38
     */
39
    private $document;
40
41
    private $defaultRootName = 'result';
42
    private $defaultRootNamespace;
43
44
    private $stack;
45
    private $metadataStack;
46
    private $currentNode;
47
    private $currentMetadata;
48
    private $hasValue;
49
    private $nullWasVisited;
50
    private $objectMetadataStack;
51
52 118
    public function __construct(
53
        bool $formatOutput = true,
54
        string $defaultEncoding = 'UTF-8',
55
        string $defaultVersion = '1.0',
56
        string $defaultRootName = 'result',
57
        string $defaultRootNamespace = null
58
    ) {
59 118
        $this->objectMetadataStack = new \SplStack;
60 118
        $this->stack = new \SplStack;
61 118
        $this->metadataStack = new \SplStack;
62
63 118
        $this->currentNode = null;
64 118
        $this->nullWasVisited = false;
65
66 118
        $this->document = $this->createDocument($formatOutput, $defaultVersion, $defaultEncoding);
67
68 118
        $this->defaultRootName = $defaultRootName;
69 118
        $this->defaultRootNamespace = $defaultRootNamespace;
70 118
    }
71
72 118
    private function createDocument(bool $formatOutput, string $defaultVersion, string $defaultEncoding): \DOMDocument
73
    {
74 118
        $document = new \DOMDocument($defaultVersion, $defaultEncoding);
75 118
        $document->formatOutput = $formatOutput;
76
77 118
        return $document;
78
    }
79
80 108
    public function createRoot(ClassMetadata $metadata = null, $rootName = null, $rootNamespace = null)
81
    {
82 108
        if ($metadata !== null && !empty($metadata->xmlRootName)) {
83 20
            $rootName = $metadata->xmlRootName;
84 20
            $rootNamespace = $metadata->xmlRootNamespace ?: $this->getClassDefaultNamespace($metadata);
85
        } else {
86 88
            $rootName = $rootName ?: $this->defaultRootName;
87 88
            $rootNamespace = $rootNamespace ?: $this->defaultRootNamespace;
88
        }
89
90 108
        $document = $this->getDocument();
91 108
        if ($rootNamespace) {
92 8
            $rootNode = $document->createElementNS($rootNamespace, $rootName);
93
        } else {
94 100
            $rootNode = $document->createElement($rootName);
95
        }
96 108
        $document->appendChild($rootNode);
97 108
        $this->setCurrentNode($rootNode);
98
99 108
        return $rootNode;
100
    }
101
102 9
    public function visitNull($data, array $type)
103
    {
104 9
        $node = $this->document->createAttribute('xsi:nil');
105 9
        $node->value = 'true';
106 9
        $this->nullWasVisited = true;
107
108 9
        return $node;
109
    }
110
111 77
    public function visitString(string $data, array $type)
112
    {
113 77
        $doCData = null !== $this->currentMetadata ? $this->currentMetadata->xmlElementCData : true;
114
115 77
        return $doCData ? $this->document->createCDATASection($data) : $this->document->createTextNode((string)$data);
116
    }
117
118 2
    public function visitSimpleString($data, array $type)
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

118
    public function visitSimpleString($data, /** @scrutinizer ignore-unused */ array $type)

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...
119
    {
120 2
        return $this->document->createTextNode((string)$data);
121
    }
122
123 5
    public function visitBoolean(bool $data, array $type)
124
    {
125 5
        return $this->document->createTextNode($data ? 'true' : 'false');
126
    }
127
128 20
    public function visitInteger(int $data, array $type)
129
    {
130 20
        return $this->document->createTextNode((string)$data);
131
    }
132
133 9
    public function visitDouble(float $data, array $type)
134
    {
135 9
        if (floor($data) === $data) {
136 4
            return $this->document->createTextNode($data . ".0");
137
        } else {
138 6
            return $this->document->createTextNode((string)$data);
139
        }
140
    }
141
142 32
    public function visitArray(array $data, array $type)
143
    {
144 32
        if ($this->currentNode === null) {
145 11
            $this->createRoot();
146
        }
147
148 32
        $entryName = (null !== $this->currentMetadata && null !== $this->currentMetadata->xmlEntryName) ? $this->currentMetadata->xmlEntryName : 'entry';
149 32
        $keyAttributeName = (null !== $this->currentMetadata && null !== $this->currentMetadata->xmlKeyAttribute) ? $this->currentMetadata->xmlKeyAttribute : null;
150 32
        $namespace = (null !== $this->currentMetadata && null !== $this->currentMetadata->xmlEntryNamespace) ? $this->currentMetadata->xmlEntryNamespace : null;
151
152 32
        $elType = $this->getElementType($type);
153 32
        foreach ($data as $k => $v) {
154
155 31
            $tagName = (null !== $this->currentMetadata && $this->currentMetadata->xmlKeyValuePairs && $this->isElementNameValid((string)$k)) ? $k : $entryName;
156
157 31
            $entryNode = $this->createElement($tagName, $namespace);
158 31
            $this->currentNode->appendChild($entryNode);
159 31
            $this->setCurrentNode($entryNode);
160
161 31
            if (null !== $keyAttributeName) {
162 7
                $entryNode->setAttribute($keyAttributeName, (string)$k);
163
            }
164
165
            try {
166 31
                if (null !== $node = $this->navigator->accept($v, $elType)) {
167 28
                    $this->currentNode->appendChild($node);
168
                }
169 5
            } catch (NotAcceptableException $e) {
170 5
                $this->currentNode->parentNode->removeChild($this->currentNode);
171
            }
172
173 31
            $this->revertCurrentNode();
174
        }
175 32
    }
176
177 77
    public function startVisitingObject(ClassMetadata $metadata, object $data, array $type): void
178
    {
179 77
        $this->objectMetadataStack->push($metadata);
180
181 77
        if ($this->currentNode === null) {
182 75
            $this->createRoot($metadata);
183
        }
184
185 77
        $this->addNamespaceAttributes($metadata, $this->currentNode);
186
187 77
        $this->hasValue = false;
188 77
    }
189
190 76
    public function visitProperty(PropertyMetadata $metadata, $v): void
191
    {
192 76
        if ($metadata->xmlAttribute) {
193 14
            $this->setCurrentMetadata($metadata);
194 14
            $node = $this->navigator->accept($v, $metadata->type);
195 14
            $this->revertCurrentMetadata();
196
197 14
            if (!$node instanceof \DOMCharacterData) {
198
                throw new RuntimeException(sprintf('Unsupported value for XML attribute for %s. Expected character data, but got %s.', $metadata->name, json_encode($v)));
199
            }
200
201 14
            $this->setAttributeOnNode($this->currentNode, $metadata->serializedName, $node->nodeValue, $metadata->xmlNamespace);
202
203 14
            return;
204
        }
205
206 73
        if (($metadata->xmlValue && $this->currentNode->childNodes->length > 0)
207 73
            || (!$metadata->xmlValue && $this->hasValue)
208
        ) {
209 1
            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));
210
        }
211
212 73
        if ($metadata->xmlValue) {
213 9
            $this->hasValue = true;
214
215 9
            $this->setCurrentMetadata($metadata);
216 9
            $node = $this->navigator->accept($v, $metadata->type);
217 9
            $this->revertCurrentMetadata();
218
219 9
            if (!$node instanceof \DOMCharacterData) {
220
                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)));
221
            }
222
223 9
            $this->currentNode->appendChild($node);
224
225 9
            return;
226
        }
227
228 69
        if ($metadata->xmlAttributeMap) {
229 2
            if (!\is_array($v)) {
230 1
                throw new RuntimeException(sprintf('Unsupported value type for XML attribute map. Expected array but got %s.', \gettype($v)));
231
            }
232
233 1
            foreach ($v as $key => $value) {
234 1
                $this->setCurrentMetadata($metadata);
235 1
                $node = $this->navigator->accept($value, null);
236 1
                $this->revertCurrentMetadata();
237
238 1
                if (!$node instanceof \DOMCharacterData) {
239
                    throw new RuntimeException(sprintf('Unsupported value for a XML attribute map value. Expected character data, but got %s.', json_encode($v)));
240
                }
241
242 1
                $this->setAttributeOnNode($this->currentNode, $key, $node->nodeValue, $metadata->xmlNamespace);
243
            }
244
245 1
            return;
246
        }
247
248 67
        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...
249
250 66
            $namespace = null !== $metadata->xmlNamespace
251 9
                ? $metadata->xmlNamespace
252 66
                : $this->getClassDefaultNamespace($this->objectMetadataStack->top());
253
254 66
            $element = $this->createElement($metadata->serializedName, $namespace);
255 66
            $this->currentNode->appendChild($element);
256 66
            $this->setCurrentNode($element);
257
        }
258
259 67
        $this->setCurrentMetadata($metadata);
260
261
        try {
262 67
            if (null !== $node = $this->navigator->accept($v, $metadata->type)) {
263 67
                $this->currentNode->appendChild($node);
264
            }
265 2
        } catch (NotAcceptableException $e) {
266 2
            $this->currentNode->parentNode->removeChild($this->currentNode);
267 2
            $this->revertCurrentMetadata();
268 2
            $this->revertCurrentNode();
269 2
            $this->hasValue = false;
270 2
            return;
271
        }
272
273 67
        $this->revertCurrentMetadata();
274
275 67
        if ($addEnclosingElement) {
276 66
            $this->revertCurrentNode();
277
278 66
            if ($this->isElementEmpty($element) && ($v === null || $this->isSkippableCollection($metadata) || $this->isSkippableEmptyObject($node, $metadata))) {
279 3
                $this->currentNode->removeChild($element);
280
            }
281
        }
282
283 67
        $this->hasValue = false;
284 67
    }
285
286 67
    private function isInLineCollection(PropertyMetadata $metadata)
287
    {
288 67
        return $metadata->xmlCollection && $metadata->xmlCollectionInline;
289
    }
290
291 6
    private function isSkippableEmptyObject($node, PropertyMetadata $metadata)
292
    {
293 6
        return $node === null && !$metadata->xmlCollection && $metadata->skipWhenEmpty;
294
    }
295
296 7
    private function isSkippableCollection(PropertyMetadata $metadata)
297
    {
298 7
        return $metadata->xmlCollection && $metadata->xmlCollectionSkipWhenEmpty;
299
    }
300
301 66
    private function isElementEmpty(\DOMElement $element)
302
    {
303 66
        return !$element->hasChildNodes() && !$element->hasAttributes();
304
    }
305
306 74
    public function endVisitingObject(ClassMetadata $metadata, object $data, array $type)
307
    {
308 74
        $this->objectMetadataStack->pop();
309 74
    }
310
311 107
    public function getResult($node)
312
    {
313 107
        if ($this->document->documentElement === null) {
314 22
            if ($node instanceof \DOMElement) {
315 1
                $this->document->appendChild($node);
316
            } else {
317 21
                $this->createRoot();
318 21
                if ($node) {
319 20
                    $this->document->documentElement->appendChild($node);
320
                }
321
            }
322
        }
323
324 107
        if ($this->nullWasVisited) {
325 9
            $this->document->documentElement->setAttributeNS(
326 9
                'http://www.w3.org/2000/xmlns/',
327 9
                'xmlns:xsi',
328 9
                'http://www.w3.org/2001/XMLSchema-instance'
329
            );
330
        }
331 107
        return $this->document->saveXML();
332
    }
333
334 2
    public function getCurrentNode(): ?\DOMNode
335
    {
336 2
        return $this->currentNode;
337
    }
338
339
    public function getCurrentMetadata()
340
    {
341
        return $this->currentMetadata;
342
    }
343
344 110
    public function getDocument(): \DOMDocument
345
    {
346 110
        if (null === $this->document) {
347
            $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

347
            /** @scrutinizer ignore-call */ 
348
            $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...
348
        }
349 110
        return $this->document;
350
    }
351
352 75
    public function setCurrentMetadata(PropertyMetadata $metadata):void
353
    {
354 75
        $this->metadataStack->push($this->currentMetadata);
355 75
        $this->currentMetadata = $metadata;
356 75
    }
357
358 109
    public function setCurrentNode(\DOMNode $node):void
359
    {
360 109
        $this->stack->push($this->currentNode);
361 109
        $this->currentNode = $node;
362 109
    }
363
364 1
    public function setCurrentAndRootNode(\DOMNode $node):void
365
    {
366 1
        $this->setCurrentNode($node);
367 1
        $this->document->appendChild($node);
368 1
    }
369
370 76
    public function revertCurrentNode()
371
    {
372 76
        return $this->currentNode = $this->stack->pop();
373
    }
374
375 75
    public function revertCurrentMetadata()
376
    {
377 75
        return $this->currentMetadata = $this->metadataStack->pop();
378
    }
379
380 118
    public function prepare($data)
381
    {
382 118
        $this->nullWasVisited = false;
383
384 118
        return $data;
385
    }
386
387
    /**
388
     * Checks that the name is a valid XML element name.
389
     *
390
     * @param string $name
391
     *
392
     * @return boolean
393
     */
394 2
    private function isElementNameValid($name)
395
    {
396 2
        return $name && false === strpos($name, ' ') && preg_match('#^[\pL_][\pL0-9._-]*$#ui', $name);
397
    }
398
399
    /**
400
     * Adds namespace attributes to the XML root element
401
     *
402
     * @param \JMS\Serializer\Metadata\ClassMetadata $metadata
403
     * @param \DOMElement $element
404
     */
405 77
    private function addNamespaceAttributes(ClassMetadata $metadata, \DOMElement $element)
406
    {
407 77
        foreach ($metadata->xmlNamespaces as $prefix => $uri) {
408 11
            $attribute = 'xmlns';
409 11
            if ($prefix !== '') {
410 11
                $attribute .= ':' . $prefix;
411 7
            } elseif ($element->namespaceURI === $uri) {
412 5
                continue;
413
            }
414 11
            $element->setAttributeNS('http://www.w3.org/2000/xmlns/', $attribute, $uri);
415
        }
416 77
    }
417
418 76
    private function createElement($tagName, $namespace = null)
419
    {
420 76
        if (null === $namespace) {
421 70
            return $this->document->createElement($tagName);
422
        }
423 10
        if ($this->currentNode->isDefaultNamespace($namespace)) {
424 6
            return $this->document->createElementNS($namespace, $tagName);
425
        }
426 9
        if (!($prefix = $this->currentNode->lookupPrefix($namespace)) && !($prefix = $this->document->lookupPrefix($namespace))) {
427 3
            $prefix = 'ns-' . substr(sha1($namespace), 0, 8);
428
        }
429 9
        return $this->document->createElementNS($namespace, $prefix . ':' . $tagName);
430
    }
431
432 15
    private function setAttributeOnNode(\DOMElement $node, $name, $value, $namespace = null)
433
    {
434 15
        if (null !== $namespace) {
435 5
            if (!$prefix = $node->lookupPrefix($namespace)) {
436 2
                $prefix = 'ns-' . substr(sha1($namespace), 0, 8);
437
            }
438 5
            $node->setAttributeNS($namespace, $prefix . ':' . $name, $value);
439
        } else {
440 12
            $node->setAttribute($name, $value);
441
        }
442 15
    }
443
444 65
    private function getClassDefaultNamespace(ClassMetadata $metadata)
445
    {
446 65
        return (isset($metadata->xmlNamespaces['']) ? $metadata->xmlNamespaces[''] : null);
447
    }
448
}
449