Completed
Pull Request — master (#660)
by Asmir
06:39
created

XmlSerializationVisitor::visitSimpleString()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2.0185

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 11
ccs 5
cts 6
cp 0.8333
rs 9.4285
cc 2
eloc 6
nc 2
nop 3
crap 2.0185
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\Exception\RuntimeException;
22
use JMS\Serializer\Metadata\ClassMetadata;
23
use JMS\Serializer\Metadata\PropertyMetadata;
24
25
/**
26
 * XmlSerializationVisitor.
27
 *
28
 * @author Johannes M. Schmitt <[email protected]>
29
 */
30
class XmlSerializationVisitor extends AbstractVisitor
31
{
32
    public $document;
33
34
    private $navigator;
35
    private $defaultRootName = 'result';
36
    private $defaultRootNamespace;
37
    private $defaultVersion = '1.0';
38
    private $defaultEncoding = 'UTF-8';
39
    private $stack;
40
    private $metadataStack;
41
    private $currentNode;
42
    private $currentMetadata;
43
    private $hasValue;
44
    private $nullWasVisited;
45
    private $objectMetadataStack;
46
47
    /** @var boolean */
48
    private $formatOutput;
49
50 260
    public function __construct($namingStrategy)
51
    {
52 260
        parent::__construct($namingStrategy);
53 260
        $this->objectMetadataStack = new \SplStack;
54 260
        $this->formatOutput = true;
55 260
    }
56
57
    public function setDefaultRootName($name, $namespace = null)
58
    {
59
        $this->defaultRootName = $name;
60
        $this->defaultRootNamespace = $namespace;
61
    }
62
63
    /**
64
     * @return boolean
65
     */
66
    public function hasDefaultRootName()
67
    {
68
        return 'result' === $this->defaultRootName;
69
    }
70
71
    public function setDefaultVersion($version)
72
    {
73
        $this->defaultVersion = $version;
74
    }
75
76
    public function setDefaultEncoding($encoding)
77
    {
78
        $this->defaultEncoding = $encoding;
79
    }
80
81 83
    public function setNavigator(GraphNavigator $navigator)
82
    {
83 83
        $this->navigator = $navigator;
84 83
        $this->document = null;
85 83
        $this->stack = new \SplStack;
86 83
        $this->metadataStack = new \SplStack;
87 83
    }
88
89
    public function getNavigator()
90
    {
91
        return $this->navigator;
92
    }
93
94 9
    public function visitNull($data, array $type, Context $context)
95
    {
96 9
        if (null === $this->document) {
97 6
            $this->document = $this->createDocument(null, null, true);
98 6
            $node = $this->document->createAttribute('xsi:nil');
99 6
            $node->value = 'true';
100 6
            $this->currentNode->appendChild($node);
101
102 6
            $this->attachNullNamespace();
103
104 6
            return;
105
        }
106
107 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...
108 3
        $node->value = 'true';
109 3
        $this->attachNullNamespace();
110
111 3
        return $node;
112
    }
113
114 52
    public function visitString($data, array $type, Context $context)
115
    {
116
117 52
        if (null !== $this->currentMetadata) {
118 45
            $doCData = $this->currentMetadata->xmlElementCData;
119 45
        } else {
120 8
            $doCData = true;
121
        }
122
123 52
        if (null === $this->document) {
124 4
            $this->document = $this->createDocument(null, null, true);
125 4
            $this->currentNode->appendChild($doCData ? $this->document->createCDATASection($data) : $this->document->createTextNode((string) $data));
126
127 4
            return;
128
        }
129
130 48
        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...
131
    }
132
133 1
    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...
134
    {
135 1
        if (null === $this->document) {
136 1
            $this->document = $this->createDocument(null, null, true);
137 1
            $this->currentNode->appendChild($this->document->createTextNode((string) $data));
138
139 1
            return;
140
        }
141
142
        return $this->document->createTextNode((string) $data);
143
    }
144
145 5
    public function visitBoolean($data, array $type, Context $context)
146
    {
147 5
        if (null === $this->document) {
148 2
            $this->document = $this->createDocument(null, null, true);
149 2
            $this->currentNode->appendChild($this->document->createTextNode($data ? 'true' : 'false'));
150
151 2
            return;
152
        }
153
154 3
        return $this->document->createTextNode($data ? 'true' : 'false');
155
    }
156
157 17
    public function visitInteger($data, array $type, Context $context)
158
    {
159 17
        return $this->visitNumeric($data, $type);
160
    }
161
162 9
    public function visitDouble($data, array $type, Context $context)
163
    {
164 9
        return $this->visitNumeric($data, $type);
165
    }
166
167 27
    public function visitArray($data, array $type, Context $context)
168
    {
169 27
        if (null === $this->document) {
170 9
            $this->document = $this->createDocument(null, null, true);
171 9
        }
172
173 27
        $entryName = (null !== $this->currentMetadata && null !== $this->currentMetadata->xmlEntryName) ? $this->currentMetadata->xmlEntryName : 'entry';
174 27
        $keyAttributeName = (null !== $this->currentMetadata && null !== $this->currentMetadata->xmlKeyAttribute) ? $this->currentMetadata->xmlKeyAttribute : null;
175 27
        $namespace = (null !== $this->currentMetadata && null !== $this->currentMetadata->xmlEntryNamespace) ? $this->currentMetadata->xmlEntryNamespace : null;
176
177 27
        foreach ($data as $k => $v) {
178
179 26
            if (null === $v && $context->shouldSerializeNull() !== true) {
180 1
                continue;
181
            }
182
183 26
            $tagName = (null !== $this->currentMetadata && $this->currentMetadata->xmlKeyValuePairs && $this->isElementNameValid($k)) ? $k : $entryName;
184
185 26
            $entryNode = $this->createElement($tagName, $namespace);
186 26
            $this->currentNode->appendChild($entryNode);
187 26
            $this->setCurrentNode($entryNode);
188
189 26
            if (null !== $keyAttributeName) {
190 6
                $entryNode->setAttribute($keyAttributeName, (string) $k);
191 6
            }
192
193 26
            if (null !== $node = $this->navigator->accept($v, $this->getElementType($type), $context)) {
194 17
                $this->currentNode->appendChild($node);
195 17
            }
196
197 26
            $this->revertCurrentNode();
198 27
        }
199 27
    }
200
201 52
    public function startVisitingObject(ClassMetadata $metadata, $data, array $type, Context $context)
202
    {
203 52
        $this->objectMetadataStack->push($metadata);
204
205 52
        if (null === $this->document) {
206 50
            $this->document = $this->createDocument(null, null, false);
207 50
            if ($metadata->xmlRootName) {
208 16
                $rootName = $metadata->xmlRootName;
209 16
                $rootNamespace = $metadata->xmlRootNamespace?:$this->getClassDefaultNamespace($metadata);
210 16
            } else {
211 34
                $rootName = $this->defaultRootName;
212 34
                $rootNamespace = $this->defaultRootNamespace;
213
            }
214
215 50
            if ($rootNamespace) {
216 4
                $this->currentNode = $this->document->createElementNS($rootNamespace, $rootName);
217 4
            } else {
218 46
                $this->currentNode = $this->document->createElement($rootName);
219
            }
220
221 50
            $this->document->appendChild($this->currentNode);
222 50
        }
223
224 52
        $this->addNamespaceAttributes($metadata, $this->currentNode);
225
226 52
        $this->hasValue = false;
227 52
    }
228
229 52
    public function visitProperty(PropertyMetadata $metadata, $object, Context $context)
230
    {
231 52
        $v = $metadata->getValue($object);
232
233 52
        if (null === $v && $context->shouldSerializeNull() !== true) {
234 2
            return;
235
        }
236
237 52
        if ($metadata->xmlAttribute) {
238 11
            $this->setCurrentMetadata($metadata);
239 11
            $node = $this->navigator->accept($v, $metadata->type, $context);
240 11
            $this->revertCurrentMetadata();
241
242 11
            if ( ! $node instanceof \DOMCharacterData) {
243
                throw new RuntimeException(sprintf('Unsupported value for XML attribute for %s. Expected character data, but got %s.', $metadata->name, json_encode($v)));
244
            }
245 11
            $attributeName = $this->namingStrategy->translateName($metadata);
246 11
            $this->setAttributeOnNode($this->currentNode, $attributeName, $node->nodeValue, $metadata->xmlNamespace);
247
248 11
            return;
249
        }
250
251 51
        if (($metadata->xmlValue && $this->currentNode->childNodes->length > 0)
252 51
            || ( ! $metadata->xmlValue && $this->hasValue)) {
253 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));
254
        }
255
256 51
        if ($metadata->xmlValue) {
257 8
            $this->hasValue = true;
258
259 8
            $this->setCurrentMetadata($metadata);
260 8
            $node = $this->navigator->accept($v, $metadata->type, $context);
261 8
            $this->revertCurrentMetadata();
262
263 8
            if ( ! $node instanceof \DOMCharacterData) {
264
                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)));
