Completed
Pull Request — master (#708)
by Asmir
11:37
created

XmlSerializationVisitor::visitNumeric()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 11
ccs 1
cts 1
cp 1
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 6
nc 2
nop 2
crap 2
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\RuntimeException;
23
use JMS\Serializer\Metadata\ClassMetadata;
24
use JMS\Serializer\Metadata\PropertyMetadata;
25
use JMS\Serializer\Naming\PropertyNamingStrategyInterface;
26
27
/**
28
 * XmlSerializationVisitor.
29
 *
30
 * @author Johannes M. Schmitt <[email protected]>
31
 */
32
class XmlSerializationVisitor extends AbstractVisitor
33
{
34
    public $document;
35
36
    private $navigator;
37
    private $defaultRootName = 'result';
38
    private $defaultRootNamespace;
39
    private $defaultVersion = '1.0';
40
    private $defaultEncoding = 'UTF-8';
41
    private $stack;
42
    private $metadataStack;
43
    private $currentNode;
44
    private $currentMetadata;
45
    private $hasValue;
46
    private $nullWasVisited;
47
    private $objectMetadataStack;
48
49
    /** @var boolean */
50
    private $formatOutput;
51
52 309
    public function __construct(PropertyNamingStrategyInterface $namingStrategy, AccessorStrategyInterface $accessorStrategy = null)
53
    {
54 309
        parent::__construct($namingStrategy, $accessorStrategy);
55 309
        $this->objectMetadataStack = new \SplStack;
56 309
        $this->formatOutput = true;
57 309
    }
58
59
    public function setDefaultRootName($name, $namespace = null)
60
    {
61
        $this->defaultRootName = $name;
62
        $this->defaultRootNamespace = $namespace;
63
    }
64
65
    /**
66
     * @return boolean
67
     */
68
    public function hasDefaultRootName()
69
    {
70
        return 'result' === $this->defaultRootName;
71
    }
72
73
    public function setDefaultVersion($version)
74
    {
75
        $this->defaultVersion = $version;
76
    }
77
78
    public function setDefaultEncoding($encoding)
79
    {
80
        $this->defaultEncoding = $encoding;
81
    }
82
83 100
    public function setNavigator(GraphNavigator $navigator)
84
    {
85 100
        $this->navigator = $navigator;
86 100
        $this->document = null;
87 100
        $this->stack = new \SplStack;
88 100
        $this->metadataStack = new \SplStack;
89 100
    }
90
91
    public function getNavigator()
92
    {
93
        return $this->navigator;
94
    }
95
96 9
    public function visitNull($data, array $type, Context $context)
97
    {
98 9
        if (null === $this->document) {
99 6
            $this->document = $this->createDocument(null, null, true);
100 6
            $node = $this->document->createAttribute('xsi:nil');
101 6
            $node->value = 'true';
102 6
            $this->currentNode->appendChild($node);
103
104 6
            $this->attachNullNamespace();
105
106 6
            return;
107
        }
108
109 3
        $node = $this->document->createAttribute('xsi:nil');
0 ignored issues
show
Bug introduced by
The method createAttribute cannot be called on $this->document (of type null).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
110 3
        $node->value = 'true';
111 3
        $this->attachNullNamespace();
112
113 3
        return $node;
114
    }
115
116 67
    public function visitString($data, array $type, Context $context)
117
    {
118
119 67
        if (null !== $this->currentMetadata) {
120 59
            $doCData = $this->currentMetadata->xmlElementCData;
121 59
        } else {
122 9
            $doCData = true;
123
        }
124
125 67
        if (null === $this->document) {
126 5
            $this->document = $this->createDocument(null, null, true);
127 5
            $this->currentNode->appendChild($doCData ? $this->document->createCDATASection($data) : $this->document->createTextNode((string) $data));
128
129 5
            return;
130
        }
131
132 62
        return $doCData ? $this->document->createCDATASection($data) : $this->document->createTextNode((string) $data);
0 ignored issues
show
Bug introduced by
The method createCDATASection cannot be called on $this->document (of type null).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
133
    }
134
135 2
    public function visitSimpleString($data, array $type, Context $context)
0 ignored issues
show
Unused Code introduced by
The parameter $type is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $context is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
136
    {
137 2
        if (null === $this->document) {
138 2
            $this->document = $this->createDocument(null, null, true);
139 2
            $this->currentNode->appendChild($this->document->createTextNode((string) $data));
140
141 2
            return;
142
        }
143
144
        return $this->document->createTextNode((string) $data);
145
    }
146
147 5
    public function visitBoolean($data, array $type, Context $context)
148
    {
149 5
        if (null === $this->document) {
150 2
            $this->document = $this->createDocument(null, null, true);
151 2
            $this->currentNode->appendChild($this->document->createTextNode($data ? 'true' : 'false'));
152
153 2
            return;
154
        }
155
156 3
        return $this->document->createTextNode($data ? 'true' : 'false');
157
    }
158
159 19
    public function visitInteger($data, array $type, Context $context)
160
    {
161 19
        return $this->visitNumeric($data, $type);
162
    }
163
164 9
    public function visitDouble($data, array $type, Context $context)
165
    {
166 9
        return $this->visitNumeric($data, $type);
167
    }
168
169 28
    public function visitArray($data, array $type, Context $context)
170
    {
171 28
        if (null === $this->document) {
172 9
            $this->document = $this->createDocument(null, null, true);
173 9
        }
174
175 28
        $entryName = (null !== $this->currentMetadata && null !== $this->currentMetadata->xmlEntryName) ? $this->currentMetadata->xmlEntryName : 'entry';
176 28
        $keyAttributeName = (null !== $this->currentMetadata && null !== $this->currentMetadata->xmlKeyAttribute) ? $this->currentMetadata->xmlKeyAttribute : null;
177 28
        $namespace = (null !== $this->currentMetadata && null !== $this->currentMetadata->xmlEntryNamespace) ? $this->currentMetadata->xmlEntryNamespace : null;
178
179 28
        foreach ($data as $k => $v) {
180
181 27
            if (null === $v && $context->shouldSerializeNull() !== true) {
182 1
                continue;
183
            }
184
185 27
            $tagName = (null !== $this->currentMetadata && $this->currentMetadata->xmlKeyValuePairs && $this->isElementNameValid($k)) ? $k : $entryName;
186
187 27
            $entryNode = $this->createElement($tagName, $namespace);
188 27
            $this->currentNode->appendChild($entryNode);
189 27
            $this->setCurrentNode($entryNode);
190
191 27
            if (null !== $keyAttributeName) {
192 7
                $entryNode->setAttribute($keyAttributeName, (string) $k);
193 7
            }
194
195 27
            if (null !== $node = $this->navigator->accept($v, $this->getElementType($type), $context)) {
196 18
                $this->currentNode->appendChild($node);
197 18
            }
198
199 27
            $this->revertCurrentNode();
200 28
        }
201 28
    }
202
203 66
    public function startVisitingObject(ClassMetadata $metadata, $data, array $type, Context $context)
204
    {
205 66
        $this->objectMetadataStack->push($metadata);
206
207 66
        if (null === $this->document) {
208 64
            $this->document = $this->createDocument(null, null, false);
209 64
            if ($metadata->xmlRootName) {
210 17
                $rootName = $metadata->xmlRootName;
211 17
                $rootNamespace = $metadata->xmlRootNamespace?:$this->getClassDefaultNamespace($metadata);
212 17
            } else {
213 47
                $rootName = $this->defaultRootName;
214 47
                $rootNamespace = $this->defaultRootNamespace;
215
            }
216
217 64
            if ($rootNamespace) {
218 5
                $this->currentNode = $this->document->createElementNS($rootNamespace, $rootName);
219 5
            } else {
220 59
                $this->currentNode = $this->document->createElement($rootName);
221
            }
222
223 64
            $this->document->appendChild($this->currentNode);
224 64
        }
225
226 66
        $this->addNamespaceAttributes($metadata, $this->currentNode);
227
228 66
        $this->hasValue = false;
229 66
    }
230
231 66
    public function visitProperty(PropertyMetadata $metadata, $object, Context $context)
232
    {
233 66
        $v = $this->accessor->getValue($object, $metadata);
234
235 66
        if (null === $v && $context->shouldSerializeNull() !== true) {
236 6
            return;
237
        }
238
239 66
        if ($metadata->xmlAttribute) {
240 12
            $this->setCurrentMetadata($metadata);
241 12
            $node = $this->navigator->accept($v, $metadata->type, $context);
242 12
            $this->revertCurrentMetadata();
243
244 12
            if ( ! $node instanceof \DOMCharacterData) {
245
                throw new RuntimeException(sprintf('Unsupported value for XML attribute for %s. Expected character data, but got %s.', $metadata->name, json_encode($v)));
246
            }
247 12
            $attributeName = $this->namingStrategy->translateName($metadata);
248 12
            $this->setAttributeOnNode($this->currentNode, $attributeName, $node->nodeValue, $metadata->xmlNamespace);
249
250 12
            return;
251
        }
252
253 64
        if (($metadata->xmlValue && $this->currentNode->childNodes->length > 0)
254 64
            || ( ! $metadata->xmlValue && $this->hasValue)) {
255 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));
256
        }
257
258 64
        if ($metadata->xmlValue) {
259 8
            $this->hasValue = true;
260
261 8
            $this->setCurrentMetadata($metadata);
262 8
            $node = $this->navigator->accept($v, $metadata->type, $context);
263 8
            $this->revertCurrentMetadata();
264
265 8
            if ( ! $node instanceof \DOMCharacterData) {
266
                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)));
