Completed
Push — master ( 9912c8...76f230 )
by Asmir
13s
created

XmlSerializationVisitor::isElementEmpty()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 2
eloc 2
nc 2
nop 1
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\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 309
    public function __construct($namingStrategy)
51
    {
52 309
        parent::__construct($namingStrategy);
53 309
        $this->objectMetadataStack = new \SplStack;
54 309
        $this->formatOutput = true;
55 309
    }
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 101
    public function setNavigator(GraphNavigator $navigator)
82
    {
83 101
        $this->navigator = $navigator;
84 101
        $this->document = null;
85 101
        $this->stack = new \SplStack;
86 101
        $this->metadataStack = new \SplStack;
87 101
    }
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 68
    public function visitString($data, array $type, Context $context)
115
    {
116
117 68
        if (null !== $this->currentMetadata) {
118 60
            $doCData = $this->currentMetadata->xmlElementCData;
119 60
        } else {
120 9
            $doCData = true;
121
        }
122
123 68
        if (null === $this->document) {
124 5
            $this->document = $this->createDocument(null, null, true);
125 5
            $this->currentNode->appendChild($doCData ? $this->document->createCDATASection($data) : $this->document->createTextNode((string) $data));
126
127 5
            return;
128
        }
129
130 63
        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 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...
134
    {
135 2
        if (null === $this->document) {
136 2
            $this->document = $this->createDocument(null, null, true);
137 2
            $this->currentNode->appendChild($this->document->createTextNode((string) $data));
138
139 2
            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 18
    public function visitInteger($data, array $type, Context $context)
158
    {
159 18
        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 28
    public function visitArray($data, array $type, Context $context)
168
    {
169 28
        if (null === $this->document) {
170 9
            $this->document = $this->createDocument(null, null, true);
171 9
        }
172
173 28
        $entryName = (null !== $this->currentMetadata && null !== $this->currentMetadata->xmlEntryName) ? $this->currentMetadata->xmlEntryName : 'entry';
174 28
        $keyAttributeName = (null !== $this->currentMetadata && null !== $this->currentMetadata->xmlKeyAttribute) ? $this->currentMetadata->xmlKeyAttribute : null;
175 28
        $namespace = (null !== $this->currentMetadata && null !== $this->currentMetadata->xmlEntryNamespace) ? $this->currentMetadata->xmlEntryNamespace : null;
176
177 28
        foreach ($data as $k => $v) {
178
179 27
            if (null === $v && $context->shouldSerializeNull() !== true) {
180 1
                continue;
181
            }
182
183 27
            $tagName = (null !== $this->currentMetadata && $this->currentMetadata->xmlKeyValuePairs && $this->isElementNameValid($k)) ? $k : $entryName;
184
185 27
            $entryNode = $this->createElement($tagName, $namespace);
186 27
            $this->currentNode->appendChild($entryNode);
187 27
            $this->setCurrentNode($entryNode);
188
189 27
            if (null !== $keyAttributeName) {
190 7
                $entryNode->setAttribute($keyAttributeName, (string) $k);
191 7
            }
192
193 27
            if (null !== $node = $this->navigator->accept($v, $this->getElementType($type), $context)) {
194 18
                $this->currentNode->appendChild($node);
195 18
            }
196
197 27
            $this->revertCurrentNode();
198 28
        }
199 28
    }
200
201 67
    public function startVisitingObject(ClassMetadata $metadata, $data, array $type, Context $context)
202
    {
203 67
        $this->objectMetadataStack->push($metadata);
204
205 67
        if (null === $this->document) {
206 65
            $this->document = $this->createDocument(null, null, false);
207 65
            if ($metadata->xmlRootName) {
208 19
                $rootName = $metadata->xmlRootName;
209 19
                $rootNamespace = $metadata->xmlRootNamespace?:$this->getClassDefaultNamespace($metadata);
210 19
            } else {
211 46
                $rootName = $this->defaultRootName;
212 46
                $rootNamespace = $this->defaultRootNamespace;
213
            }
214
215 65
            if ($rootNamespace) {
216 7
                $this->currentNode = $this->document->createElementNS($rootNamespace, $rootName);
217 7
            } else {
218 58
                $this->currentNode = $this->document->createElement($rootName);
219
            }
220
221 65
            $this->document->appendChild($this->currentNode);
222 65
        }
223
224 67
        $this->addNamespaceAttributes($metadata, $this->currentNode);
225
226 67
        $this->hasValue = false;
227 67
    }
228
229 67
    public function visitProperty(PropertyMetadata $metadata, $object, Context $context)
230
    {
231 67
        $v = $metadata->getValue($object);
232
233 67
        if (null === $v && $context->shouldSerializeNull() !== true) {
234 6
            return;
235
        }
236
237 67
        if ($metadata->xmlAttribute) {
238 12
            $this->setCurrentMetadata($metadata);
239 12
            $node = $this->navigator->accept($v, $metadata->type, $context);
240 12
            $this->revertCurrentMetadata();
241
242 12
            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 12
            $attributeName = $this->namingStrategy->translateName($metadata);
246 12
            $this->setAttributeOnNode($this->currentNode, $attributeName, $node->nodeValue, $metadata->xmlNamespace);
247
248 12
            return;
249
        }
250
251 65
        if (($metadata->xmlValue && $this->currentNode->childNodes->length > 0)
252 65
            || ( ! $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 65
        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 61
        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 59
        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...
293 58
            $elementName = $this->namingStrategy->translateName($metadata);
294
295 58
            $namespace = null !== $metadata->xmlNamespace
296 58
                ? $metadata->xmlNamespace
297 58
                : $this->getClassDefaultNamespace($this->objectMetadataStack->top());
298
299 58
            $element = $this->createElement($elementName, $namespace);
300 58
            $this->currentNode->appendChild($element);
301 58
            $this->setCurrentNode($element);
302 58
        }
303
304 59
        $this->setCurrentMetadata($metadata);
305
306 59
        if (null !== $node = $this->navigator->accept($v, $metadata->type, $context)) {
307 48
            $this->currentNode->appendChild($node);
308 48
        }
309
310 59
        $this->revertCurrentMetadata();
311
312 59
        if ($addEnclosingElement) {
313 58
            $this->revertCurrentNode();
314
315 58
            if ($this->isElementEmpty($element) && ($this->isSkippableCollection($metadata) || $v === null || $context->isVisiting($v))) {
316 3
                $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...
317 3
            }
318 58
        }
319
320 59
        $this->hasValue = false;
321 59
    }
322
323 59
    private function isInLineCollection(PropertyMetadata $metadata)
324
    {
325 59
        return $metadata->xmlCollection && $metadata->xmlCollectionInline;
326
    }
327
328 4
    private function isSkippableCollection(PropertyMetadata $metadata)
329
    {
330 4
        return $metadata->xmlCollection && $metadata->xmlCollectionSkipWhenEmpty;
331
    }
332
333 58
    private function isElementEmpty(\DOMElement $element)
334
    {
335 58
        return !$element->hasChildNodes() && !$element->hasAttributes();
336
    }
337
338 65
    public function endVisitingObject(ClassMetadata $metadata, $data, array $type, Context $context)
339
    {
340 65
        $this->objectMetadataStack->pop();
341 65
    }
342
343 97
    public function getResult()
344
    {
345 97
        return $this->document->saveXML();
346
    }
347
348 2
    public function getCurrentNode()
349
    {
350 2
        return $this->currentNode;
351
    }
352
353
    public function getCurrentMetadata()
354
    {
355
        return $this->currentMetadata;
356
    }
357
358
    public function getDocument()
359
    {
360
        return $this->document;
361
    }
362
363 66
    public function setCurrentMetadata(PropertyMetadata $metadata)
364
    {
365 66
        $this->metadataStack->push($this->currentMetadata);
366 66
        $this->currentMetadata = $metadata;
367 66
    }
368
369 90
    public function setCurrentNode(\DOMNode $node)
370
    {
371 90
        $this->stack->push($this->currentNode);
372 90
        $this->currentNode = $node;
373 90
    }
374
375 66
    public function revertCurrentNode()
376
    {
377 66
        return $this->currentNode = $this->stack->pop();
378
    }
379
380 66
    public function revertCurrentMetadata()
381
    {
382 66
        return $this->currentMetadata = $this->metadataStack->pop();
383
    }
384
385 99
    public function createDocument($version = null, $encoding = null, $addRoot = true)
386
    {
387 99
        $doc = new \DOMDocument($version ?: $this->defaultVersion, $encoding ?: $this->defaultEncoding);
388 99
        $doc->formatOutput = $this->isFormatOutput();
389
390 99
        if ($addRoot) {
391 30
            if ($this->defaultRootNamespace) {
392
                $rootNode = $doc->createElementNS($this->defaultRootNamespace, $this->defaultRootName);
393
            } else {
394 30
                $rootNode = $doc->createElement($this->defaultRootName);
395
            }
396 30
            $this->setCurrentNode($rootNode);
397 30
            $doc->appendChild($rootNode);
398 30
        }
399
400 99
        return $doc;
401
    }
402
403 101
    public function prepare($data)
404
    {
405 101
        $this->nullWasVisited = false;
406
407 101
        return $data;
408
    }
409
410 27
    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...
411
    {
412 27
        if (null === $this->document) {
413 5
            $this->document = $this->createDocument(null, null, true);
414 5
            $this->currentNode->appendChild($textNode = $this->document->createTextNode((string) $data));
415
416 5
            return $textNode;
417
        }
418
419 22
        return $this->document->createTextNode((string) $data);
420
    }
421
422
    /**
423
     * Checks that the name is a valid XML element name.
424
     *
425
     * @param string $name
426
     *
427
     * @return boolean
428
     */
429 2
    private function isElementNameValid($name)
430
    {
431 2
        return $name && false === strpos($name, ' ') && preg_match('#^[\pL_][\pL0-9._-]*$#ui', $name);
432
    }
433
434 9
    private function attachNullNamespace()
435
    {
436 9
        if ( ! $this->nullWasVisited) {
437 9
            $this->document->documentElement->setAttributeNS(
438 9
                'http://www.w3.org/2000/xmlns/',
439 9
                'xmlns:xsi',
440
                'http://www.w3.org/2001/XMLSchema-instance'
441 9
            );
442 9
            $this->nullWasVisited = true;
443 9
        }
444 9
    }
445
446
    /**
447
     * Adds namespace attributes to the XML root element
448
     *
449
     * @param \JMS\Serializer\Metadata\ClassMetadata $metadata
450
     * @param \DOMElement $element
451
     */
452 67
    private function addNamespaceAttributes(ClassMetadata $metadata, \DOMElement $element)
453
    {
454 67
        foreach ($metadata->xmlNamespaces as $prefix => $uri) {
455 8
            $attribute = 'xmlns';
456 8
            if ($prefix !== '') {
457 8
                $attribute .= ':'.$prefix;
458 8
            } elseif ($element->namespaceURI === $uri) {
459 4
                continue;
460
            }
461 8
            $element->setAttributeNS('http://www.w3.org/2000/xmlns/', $attribute, $uri);
462 67
        }
463 67
    }
464
465 66
    private function createElement($tagName, $namespace = null)
466
    {
467 66
        if (null === $namespace) {
468 61
            return $this->document->createElement($tagName);
469
        }
470 8
        if ($this->currentNode->isDefaultNamespace($namespace)) {
471 5
            return $this->document->createElementNS($namespace, $tagName);
472
        }
473 8
        if (!($prefix = $this->currentNode->lookupPrefix($namespace)) && !($prefix = $this->document->lookupPrefix($namespace))) {
474 3
            $prefix = 'ns-'.  substr(sha1($namespace), 0, 8);
475 3
        }
476 8
        return $this->document->createElementNS($namespace, $prefix . ':' . $tagName);
477
    }
478
479 13
    private function setAttributeOnNode(\DOMElement $node, $name, $value, $namespace = null)
480
    {
481 13
        if (null !== $namespace) {
482 4
            if (!$prefix = $node->lookupPrefix($namespace)) {
483 2
                $prefix = 'ns-'.  substr(sha1($namespace), 0, 8);
484 2
            }
485 4
            $node->setAttributeNS($namespace, $prefix.':'.$name, $value);
486 4
        } else {
487 11
            $node->setAttribute($name, $value);
488
        }
489 13
    }
490
491 58
    private function getClassDefaultNamespace(ClassMetadata $metadata)
492
    {
493 58
        return (isset($metadata->xmlNamespaces[''])?$metadata->xmlNamespaces['']:null);
494
    }
495
496
    /**
497
     * @return bool
498
     */
499 99
    public function isFormatOutput()
500
    {
501 99
        return $this->formatOutput;
502
    }
503
504
    /**
505
     * @param bool $formatOutput
506
     */
507 1
    public function setFormatOutput($formatOutput)
508
    {
509 1
        $this->formatOutput = (boolean) $formatOutput;
510 1
    }
511
}
512