Passed
Branch master (bf85d9)
by Johannes
05:40
created

XmlSerializationVisitor::createRoot()   B

Complexity

Conditions 7
Paths 4

Size

Total Lines 20
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 7

Importance

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

127
    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...
128
    {
129 2
        return $this->document->createTextNode((string)$data);
130
    }
131
132 5
    public function visitBoolean(bool $data, array $type)
133
    {
134 5
        return $this->document->createTextNode($data ? 'true' : 'false');
135
    }
136
137 20
    public function visitInteger(int $data, array $type)
138
    {
139 20
        return $this->document->createTextNode((string)$data);
140
    }
141
142 9
    public function visitDouble(float $data, array $type)
143
    {
144 9
        if (floor($data) === $data) {
145 4
            return $this->document->createTextNode($data.".0");
146
        } else {
147 6
            return $this->document->createTextNode((string)$data);
148
        }
149
    }
150
151 31
    public function visitArray(array $data, array $type)
152
    {
153 31
        if ($this->currentNode === null) {
154 11
            $this->createRoot();
155
        }
156
157 31
        $entryName = (null !== $this->currentMetadata && null !== $this->currentMetadata->xmlEntryName) ? $this->currentMetadata->xmlEntryName : 'entry';
158 31
        $keyAttributeName = (null !== $this->currentMetadata && null !== $this->currentMetadata->xmlKeyAttribute) ? $this->currentMetadata->xmlKeyAttribute : null;
159 31
        $namespace = (null !== $this->currentMetadata && null !== $this->currentMetadata->xmlEntryNamespace) ? $this->currentMetadata->xmlEntryNamespace : null;
160
161 31
        $elType = $this->getElementType($type);
162 31
        foreach ($data as $k => $v) {
163
164 30
            if (null === $v && $this->shouldSerializeNull !== true) {
165 1
                continue;
166
            }
167
168 30
            $tagName = (null !== $this->currentMetadata && $this->currentMetadata->xmlKeyValuePairs && $this->isElementNameValid($k)) ? $k : $entryName;
169
170 30
            $entryNode = $this->createElement($tagName, $namespace);
171 30
            $this->currentNode->appendChild($entryNode);
172 30
            $this->setCurrentNode($entryNode);
173
174 30
            if (null !== $keyAttributeName) {
175 7
                $entryNode->setAttribute($keyAttributeName, (string)$k);
176
            }
177
178
            try {
179 30
                if (null !== $node = $this->navigator->accept($v, $elType, $this->context)) {
180 27
                    $this->currentNode->appendChild($node);
181
                }
182 4
            } catch (NotAcceptableException $e) {
183 4
                $this->currentNode->parentNode->removeChild($this->currentNode);
184
            }
185
186 30
            $this->revertCurrentNode();
187
        }
188 31
    }
189
190 74
    public function startVisitingObject(ClassMetadata $metadata, object $data, array $type): void
191
    {
192 74
        $this->objectMetadataStack->push($metadata);
193
194 74
        if ($this->currentNode === null) {
195 72
            $this->createRoot($metadata);
196
        }
197
198 74
        $this->addNamespaceAttributes($metadata, $this->currentNode);
199
200 74
        $this->hasValue = false;
201 74
    }
202
203 74
    public function visitProperty(PropertyMetadata $metadata, $object): void
204
    {
205 74
        $v = $this->accessor->getValue($object, $metadata);
206
207 73
        if (null === $v && $this->shouldSerializeNull !== true) {
208 9
            return;
209
        }
210
211 73
        if ($metadata->xmlAttribute) {
212 13
            $this->setCurrentMetadata($metadata);
213 13
            $node = $this->navigator->accept($v, $metadata->type, $this->context);
214 13
            $this->revertCurrentMetadata();
215
216 13
            if (!$node instanceof \DOMCharacterData) {
217
                throw new RuntimeException(sprintf('Unsupported value for XML attribute for %s. Expected character data, but got %s.', $metadata->name, json_encode($v)));
218
            }
219
220 13
            $this->setAttributeOnNode($this->currentNode, $metadata->serializedName, $node->nodeValue, $metadata->xmlNamespace);
221
222 13
            return;
223
        }
224
225 71
        if (($metadata->xmlValue && $this->currentNode->childNodes->length > 0)
226 71
            || (!$metadata->xmlValue && $this->hasValue)
227
        ) {
228 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));
229
        }
230
231 71
        if ($metadata->xmlValue) {
232 9
            $this->hasValue = true;
233
234 9
            $this->setCurrentMetadata($metadata);
235 9
            $node = $this->navigator->accept($v, $metadata->type, $this->context);
236 9
            $this->revertCurrentMetadata();
237
238 9
            if (!$node instanceof \DOMCharacterData) {
239
                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)));
