Completed
Pull Request — master (#925)
by Asmir
02:40
created

XmlSerializationVisitor::visitArray()   F

Complexity

Conditions 15
Paths 6272

Size

Total Lines 32
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 15

Importance

Changes 0
Metric Value
cc 15
eloc 19
nc 6272
nop 2
dl 0
loc 32
ccs 19
cts 19
cp 1
crap 15
rs 2.7451
c 0
b 0
f 0

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
/*
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
        bool $formatOutput = true,
53
        string $defaultEncoding = 'UTF-8',
54
        string $defaultVersion = '1.0',
55
        string $defaultRootName = 'result',
56
        string $defaultRootNamespace = null
57
    ) {
58 116
        $this->objectMetadataStack = new \SplStack;
59 116
        $this->stack = new \SplStack;
60 116
        $this->metadataStack = new \SplStack;
61
62 116
        $this->currentNode = null;
63 116
        $this->nullWasVisited = false;
64
65 116
        $this->document = $this->createDocument($formatOutput, $defaultVersion, $defaultEncoding);
66
67 116
        $this->defaultRootName = $defaultRootName;
68 116
        $this->defaultRootNamespace = $defaultRootNamespace;
69 116
    }
70
71 116
    private function createDocument(bool $formatOutput, string $defaultVersion, string $defaultEncoding): \DOMDocument
72
    {
73 116
        $document = new \DOMDocument($defaultVersion, $defaultEncoding);
74 116
        $document->formatOutput = $formatOutput;
75
76 116
        return $document;
77
    }
78
79 106
    public function createRoot(ClassMetadata $metadata = null, $rootName = null, $rootNamespace = null)
80
    {
81 106
        if ($metadata !== null && !empty($metadata->xmlRootName)) {
82 20
            $rootName = $metadata->xmlRootName;
83 20
            $rootNamespace = $metadata->xmlRootNamespace ?: $this->getClassDefaultNamespace($metadata);
84
        } else {
85 86
            $rootName = $rootName ?: $this->defaultRootName;
86 86
            $rootNamespace = $rootNamespace ?: $this->defaultRootNamespace;
87
        }
88
89 106
        $document = $this->getDocument();
90 106
        if ($rootNamespace) {
91 8
            $rootNode = $document->createElementNS($rootNamespace, $rootName);
92
        } else {
93 98
            $rootNode = $document->createElement($rootName);
94
        }
95 106
        $document->appendChild($rootNode);
96 106
        $this->setCurrentNode($rootNode);
97
98 106
        return $rootNode;
99
    }
100
101 9
    public function visitNull($data, array $type)
102
    {
103 9
        $node = $this->document->createAttribute('xsi:nil');
104 9
        $node->value = 'true';
105 9
        $this->nullWasVisited = true;
106
107 9
        return $node;
108
    }
109
110 76
    public function visitString(string $data, array $type)
111
    {
112 76
        $doCData = null !== $this->currentMetadata ? $this->currentMetadata->xmlElementCData : true;
113
114 76
        return $doCData ? $this->document->createCDATASection($data) : $this->document->createTextNode((string)$data);
115
    }
116
117 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

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

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