267
            }
268
269 8
            $this->currentNode->appendChild($node);
270
271 8
            return;
272
        }
273
274 60
        if ($metadata->xmlAttributeMap) {
275 2
            if ( ! is_array($v)) {
276 1
                throw new RuntimeException(sprintf('Unsupported value type for XML attribute map. Expected array but got %s.', gettype($v)));
277
            }
278
279 1
            foreach ($v as $key => $value) {
280 1
                $this->setCurrentMetadata($metadata);
281 1
                $node = $this->navigator->accept($value, null, $context);
282 1
                $this->revertCurrentMetadata();
283
284 1
                if ( ! $node instanceof \DOMCharacterData) {
285
                    throw new RuntimeException(sprintf('Unsupported value for a XML attribute map value. Expected character data, but got %s.', json_encode($v)));
286
                }
287
288 1
                $this->setAttributeOnNode($this->currentNode, $key, $node->nodeValue, $metadata->xmlNamespace);
289 1
            }
290
291 1
            return;
292
        }
293
294 58
        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...
295 57
            $elementName = $this->namingStrategy->translateName($metadata);
296
297 57
            $namespace = null !== $metadata->xmlNamespace
298 6
                ? $metadata->xmlNamespace
299 6
                : $this->getClassDefaultNamespace($this->objectMetadataStack->top());