265
            }
266
267 8
            $this->currentNode->appendChild($node);
268
269 8
            return;
270
        }
271
272 47
        if ($metadata->xmlAttributeMap) {
273 2
            if ( ! is_array($v)) {
274 1
                throw new RuntimeException(sprintf('Unsupported value type for XML attribute map. Expected array but got %s.', gettype($v)));
275
            }
276
277 1
            foreach ($v as $key => $value) {
278 1
                $this->setCurrentMetadata($metadata);
279 1
                $node = $this->navigator->accept($value, null, $context);
280 1
                $this->revertCurrentMetadata();
281
282 1
                if ( ! $node instanceof \DOMCharacterData) {
283
                    throw new RuntimeException(sprintf('Unsupported value for a XML attribute map value. Expected character data, but got %s.', json_encode($v)));
284
                }
285
286 1
                $this->setAttributeOnNode($this->currentNode, $key, $node->nodeValue, $metadata->xmlNamespace);
287 1
            }
288
289 1
            return;
290
        }
291
292 45
        if ($addEnclosingElement = ( ! $metadata->xmlCollection || ! $metadata->xmlCollectionInline) && ! $metadata->inline) {
293 44
            $elementName = $this->namingStrategy->translateName($metadata);
294
295 44
            if (null !== $metadata->xmlNamespace) {
296 5
                $element = $this->createElement($elementName, $metadata->xmlNamespace);
297 5
            } else {
298 41
                $defaultNamespace = $this->getClassDefaultNamespace($this->objectMetadataStack->top());
299 41
                $element = $this->createElement($elementName, $defaultNamespace);
300
            }
301 44
            $this->setCurrentNode($element);
302 44
        }
