Completed
Push — master ( 751c6b...94b3c9 )
by Asmir
17:55 queued 14:19
created

XmlSerializationVisitor::setCurrentNode()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 5
c 0
b 0
f 0
ccs 4
cts 4
cp 1
rs 9.4285
cc 1
eloc 3
nc 1
nop 1
crap 1
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 253
    public function __construct($namingStrategy)
51
    {
52 253
        parent::__construct($namingStrategy);
53 253
        $this->objectMetadataStack = new \SplStack;
54 253
        $this->formatOutput = true;
55 253
    }
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 82
    public function setNavigator(GraphNavigator $navigator)
82
    {
83 82
        $this->navigator = $navigator;
84 82
        $this->document = null;
85 82
        $this->stack = new \SplStack;
86 82
        $this->metadataStack = new \SplStack;
87 82
    }
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 51
    public function visitString($data, array $type, Context $context)
115
    {
116
117 51
        if (null !== $this->currentMetadata) {
118 45
            $doCData = $this->currentMetadata->xmlElementCData;
119 45
        } else {
120 7
            $doCData = true;
121
        }
122
123 51
        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 47
        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 26
    public function visitArray($data, array $type, Context $context)
168
    {
169 26
        if (null === $this->document) {
170 8
            $this->document = $this->createDocument(null, null, true);
171 8
        }
172
173 26
        $entryName = (null !== $this->currentMetadata && null !== $this->currentMetadata->xmlEntryName) ? $this->currentMetadata->xmlEntryName : 'entry';
174 26
        $keyAttributeName = (null !== $this->currentMetadata && null !== $this->currentMetadata->xmlKeyAttribute) ? $this->currentMetadata->xmlKeyAttribute : null;
175 26
        $namespace = (null !== $this->currentMetadata && null !== $this->currentMetadata->xmlEntryNamespace) ? $this->currentMetadata->xmlEntryNamespace : null;
176
177 26
        foreach ($data as $k => $v) {
178 25
            $tagName = (null !== $this->currentMetadata && $this->currentMetadata->xmlKeyValuePairs && $this->isElementNameValid($k)) ? $k : $entryName;
179
180 25
            $entryNode = $this->createElement($tagName, $namespace);
181 25
            $this->currentNode->appendChild($entryNode);
182 25
            $this->setCurrentNode($entryNode);
183
184 25
            if (null !== $keyAttributeName) {
185 6
                $entryNode->setAttribute($keyAttributeName, (string) $k);
186 6
            }
187
188 25
            if (null !== $node = $this->navigator->accept($v, $this->getElementType($type), $context)) {
189 16
                $this->currentNode->appendChild($node);
190 16
            }
191
192 25
            $this->revertCurrentNode();
193 26
        }
194 26
    }
195
196 52
    public function startVisitingObject(ClassMetadata $metadata, $data, array $type, Context $context)
197
    {
198 52
        $this->objectMetadataStack->push($metadata);
199
200 52
        if (null === $this->document) {
201 50
            $this->document = $this->createDocument(null, null, false);
202 50
            if ($metadata->xmlRootName) {
203 16
                $rootName = $metadata->xmlRootName;
204 16
                $rootNamespace = $metadata->xmlRootNamespace?:$this->getClassDefaultNamespace($metadata);
205 16
            } else {
206 34
                $rootName = $this->defaultRootName;
207 34
                $rootNamespace = $this->defaultRootNamespace;
208
            }
209
210 50
            if ($rootNamespace) {
211 4
                $this->currentNode = $this->document->createElementNS($rootNamespace, $rootName);
212 4
            } else {
213 46
                $this->currentNode = $this->document->createElement($rootName);
214
            }
215
216 50
            $this->document->appendChild($this->currentNode);
217 50
        }
218
219 52
        $this->addNamespaceAttributes($metadata, $this->currentNode);
220
221 52
        $this->hasValue = false;
222 52
    }
223
224 52
    public function visitProperty(PropertyMetadata $metadata, $object, Context $context)
225
    {
226 52
        $v = $metadata->getValue($object);
227
228 52
        if (null === $v && $context->shouldSerializeNull() !== true) {
229 2
            return;
230
        }
231
232 52
        if ($metadata->xmlAttribute) {
233 11
            $this->setCurrentMetadata($metadata);
234 11
            $node = $this->navigator->accept($v, $metadata->type, $context);
235 11
            $this->revertCurrentMetadata();
236
237 11
            if ( ! $node instanceof \DOMCharacterData) {
238
                throw new RuntimeException(sprintf('Unsupported value for XML attribute for %s. Expected character data, but got %s.', $metadata->name, json_encode($v)));
239
            }
240 11
            $attributeName = $this->namingStrategy->translateName($metadata);
241 11
            $this->setAttributeOnNode($this->currentNode, $attributeName, $node->nodeValue, $metadata->xmlNamespace);
242
243 11
            return;
244
        }
245
246 51
        if (($metadata->xmlValue && $this->currentNode->childNodes->length > 0)
247 51
            || ( ! $metadata->xmlValue && $this->hasValue)) {
248 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));
249
        }