300 54
301 54
            $element = $this->createElement($elementName, $namespace);
302
            $this->currentNode->appendChild($element);
303 57
            $this->setCurrentNode($element);
304 57
        }
305
306 58
        $this->setCurrentMetadata($metadata);
307
308 58
        if (null !== $node = $this->navigator->accept($v, $metadata->type, $context)) {
309 47
            $this->currentNode->appendChild($node);
310 47
        }
311
312 58
        $this->revertCurrentMetadata();
313
314 58
        if ($addEnclosingElement) {
315 57
            $this->revertCurrentNode();
316
317 57
            if ($this->isElementEmpty($element) && ($this->isSkippableCollection($metadata) || $v === null || $context->isVisiting($v))) {
318 56
                $this->currentNode->removeChild($element);
0 ignored issues
show
Bug introduced by
The variable $element does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
319 56
            }
320 57
        }
321
322 58
        $this->hasValue = false;
323 58
    }
324
325 57
    private function isInLineCollection(PropertyMetadata $metadata)
326
    {
327 57
        return $metadata->xmlCollection && $metadata->xmlCollectionInline;
328
    }
329
330 64
    private function isSkippableCollection(PropertyMetadata $metadata)
331
    {
332 64
        return $metadata->xmlCollection && $metadata->xmlCollectionSkipWhenEmpty;
333 64
    }
334
335 96
    private function isElementEmpty(\DOMElement $element)
336
    {
337 96
        return !$element->hasChildNodes() && !$element->hasAttributes();
338
    }
339
340 2
    public function endVisitingObject(ClassMetadata $metadata, $data, array $type, Context $context)
341
    {
342 2
        $this->objectMetadataStack->pop();
343
    }
344
345
    public function getResult()
346
    {
347
        return $this->document->saveXML();
348
    }
349
350
    public function getCurrentNode()
351
    {
352
        return $this->currentNode;
353
    }
354
355 65
    public function getCurrentMetadata()
356
    {
357 65
        return $this->currentMetadata;
358 65
    }
359 65
360
    public function getDocument()
361 89
    {
362
        return $this->document;
363 89
    }
364 89
365 89
    public function setCurrentMetadata(PropertyMetadata $metadata)
366
    {
367 65
        $this->metadataStack->push($this->currentMetadata);
368
        $this->currentMetadata = $metadata;
369 65
    }
370
371
    public function setCurrentNode(\DOMNode $node)
372 65
    {
373
        $this->stack->push($this->currentNode);
374 65
        $this->currentNode = $node;
375
    }
376
377 98
    public function revertCurrentNode()
378
    {
379 98
        return $this->currentNode = $this->stack->pop();
380 98
    }
381
382 98
    public function revertCurrentMetadata()
383 30
    {
384
        return $this->currentMetadata = $this->metadataStack->pop();
385
    }
386 30
387
    public function createDocument($version = null, $encoding = null, $addRoot = true)