240
            }
241
242 9
            $this->currentNode->appendChild($node);
243
244 9
            return;
245
        }
246
247 67
        if ($metadata->xmlAttributeMap) {
248 2
            if (!\is_array($v)) {
249 1
                throw new RuntimeException(sprintf('Unsupported value type for XML attribute map. Expected array but got %s.', \gettype($v)));
250
            }
251
252 1
            foreach ($v as $key => $value) {
253 1
                $this->setCurrentMetadata($metadata);
254 1
                $node = $this->navigator->accept($value, null, $this->context);
255 1
                $this->revertCurrentMetadata();
256
257 1
                if (!$node instanceof \DOMCharacterData) {
258
                    throw new RuntimeException(sprintf('Unsupported value for a XML attribute map value. Expected character data, but got %s.', json_encode($v)));
259
                }
260
261 1
                $this->setAttributeOnNode($this->currentNode, $key, $node->nodeValue, $metadata->xmlNamespace);
262
            }
263
264 1
            return;
265
        }
266
267 65
        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...
268
269 64
            $namespace = null !== $metadata->xmlNamespace
270 9
                ? $metadata->xmlNamespace
271 64
                : $this->getClassDefaultNamespace($this->objectMetadataStack->top());
272
273 64
            $element = $this->createElement($metadata->serializedName, $namespace);
274 64
            $this->currentNode->appendChild($element);
275 64
            $this->setCurrentNode($element);
276
        }
277
278 65
        $this->setCurrentMetadata($metadata);
279
280
        try {
281 65
            if (null !== $node = $this->navigator->accept($v, $metadata->type, $this->context)) {
282 65
                $this->currentNode->appendChild($node);
283
            }
284 2
        } catch (NotAcceptableException $e) {
285 2
            $this->currentNode->parentNode->removeChild($this->currentNode);
286 2
            $this->revertCurrentMetadata();
287 2
            $this->revertCurrentNode();
288 2
            $this->hasValue = false;
289 2
            return;
290
        }
291
292 65
        $this->revertCurrentMetadata();
293
294 65
        if ($addEnclosingElement) {
295 64
            $this->revertCurrentNode();
296
297 64
            if ($this->isElementEmpty($element) && ($v === null || $this->isSkippableCollection($metadata) || $this->isSkippableEmptyObject($node, $metadata) || $this->isCircularRef($this->context, $v))) {
298 3
                $this->currentNode->removeChild($element);
299
            }
300
        }
301
302 65
        $this->hasValue = false;
303 65
    }
304
305 65
    private function isInLineCollection(PropertyMetadata $metadata)
306
    {
307 65
        return $metadata->xmlCollection && $metadata->xmlCollectionInline;
308
    }
309
310 5
    private function isCircularRef(SerializationContext $context, $v)
0 ignored issues
show
Unused Code introduced by
The parameter $context 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

310
    private function isCircularRef(/** @scrutinizer ignore-unused */ SerializationContext $context, $v)

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...
311
    {
312 5
        return $this->context->isVisiting($v);
0 ignored issues
show
Bug introduced by
The method isVisiting() does not exist on JMS\Serializer\Context. It seems like you code against a sub-type of JMS\Serializer\Context such as JMS\Serializer\SerializationContext. ( Ignorable by Annotation )

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

312
        return $this->context->/** @scrutinizer ignore-call */ isVisiting($v);
Loading history...
313
    }
314
315 6
    private function isSkippableEmptyObject($node, PropertyMetadata $metadata)
316
    {
317 6
        return $node === null && !$metadata->xmlCollection && $metadata->skipWhenEmpty;
318
    }
319
320 7
    private function isSkippableCollection(PropertyMetadata $metadata)
321
    {
322 7
        return $metadata->xmlCollection && $metadata->xmlCollectionSkipWhenEmpty;
323
    }
324
325 64
    private function isElementEmpty(\DOMElement $element)
326
    {
327 64
        return !$element->hasChildNodes() && !$element->hasAttributes();
328
    }
329
330 71
    public function endVisitingObject(ClassMetadata $metadata, object $data, array $type)
331
    {
332 71
        $this->objectMetadataStack->pop();
333 71
    }
334
335 103
    public function getResult($node)