250
251 51
        if ($metadata->xmlValue) {
252 8
            $this->hasValue = true;
253
254 8
            $this->setCurrentMetadata($metadata);
255 8
            $node = $this->navigator->accept($v, $metadata->type, $context);
256 8
            $this->revertCurrentMetadata();
257
258 8
            if ( ! $node instanceof \DOMCharacterData) {
259
                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)));
260
            }
261
262 8
            $this->currentNode->appendChild($node);
263
264 8
            return;
265
        }
266
267 47
        if ($metadata->xmlAttributeMap) {
268 2
            if ( ! is_array($v)) {
269 1
                throw new RuntimeException(sprintf('Unsupported value type for XML attribute map. Expected array but got %s.', gettype($v)));
270
            }
271
272 1
            foreach ($v as $key => $value) {
273 1
                $this->setCurrentMetadata($metadata);
274 1
                $node = $this->navigator->accept($value, null, $context);
275 1
                $this->revertCurrentMetadata();
276
277 1
                if ( ! $node instanceof \DOMCharacterData) {
278
                    throw new RuntimeException(sprintf('Unsupported value for a XML attribute map value. Expected character data, but got %s.', json_encode($v)));
279
                }
280
281 1
                $this->setAttributeOnNode($this->currentNode, $key, $node->nodeValue, $metadata->xmlNamespace);
282 1
            }
283
284 1
            return;
285
        }
286
287 45
        if ($addEnclosingElement = ( ! $metadata->xmlCollection || ! $metadata->xmlCollectionInline) && ! $metadata->inline) {
288 44
            $elementName = $this->namingStrategy->translateName($metadata);
289
290 44
            if (null !== $metadata->xmlNamespace) {
291 5
                $element = $this->createElement($elementName, $metadata->xmlNamespace);
292 5
            } else {
293 41
                $defaultNamespace = $this->getClassDefaultNamespace($this->objectMetadataStack->top());
294 41
                $element = $this->createElement($elementName, $defaultNamespace);
295
            }
296 44
            $this->setCurrentNode($element);
297 44
        }
298
299 45
        $this->setCurrentMetadata($metadata);
300
301 45
        if (null !== $node = $this->navigator->accept($v, $metadata->type, $context)) {
302 35
            $this->currentNode->appendChild($node);
303 35
        }
304
305 45
        $this->revertCurrentMetadata();
306
307 45
        if ($addEnclosingElement) {
308 44
            $this->revertCurrentNode();
309
310 44
            if ($this->nodeNotEmpty($element) || ((!$metadata->xmlCollection || !$metadata->xmlCollectionSkipWhenEmpty) && $node === null && $v !== null && !$context->isVisiting($v))) {
311 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...
312 44
            }
313 44
        }
314
315 45
        $this->hasValue = false;
316 45
    }
317
318 44
    private function nodeNotEmpty(\DOMElement $element)
319
    {
320 44
        return $element->hasChildNodes() || $element->hasAttributes();
321
    }
322
323 50
    public function endVisitingObject(ClassMetadata $metadata, $data, array $type, Context $context)
324
    {
325 50
        $this->objectMetadataStack->pop();
326 50
    }
327
328 79
    public function getResult()
