Completed
Pull Request — master (#923)
by Asmir
02:54
created

XmlSerializationVisitor::isSkippableEmptyObject()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 1
nc 3
nop 2
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 3
rs 10
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
28
/**
29
 * XmlSerializationVisitor.
30
 *
31
 * @author Johannes M. Schmitt <[email protected]>
32
 */
33
class XmlSerializationVisitor extends AbstractVisitor implements SerializationVisitorInterface
34
{
35
    /**
36
     * @var \DOMDocument
37
     */
38
    private $document;
39
40
    private $defaultRootName = 'result';
41
    private $defaultRootNamespace;
42
43
    private $stack;
44
    private $metadataStack;
45
    private $currentNode;
46
    private $currentMetadata;
47
    private $hasValue;
48
    private $nullWasVisited;
49
    private $objectMetadataStack;
50
51 116
    public function __construct(
52
        GraphNavigatorInterface $navigator,
53
        SerializationContext $context,
54
        bool $formatOutput = true,
55
        string $defaultEncoding = 'UTF-8',
56
        string $defaultVersion = '1.0',
57
        string $defaultRootName = 'result',
58
        string $defaultRootNamespace = null
59
    ) {
60 116
        parent::__construct($navigator, $context);
61
62 116
        $this->objectMetadataStack = new \SplStack;
63 116
        $this->stack = new \SplStack;
64 116
        $this->metadataStack = new \SplStack;
65
66 116
        $this->currentNode = null;
67 116
        $this->nullWasVisited = false;
68
69 116
        $this->document = $this->createDocument($formatOutput, $defaultVersion, $defaultEncoding);
70
71 116
        $this->defaultRootName = $defaultRootName;
72 116
        $this->defaultRootNamespace = $defaultRootNamespace;
73 116
    }
74
75 116
    private function createDocument(bool $formatOutput, string $defaultVersion, string $defaultEncoding): \DOMDocument
76
    {
77 116
        $document = new \DOMDocument($defaultVersion, $defaultEncoding);
78 116
        $document->formatOutput = $formatOutput;
79
80 116
        return $document;
81
    }
82
83 106
    public function createRoot(ClassMetadata $metadata = null, $rootName = null, $rootNamespace = null)
84
    {
85 106
        if ($metadata !== null && !empty($metadata->xmlRootName)) {
86 20
            $rootName = $metadata->xmlRootName;
87 20
            $rootNamespace = $metadata->xmlRootNamespace ?: $this->getClassDefaultNamespace($metadata);
88
        } else {
89 86
            $rootName = $rootName ?: $this->defaultRootName;
90 86
            $rootNamespace = $rootNamespace ?: $this->defaultRootNamespace;
91
        }
92
93 106
        $document = $this->getDocument();
94 106
        if ($rootNamespace) {
95 8
            $rootNode = $document->createElementNS($rootNamespace, $rootName);
96
        } else {
97 98
            $rootNode = $document->createElement($rootName);
98
        }
99 106
        $document->appendChild($rootNode);
100 106
        $this->setCurrentNode($rootNode);
101
102 106
        return $rootNode;
103
    }
104
105 9
    public function visitNull($data, array $type)
106
    {
107 9
        $node = $this->document->createAttribute('xsi:nil');
108 9
        $node->value = 'true';
109 9
        $this->nullWasVisited = true;
110
111 9
        return $node;
112
    }
113
114 76
    public function visitString(string $data, array $type)
115
    {
116 76
        $doCData = null !== $this->currentMetadata ? $this->currentMetadata->xmlElementCData : true;
117
118 76
        return $doCData ? $this->document->createCDATASection($data) : $this->document->createTextNode((string)$data);
119
    }
120
121 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

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

294
    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...
295
    {
296 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

296
        return $this->context->/** @scrutinizer ignore-call */ isVisiting($v);
Loading history...
297
    }
298
299 6
    private function isSkippableEmptyObject($node, PropertyMetadata $metadata)
300
    {
301 6
        return $node === null && !$metadata->xmlCollection && $metadata->skipWhenEmpty;
302
    }
303
304 7
    private function isSkippableCollection(PropertyMetadata $metadata)
305
    {
306 7
        return $metadata->xmlCollection && $metadata->xmlCollectionSkipWhenEmpty;
307
    }
308
309 66
    private function isElementEmpty(\DOMElement $element)
310
    {
311 66
        return !$element->hasChildNodes() && !$element->hasAttributes();
312
    }
313
314 73
    public function endVisitingObject(ClassMetadata $metadata, object $data, array $type)
315
    {
316 73
        $this->objectMetadataStack->pop();
317 73
    }
318
319 105
    public function getResult($node)
320
    {
321 105
        if ($this->document->documentElement === null) {
322 21
            if ($node instanceof \DOMElement) {
323 1
                $this->document->appendChild($node);
324
            } else {
325 20
                $this->createRoot();
326 20
                if ($node) {
327 20
                    $this->document->documentElement->appendChild($node);
328
                }
329
            }
330
        }
331
332 105
        if ($this->nullWasVisited) {
333 9
            $this->document->documentElement->setAttributeNS(
334 9
                'http://www.w3.org/2000/xmlns/',
335 9
                'xmlns:xsi',
336 9
                'http://www.w3.org/2001/XMLSchema-instance'
337
            );
338
        }
339 105
        return $this->document->saveXML();
340
    }
341
342 2
    public function getCurrentNode()
343
    {
344 2
        return $this->currentNode;
345
    }
346
347
    public function getCurrentMetadata()
348
    {
349
        return $this->currentMetadata;
350
    }
351
352 108
    public function getDocument()
353
    {
354 108
        if (null === $this->document) {
355
            $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

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