336
    {
337 103
        if ($this->document->documentElement === null) {
338 21
            if ($node instanceof \DOMElement) {
339 1
                $this->document->appendChild($node);
340
            } else {
341 20
                $this->createRoot();
342 20
                if ($node) {
343 20
                    $this->document->documentElement->appendChild($node);
344
                }
345
            }
346
        }
347
348 103
        if ($this->nullWasVisited) {
349 9
            $this->document->documentElement->setAttributeNS(
350 9
                'http://www.w3.org/2000/xmlns/',
351 9
                'xmlns:xsi',
352 9
                'http://www.w3.org/2001/XMLSchema-instance'
353
            );
354
        }
355 103
        return $this->document->saveXML();
356
    }
357
358 2
    public function getCurrentNode()
359
    {
360 2
        return $this->currentNode;
361
    }
362
363
    public function getCurrentMetadata()
364
    {
365
        return $this->currentMetadata;
366
    }
367
368 106
    public function getDocument()
369
    {
370 106
        if (null === $this->document) {
371
            $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

371
            /** @scrutinizer ignore-call */ 
372
            $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...
372
        }
373 106
        return $this->document;
374
    }
375
376 72
    public function setCurrentMetadata(PropertyMetadata $metadata)
377
    {
378 72
        $this->metadataStack->push($this->currentMetadata);
379 72
        $this->currentMetadata = $metadata;
380 72
    }
381
382 105
    public function setCurrentNode(\DOMNode $node)
383
    {
384 105
        $this->stack->push($this->currentNode);
385 105
        $this->currentNode = $node;
386 105
    }
387
388 1
    public function setCurrentAndRootNode(\DOMNode $node)
389
    {
390 1
        $this->setCurrentNode($node);
391 1
        $this->document->appendChild($node);
392 1
    }
393
394 74
    public function revertCurrentNode()
395
    {
396 74
        return $this->currentNode = $this->stack->pop();
397
    }
398
399 72
    public function revertCurrentMetadata()
400
    {
401 72
        return $this->currentMetadata = $this->metadataStack->pop();
402
    }
403
404 108
    public function prepare($data)
405
    {
406 108
        $this->nullWasVisited = false;
407
408 108
        return $data;
409
    }
410
411
    /**
412
     * Checks that the name is a valid XML element name.
413
     *
414
     * @param string $name
415
     *
416
     * @return boolean
417
     */
418 2
    private function isElementNameValid($name)
419
    {
420 2
        return $name && false === strpos($name, ' ') && preg_match('#^[\pL_][\pL0-9._-]*$#ui', $name);
421
    }
422
423
    /**
424
     * Adds namespace attributes to the XML root element
425
     *
426
     * @param \JMS\Serializer\Metadata\ClassMetadata $metadata
427
     * @param \DOMElement $element
428
     */
429 74
    private function addNamespaceAttributes(ClassMetadata $metadata, \DOMElement $element)
430
    {
431 74
        foreach ($metadata->xmlNamespaces as $prefix => $uri) {
432 10
            $attribute = 'xmlns';
433 10
            if ($prefix !== '') {
434 10
                $attribute .= ':' . $prefix;
435 7
            } elseif ($element->namespaceURI === $uri) {
436 5
                continue;
437
            }
438 10
            $element->setAttributeNS('http://www.w3.org/2000/xmlns/', $attribute, $uri);
439
        }
440 74
    }
441
442 74
    private function createElement($tagName, $namespace = null)
443
    {
444 74
        if (null === $namespace) {
445 68
            return $this->document->createElement($tagName);
446
        }
447 10
        if ($this->currentNode->isDefaultNamespace($namespace)) {
448 6
            return $this->document->createElementNS($namespace, $tagName);
449
        }
450 9
        if (!($prefix = $this->currentNode->lookupPrefix($namespace)) && !($prefix = $this->document->lookupPrefix($namespace))) {
451 3
            $prefix = 'ns-' . substr(sha1($namespace), 0, 8);
452
        }
453 9
        return $this->document->createElementNS($namespace, $prefix . ':' . $tagName);
454
    }
455
456 14
    private function setAttributeOnNode(\DOMElement $node, $name, $value, $namespace = null)
457
    {
458 14
        if (null !== $namespace) {
459 4
            if (!$prefix = $node->lookupPrefix($namespace)) {
460 2
                $prefix = 'ns-' . substr(sha1($namespace), 0, 8);
461
            }
462 4
            $node->setAttributeNS($namespace, $prefix . ':' . $name, $value);
463
        } else {
464 12
            $node->setAttribute($name, $value);
465
        }
466 14
    }
467
468 63
    private function getClassDefaultNamespace(ClassMetadata $metadata)
469
    {
470 63
        return (isset($metadata->xmlNamespaces['']) ? $metadata->xmlNamespaces[''] : null);
471
    }
472
}
473