329
    {
330 79
        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
    public function getDocument()
344
    {
345
        return $this->document;
346
    }
347
348 51
    public function setCurrentMetadata(PropertyMetadata $metadata)
349
    {
350 51
        $this->metadataStack->push($this->currentMetadata);
351 51
        $this->currentMetadata = $metadata;
352 51
    }
353
354 73
    public function setCurrentNode(\DOMNode $node)
355
    {
356 73
        $this->stack->push($this->currentNode);
357 73
        $this->currentNode = $node;
358 73
    }
359
360 51
    public function revertCurrentNode()
361
    {
362 51
        return $this->currentNode = $this->stack->pop();
363
    }
364
365 51
    public function revertCurrentMetadata()
366
    {
367 51
        return $this->currentMetadata = $this->metadataStack->pop();
368
    }
369
370 81
    public function createDocument($version = null, $encoding = null, $addRoot = true)
371
    {
372 81
        $doc = new \DOMDocument($version ?: $this->defaultVersion, $encoding ?: $this->defaultEncoding);
373 81
        $doc->formatOutput = $this->isFormatOutput();
374
375 81
        if ($addRoot) {
376 27
            if ($this->defaultRootNamespace) {
377
                $rootNode = $doc->createElementNS($this->defaultRootNamespace, $this->defaultRootName);
378
            } else {
379 27
                $rootNode = $doc->createElement($this->defaultRootName);
380
            }
381 27
            $this->setCurrentNode($rootNode);
382 27
            $doc->appendChild($rootNode);
383 27
        }
384
385 81
        return $doc;
386
    }
387
388 82
    public function prepare($data)
389
    {
390 82
        $this->nullWasVisited = false;
391
392 82
        return $data;
393
    }
394
395 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...
396
    {
397 26
        if (null === $this->document) {
398 5
            $this->document = $this->createDocument(null, null, true);
399 5
            $this->currentNode->appendChild($textNode = $this->document->createTextNode((string) $data));
400
401 5
            return $textNode;
402
        }
403
404 21
        return $this->document->createTextNode((string) $data);
405
    }
406
407
    /**
408
     * Checks that the name is a valid XML element name.
409
     *
410
     * @param string $name
411
     *
412
     * @return boolean
413
     */
414 2
    private function isElementNameValid($name)
415
    {
416 2
        return $name && false === strpos($name, ' ') && preg_match('#^[\pL_][\pL0-9._-]*$#ui', $name);
417
    }
418
419 9
    private function attachNullNamespace()
420
    {
421 9
        if ( ! $this->nullWasVisited) {
422 9
            $this->document->documentElement->setAttributeNS(
423 9
                'http://www.w3.org/2000/xmlns/',
424 9
                'xmlns:xsi',
425
                'http://www.w3.org/2001/XMLSchema-instance'
426 9
            );
427 9
            $this->nullWasVisited = true;
428 9
        }
429 9
    }
430
431
    /**
432
     * Adds namespace attributes to the XML root element
433
     *
434
     * @param \JMS\Serializer\Metadata\ClassMetadata $metadata
435
     * @param \DOMElement $element
436
     */
437 52
    private function addNamespaceAttributes(ClassMetadata $metadata, \DOMElement $element)
438
    {
439 52
        foreach ($metadata->xmlNamespaces as $prefix => $uri) {
440 5
            $attribute = 'xmlns';
441 5
            if ($prefix !== '') {
442 5
                $attribute .= ':'.$prefix;
443 5
            } elseif ($element->namespaceURI === $uri) {
444 3
                continue;
445
            }
446 5
            $element->setAttributeNS('http://www.w3.org/2000/xmlns/', $attribute, $uri);
447 52
        }
448 52
    }
449
450 51
    private function createElement($tagName, $namespace = null)
451
    {
452 51
        if (null === $namespace) {
453 48
            return $this->document->createElement($tagName);
454
        }
455 5
        if ($this->currentNode->isDefaultNamespace($namespace)) {
456 2
            return $this->document->createElementNS($namespace, $tagName);
457
        }
458 5
        if (!($prefix = $this->currentNode->lookupPrefix($namespace)) && !($prefix = $this->document->lookupPrefix($namespace))) {
459 1
            $prefix = 'ns-'.  substr(sha1($namespace), 0, 8);
460 1
            return $this->document->createElementNS($namespace, $prefix . ':' . $tagName);
461
        }
462 5
        return $this->document->createElement($prefix . ':' . $tagName);
463
    }
464
465 12
    private function setAttributeOnNode(\DOMElement $node, $name, $value, $namespace = null)
466
    {
467 12
        if (null !== $namespace) {
468 4
            if (!$prefix = $node->lookupPrefix($namespace)) {
469 2
                $prefix = 'ns-'.  substr(sha1($namespace), 0, 8);
470 2
            }
471 4
            $node->setAttributeNS($namespace, $prefix.':'.$name, $value);
472 4
        } else {
473 10
            $node->setAttribute($name, $value);
474
        }
475 12
    }
476
477 45
    private function getClassDefaultNamespace(ClassMetadata $metadata)
478
    {
479 45
        return (isset($metadata->xmlNamespaces[''])?$metadata->xmlNamespaces['']:null);
480
    }
481
482
    /**
483
     * @return bool
484
     */
485 81
    public function isFormatOutput()
486
    {
487 81
        return $this->formatOutput;
488
    }
489
490
    /**
491
     * @param bool $formatOutput
492
     */
493 1
    public function setFormatOutput($formatOutput)
494
    {
495 1
        $this->formatOutput = (boolean) $formatOutput;
496 1
    }
497
}
498