388 30
    {
389 30
        $doc = new \DOMDocument($version ?: $this->defaultVersion, $encoding ?: $this->defaultEncoding);
390 30
        $doc->formatOutput = $this->isFormatOutput();
391
392 98
        if ($addRoot) {
393
            if ($this->defaultRootNamespace) {
394
                $rootNode = $doc->createElementNS($this->defaultRootNamespace, $this->defaultRootName);
395 100
            } else {
396
                $rootNode = $doc->createElement($this->defaultRootName);
397 100
            }
398
            $this->setCurrentNode($rootNode);
399 100
            $doc->appendChild($rootNode);
400
        }
401
402 28
        return $doc;
403
    }
404 28
405 5
    public function prepare($data)
406 5
    {
407
        $this->nullWasVisited = false;
408 5
409
        return $data;
410
    }
411 23
412
    private function visitNumeric($data, array $type)
0 ignored issues
show
Unused Code introduced by
The parameter $type is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
413
    {
414
        if (null === $this->document) {
415
            $this->document = $this->createDocument(null, null, true);
416
            $this->currentNode->appendChild($textNode = $this->document->createTextNode((string) $data));
417
418
            return $textNode;
419
        }
420
421 2
        return $this->document->createTextNode((string) $data);
422
    }
423 2
424
    /**
425
     * Checks that the name is a valid XML element name.
426 9
     *
427
     * @param string $name
428 9
     *
429 9
     * @return boolean
430 9
     */
431 9
    private function isElementNameValid($name)
432
    {
433 9
        return $name && false === strpos($name, ' ') && preg_match('#^[\pL_][\pL0-9._-]*$#ui', $name);
434 9
    }
435 9
436 9
    private function attachNullNamespace()
437
    {
438
        if ( ! $this->nullWasVisited) {
439
            $this->document->documentElement->setAttributeNS(
440
                'http://www.w3.org/2000/xmlns/',
441
                'xmlns:xsi',
442
                'http://www.w3.org/2001/XMLSchema-instance'
443
            );
444 66
            $this->nullWasVisited = true;
445
        }
446 66
    }
447 6
448 6
    /**
449 6
     * Adds namespace attributes to the XML root element
450 6
     *
451 4
     * @param \JMS\Serializer\Metadata\ClassMetadata $metadata
452
     * @param \DOMElement $element
453 6
     */
454 66
    private function addNamespaceAttributes(ClassMetadata $metadata, \DOMElement $element)
455 66
    {
456
        foreach ($metadata->xmlNamespaces as $prefix => $uri) {
457 65
            $attribute = 'xmlns';
458
            if ($prefix !== '') {
459 65
                $attribute .= ':'.$prefix;
460 61
            } elseif ($element->namespaceURI === $uri) {
461
                continue;
462 6
            }
463 3
            $element->setAttributeNS('http://www.w3.org/2000/xmlns/', $attribute, $uri);
464
        }
465 6
    }
466 1
467 1
    private function createElement($tagName, $namespace = null)
468
    {
469 6
        if (null === $namespace) {
470
            return $this->document->createElement($tagName);
471
        }
472 13
        if ($this->currentNode->isDefaultNamespace($namespace)) {
473
            return $this->document->createElementNS($namespace, $tagName);
474 13
        }
475 4
        if (!($prefix = $this->currentNode->lookupPrefix($namespace)) && !($prefix = $this->document->lookupPrefix($namespace))) {
476 2
            $prefix = 'ns-'.  substr(sha1($namespace), 0, 8);
477 2
        }
478 4
        return $this->document->createElementNS($namespace, $prefix . ':' . $tagName);
479 4
    }
480 11
481
    private function setAttributeOnNode(\DOMElement $node, $name, $value, $namespace = null)
482 13
    {
483
        if (null !== $namespace) {
484 58
            if (!$prefix = $node->lookupPrefix($namespace)) {
485
                $prefix = 'ns-'.  substr(sha1($namespace), 0, 8);
486 58
            }
487
            $node->setAttributeNS($namespace, $prefix.':'.$name, $value);
488
        } else {
489
            $node->setAttribute($name, $value);
490
        }
491
    }
492 98
493
    private function getClassDefaultNamespace(ClassMetadata $metadata)
494 98
    {
495
        return (isset($metadata->xmlNamespaces[''])?$metadata->xmlNamespaces['']:null);
496
    }
497
498
    /**
499
     * @return bool
500 1
     */
501
    public function isFormatOutput()
502 1
    {
503 1
        return $this->formatOutput;
504
    }
505
506
    /**
507
     * @param bool $formatOutput
508
     */
509
    public function setFormatOutput($formatOutput)
510
    {
511
        $this->formatOutput = (boolean) $formatOutput;
512
    }
513
}
514