Completed
Push — master ( 6b2ea7...c30cd0 )
by Asmir
06:14 queued 03:19
created

XmlSerializationVisitor::prepare()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 5
ccs 3
cts 3
cp 1
crap 1
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
    private $defaultRootPrefix;
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
        string $defaultRootPrefix = null
59
    ) {
60 118
        $this->objectMetadataStack = new \SplStack;
61 118
        $this->stack = new \SplStack;
62 118
        $this->metadataStack = new \SplStack;
63
64 118
        $this->currentNode = null;
65 118
        $this->nullWasVisited = false;
66
67 118
        $this->document = $this->createDocument($formatOutput, $defaultVersion, $defaultEncoding);
68
69 118
        $this->defaultRootName = $defaultRootName;
70 118
        $this->defaultRootNamespace = $defaultRootNamespace;
71 118
        $this->defaultRootPrefix = $defaultRootPrefix;
72 118
    }
73
74 118
    private function createDocument(bool $formatOutput, string $defaultVersion, string $defaultEncoding): \DOMDocument
75
    {
76 118
        $document = new \DOMDocument($defaultVersion, $defaultEncoding);
77 118
        $document->formatOutput = $formatOutput;
78
79 118
        return $document;
80
    }
81
82 108
    public function createRoot(ClassMetadata $metadata = null, ?string $rootName = null, ?string $rootNamespace = null, ?string $rootPrefix = null)
83
    {
84 108
        if ($metadata !== null && !empty($metadata->xmlRootName)) {
85 20
            $rootPrefix = $metadata->xmlRootPrefix;
86 20
            $rootName = $metadata->xmlRootName;
87 20
            $rootNamespace = $metadata->xmlRootNamespace ?: $this->getClassDefaultNamespace($metadata);
88
        } else {
89 88
            $rootName = $rootName ?: ($this->defaultRootName);
90 88
            $rootNamespace = $rootNamespace ?: $this->defaultRootNamespace;
91 88
            $rootPrefix = $rootPrefix ?: $this->defaultRootPrefix;
92
        }
93
94 108
        $document = $this->getDocument();
95 108
        if ($rootNamespace) {
96 8
            $rootNode = $document->createElementNS($rootNamespace, ($rootPrefix !== null ? ($rootPrefix . ':') : '') . $rootName);
97
        } else {
98 100
            $rootNode = $document->createElement($rootName);
99
        }
100 108
        $document->appendChild($rootNode);
101 108
        $this->setCurrentNode($rootNode);
102
103 108
        return $rootNode;
104
    }
105
106 9
    public function visitNull($data, array $type)
107
    {
108 9
        $node = $this->document->createAttribute('xsi:nil');
109 9
        $node->value = 'true';
110 9
        $this->nullWasVisited = true;
111
112 9
        return $node;
113
    }
114
115 77
    public function visitString(string $data, array $type)
116
    {
117 77
        $doCData = null !== $this->currentMetadata ? $this->currentMetadata->xmlElementCData : true;
118
119 77
        return $doCData ? $this->document->createCDATASection($data) : $this->document->createTextNode((string)$data);
120
    }
121
122 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

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

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