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

XmlSerializationVisitor::__construct()   B

Complexity

Conditions 1
Paths 1

Size

Total Lines 24
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 10
nc 1
nop 8
dl 0
loc 24
ccs 11
cts 11
cp 1
crap 1
rs 8.9713
c 0
b 0
f 0

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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