303
304 45
        $this->setCurrentMetadata($metadata);
305
306 45
        if (null !== $node = $this->navigator->accept($v, $metadata->type, $context)) {
307 35
            $this->currentNode->appendChild($node);
308 35
        }
309
310 45
        $this->revertCurrentMetadata();
311
312 45
        if ($addEnclosingElement) {
313 44
            $this->revertCurrentNode();
314
315 44
            if ($this->nodeNotEmpty($element) || ((!$metadata->xmlCollection || !$metadata->xmlCollectionSkipWhenEmpty) && $node === null && $v !== null && !$context->isVisiting($v))) {
316 44
                $this->currentNode->appendChild($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...
317 44
            }
318 44
        }
319
320 45
        $this->hasValue = false;
321 45
    }
322
323 44
    private function nodeNotEmpty(\DOMElement $element)
324
    {
325 44
        return $element->hasChildNodes() || $element->hasAttributes();
326
    }
327
328 50
    public function endVisitingObject(ClassMetadata $metadata, $data, array $type, Context $context)
329
    {
330 50
        $this->objectMetadataStack->pop();
331 50
    }
332
333 80
    public function getResult()
334
    {
335 80
        return $this->document->saveXML();
336
    }
337
338 2
    public function getCurrentNode()
339
    {
340 2
        return $this->currentNode;
341
    }
342
343
    public function getCurrentMetadata()
344
    {
345
        return $this->currentMetadata;
346
    }
347
348
    public function getDocument()
349
    {
350
        return $this->document;
351
    }
352
353 51
    public function setCurrentMetadata(PropertyMetadata $metadata)
354
    {
355 51
        $this->metadataStack->push($this->currentMetadata);
356 51
        $this->currentMetadata = $metadata;
357 51
    }
358
359 74
    public function setCurrentNode(\DOMNode $node)
360
    {
361 74
        $this->stack->push($this->currentNode);
362 74
        $this->currentNode = $node;
363 74
    }
364
365 52
    public function revertCurrentNode()
366
    {
367 52
        return $this->currentNode = $this->stack->pop();
368
    }
369
370 51
    public function revertCurrentMetadata()
371
    {
372 51
        return $this->currentMetadata = $this->metadataStack->pop();
373
    }
374
375 82
    public function createDocument($version = null, $encoding = null, $addRoot = true)
376
    {
377 82
        $doc = new \DOMDocument($version ?: $this->defaultVersion, $encoding ?: $this->defaultEncoding);
378 82
        $doc->formatOutput = $this->isFormatOutput();
379
380 82
        if ($addRoot) {
381 28
            if ($this->defaultRootNamespace) {
382
                $rootNode = $doc->createElementNS($this->defaultRootNamespace, $this->defaultRootName);
383
            } else {
384 28
                $rootNode = $doc->createElement($this->defaultRootName);
385
            }
386 28
            $this->setCurrentNode($rootNode);
387 28
            $doc->appendChild($rootNode);
388 28
        }
389
390 82
        return $doc;
391
    }
392
393 83
    public function prepare($data)
394
    {
395 83
        $this->nullWasVisited = false;
396
397 83
        return $data;
398
    }
399
400 26
    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...
401
    {
402 26
        if (null === $this->document) {
403 5
            $this->document = $this->createDocument(null, null, true);
404 5
            $this->currentNode->appendChild($textNode = $this->document->createTextNode((string) $data));
405
406 5
            return $textNode;
407
        }
408
409 21
        return $this->document->createTextNode((string) $data);
410
    }
411
412
    /**
413
     * Checks that the name is a valid XML element name.
414
     *
415
     * @param string $name
416
     *
417
     * @return boolean
418
     */
419 2
    private function isElementNameValid($name)
420
    {
421 2
        return $name && false === strpos($name, ' ') && preg_match('#^[\pL_][\pL0-9._-]*$#ui', $name);
422
    }
423
424 9
    private function attachNullNamespace()
425
    {
426 9
        if ( ! $this->nullWasVisited) {
427 9
            $this->document->documentElement->setAttributeNS(
428 9
                'http://www.w3.org/2000/xmlns/',
429 9
                'xmlns:xsi',
430
                'http://www.w3.org/2001/XMLSchema-instance'
431 9
            );
432 9
            $this->nullWasVisited = true;
433 9
        }
434 9
    }
435
436
    /**
437
     * Adds namespace attributes to the XML root element
438
     *
439
     * @param \JMS\Serializer\Metadata\ClassMetadata $metadata
440
     * @param \DOMElement $element
441
     */
442 52
    private function addNamespaceAttributes(ClassMetadata $metadata, \DOMElement $element)
443
    {
444 52
        foreach ($metadata->xmlNamespaces as $prefix => $uri) {
445 5
            $attribute = 'xmlns';
446 5
            if ($prefix !== '') {
447 5
                $attribute .= ':'.$prefix;
448 5
            } elseif ($element->namespaceURI === $uri) {
449 3
                continue;
450
            }
451 5
            $element->setAttributeNS('http://www.w3.org/2000/xmlns/', $attribute, $uri);
452 52
        }
453 52
    }
454
455 52
    private function createElement($tagName, $namespace = null)
456
    {
457 52
        if (null === $namespace) {
458 49
            return $this->document->createElement($tagName);
459
        }
460 5
        if ($this->currentNode->isDefaultNamespace($namespace)) {
461 2
            return $this->document->createElementNS($namespace, $tagName);
462
        }
463 5
        if (!($prefix = $this->currentNode->lookupPrefix($namespace)) && !($prefix = $this->document->lookupPrefix($namespace))) {
464 1
            $prefix = 'ns-'.  substr(sha1($namespace), 0, 8);
465 1
            return $this->document->createElementNS($namespace, $prefix . ':' . $tagName);
466
        }
467 5
        return $this->document->createElement($prefix . ':' . $tagName);
468
    }
469
470 12
    private function setAttributeOnNode(\DOMElement $node, $name, $value, $namespace = null)
471
    {
472 12
        if (null !== $namespace) {
473 4
            if (!$prefix = $node->lookupPrefix($namespace)) {
474 2
                $prefix = 'ns-'.  substr(sha1($namespace), 0, 8);
475 2
            }
476 4
            $node->setAttributeNS($namespace, $prefix.':'.$name, $value);
477 4
        } else {
478 10
            $node->setAttribute($name, $value);
479
        }
480 12
    }
481
482 45
    private function getClassDefaultNamespace(ClassMetadata $metadata)
483
    {
484 45
        return (isset($metadata->xmlNamespaces[''])?$metadata->xmlNamespaces['']:null);
485
    }
486
487
    /**
488
     * @return bool
489
     */
490 82
    public function isFormatOutput()
491
    {
492 82
        return $this->formatOutput;
493
    }
494
495
    /**
496
     * @param bool $formatOutput
497
     */
498 1
    public function setFormatOutput($formatOutput)
499
    {
500 1
        $this->formatOutput = (boolean) $formatOutput;
501 1
    }
502
}
503