Completed
Push — master ( 0d7a9a...ac7eed )
by Asmir
07:43
created

XmlSerializationVisitor::visitBoolean()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 11
c 0
b 0
f 0
ccs 6
cts 6
cp 1
rs 9.2
cc 4
eloc 6
nc 2
nop 3
crap 4
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 379
    public function __construct(PropertyNamingStrategyInterface $namingStrategy, AccessorStrategyInterface $accessorStrategy = null)
53
    {
54 379
        parent::__construct($namingStrategy, $accessorStrategy);
55 379
        $this->objectMetadataStack = new \SplStack;
56 379
        $this->formatOutput = true;
57 379
    }
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 107
    public function setNavigator(GraphNavigator $navigator)
84
    {
85 107
        $this->navigator = $navigator;
86 107
        $this->document = null;
87 107
        $this->stack = new \SplStack;
88 107
        $this->metadataStack = new \SplStack;
89 107
    }
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 73
    public function visitString($data, array $type, Context $context)
117
    {
118
119 73
        if (null !== $this->currentMetadata) {
120 65
            $doCData = $this->currentMetadata->xmlElementCData;
121 65
        } else {
122 9
            $doCData = true;
123
        }
124
125 73
        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 68
        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 73
    public function startVisitingObject(ClassMetadata $metadata, $data, array $type, Context $context)
204
    {
205 73
        $this->objectMetadataStack->push($metadata);
206
207 73
        if (null === $this->document) {
208 71
            $this->document = $this->createDocument(null, null, false);
209 71
            if ($metadata->xmlRootName) {
210 19
                $rootName = $metadata->xmlRootName;
211 19
                $rootNamespace = $metadata->xmlRootNamespace?:$this->getClassDefaultNamespace($metadata);
212 19
            } else {
213 52
                $rootName = $this->defaultRootName;
214 52
                $rootNamespace = $this->defaultRootNamespace;
215
            }
216
217 71
            if ($rootNamespace) {
218 7
                $this->currentNode = $this->document->createElementNS($rootNamespace, $rootName);
219 7
            } else {
220 64
                $this->currentNode = $this->document->createElement($rootName);
221
            }
222
223 71
            $this->document->appendChild($this->currentNode);
224 71
        }
225
226 73
        $this->addNamespaceAttributes($metadata, $this->currentNode);
227
228 73
        $this->hasValue = false;
229 73
    }
230
231 73
    public function visitProperty(PropertyMetadata $metadata, $object, Context $context)
232
    {
233 73
        $v = $this->accessor->getValue($object, $metadata);
234
235 72
        if (null === $v && $context->shouldSerializeNull() !== true) {
236 8
            return;
237
        }
238
239 72
        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 70
        if (($metadata->xmlValue && $this->currentNode->childNodes->length > 0)
254 70
            || ( ! $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 70
        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 66
        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 64
        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 63
            $elementName = $this->namingStrategy->translateName($metadata);
296
297 63
            $namespace = null !== $metadata->xmlNamespace
298 63
                ? $metadata->xmlNamespace
299 63
                : $this->getClassDefaultNamespace($this->objectMetadataStack->top());
300
301 63
            $element = $this->createElement($elementName, $namespace);
302 63
            $this->currentNode->appendChild($element);
303 63
            $this->setCurrentNode($element);
304 63
        }
305
306 64
        $this->setCurrentMetadata($metadata);
307
308 64
        if (null !== $node = $this->navigator->accept($v, $metadata->type, $context)) {
309 53
            $this->currentNode->appendChild($node);
310 53
        }
311
312 64
        $this->revertCurrentMetadata();
313
314 64
        if ($addEnclosingElement) {
315 63
            $this->revertCurrentNode();
316
317 63
            if ($this->isElementEmpty($element) && ($v === null || $this->isSkippableCollection($metadata) || $this->isSkippableEmptyObject($node, $metadata) || $this->isCircularRef($context, $v))) {
0 ignored issues
show
Compatibility introduced by
$context of type object<JMS\Serializer\Context> is not a sub-type of object<JMS\Serializer\SerializationContext>. It seems like you assume a child class of the class JMS\Serializer\Context to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
318 5
                $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 5
            }
320 63
        }
321
322 64
        $this->hasValue = false;
323 64
    }
324
325 64
    private function isInLineCollection(PropertyMetadata $metadata)
326
    {
327 64
        return $metadata->xmlCollection && $metadata->xmlCollectionInline;
328
    }
329
330 4
    private function isCircularRef(SerializationContext $context, $v)
331
    {
332 4
        return $context->isVisiting($v);
333
    }
334
335 6
    private function isSkippableEmptyObject($node, PropertyMetadata $metadata)
336
    {
337 6
        return $node === null && !$metadata->xmlCollection && $metadata->skipWhenEmpty;
338
    }
339
340 7
    private function isSkippableCollection(PropertyMetadata $metadata)
341
    {
342 7
        return $metadata->xmlCollection && $metadata->xmlCollectionSkipWhenEmpty;
343
    }
344
345 63
    private function isElementEmpty(\DOMElement $element)
346
    {
347 63
        return !$element->hasChildNodes() && !$element->hasAttributes();
348
    }
349
350 70
    public function endVisitingObject(ClassMetadata $metadata, $data, array $type, Context $context)
351
    {
352 70
        $this->objectMetadataStack->pop();
353 70
    }
354
355 102
    public function getResult()
356
    {
357 102
        return $this->document->saveXML();
358
    }
359
360 2
    public function getCurrentNode()
361
    {
362 2
        return $this->currentNode;
363
    }
364
365
    public function getCurrentMetadata()
366
    {
367
        return $this->currentMetadata;
368
    }
369
370
    public function getDocument()
371
    {
372
        return $this->document;
373
    }
374
375 71
    public function setCurrentMetadata(PropertyMetadata $metadata)
376
    {
377 71
        $this->metadataStack->push($this->currentMetadata);
378 71
        $this->currentMetadata = $metadata;
379 71
    }
380
381 95
    public function setCurrentNode(\DOMNode $node)
382
    {
383 95
        $this->stack->push($this->currentNode);
384 95
        $this->currentNode = $node;
385 95
    }
386
387 71
    public function revertCurrentNode()
388
    {
389 71
        return $this->currentNode = $this->stack->pop();
390
    }
391
392 71
    public function revertCurrentMetadata()
393
    {
394 71
        return $this->currentMetadata = $this->metadataStack->pop();
395
    }
396
397 105
    public function createDocument($version = null, $encoding = null, $addRoot = true)
398
    {
399 105
        $doc = new \DOMDocument($version ?: $this->defaultVersion, $encoding ?: $this->defaultEncoding);
400 105
        $doc->formatOutput = $this->isFormatOutput();
401
402 105
        if ($addRoot) {
403 30
            if ($this->defaultRootNamespace) {
404
                $rootNode = $doc->createElementNS($this->defaultRootNamespace, $this->defaultRootName);
405
            } else {
406 30
                $rootNode = $doc->createElement($this->defaultRootName);
407
            }
408 30
            $this->setCurrentNode($rootNode);
409 30
            $doc->appendChild($rootNode);
410 30
        }
411
412 105
        return $doc;
413
    }
414
415 107
    public function prepare($data)
416
    {
417 107
        $this->nullWasVisited = false;
418
419 107
        return $data;
420
    }
421
422 28
    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...
423
    {
424 28
        if (null === $this->document) {
425 5
            $this->document = $this->createDocument(null, null, true);
426 5
            $this->currentNode->appendChild($textNode = $this->document->createTextNode((string) $data));
427
428 5
            return $textNode;
429
        }
430
431 23
        return $this->document->createTextNode((string) $data);
432
    }
433
434
    /**
435
     * Checks that the name is a valid XML element name.
436
     *
437
     * @param string $name
438
     *
439
     * @return boolean
440
     */
441 2
    private function isElementNameValid($name)
442
    {
443 2
        return $name && false === strpos($name, ' ') && preg_match('#^[\pL_][\pL0-9._-]*$#ui', $name);
444
    }
445
446 9
    private function attachNullNamespace()
447
    {
448 9
        if ( ! $this->nullWasVisited) {
449 9
            $this->document->documentElement->setAttributeNS(
450 9
                'http://www.w3.org/2000/xmlns/',
451 9
                'xmlns:xsi',
452
                'http://www.w3.org/2001/XMLSchema-instance'
453 9
            );
454 9
            $this->nullWasVisited = true;
455 9
        }
456 9
    }
457
458
    /**
459
     * Adds namespace attributes to the XML root element
460
     *
461
     * @param \JMS\Serializer\Metadata\ClassMetadata $metadata
462
     * @param \DOMElement $element
463
     */
464 73
    private function addNamespaceAttributes(ClassMetadata $metadata, \DOMElement $element)
465
    {
466 73
        foreach ($metadata->xmlNamespaces as $prefix => $uri) {
467 9
            $attribute = 'xmlns';
468 9
            if ($prefix !== '') {
469 9
                $attribute .= ':'.$prefix;
470 9
            } elseif ($element->namespaceURI === $uri) {
471 4
                continue;
472
            }
473 9
            $element->setAttributeNS('http://www.w3.org/2000/xmlns/', $attribute, $uri);
474 73
        }
475 73
    }
476
477 71
    private function createElement($tagName, $namespace = null)
478
    {
479 71
        if (null === $namespace) {
480 65
            return $this->document->createElement($tagName);
481
        }
482 9
        if ($this->currentNode->isDefaultNamespace($namespace)) {
483 5
            return $this->document->createElementNS($namespace, $tagName);
484
        }
485 9
        if (!($prefix = $this->currentNode->lookupPrefix($namespace)) && !($prefix = $this->document->lookupPrefix($namespace))) {
486 3
            $prefix = 'ns-'.  substr(sha1($namespace), 0, 8);
487 3
        }
488 9
        return $this->document->createElementNS($namespace, $prefix . ':' . $tagName);
489
    }
490
491 13
    private function setAttributeOnNode(\DOMElement $node, $name, $value, $namespace = null)
492
    {
493 13
        if (null !== $namespace) {
494 4
            if (!$prefix = $node->lookupPrefix($namespace)) {
495 2
                $prefix = 'ns-'.  substr(sha1($namespace), 0, 8);
496 2
            }
497 4
            $node->setAttributeNS($namespace, $prefix.':'.$name, $value);
498 4
        } else {
499 11
            $node->setAttribute($name, $value);
500
        }
501 13
    }
502
503 62
    private function getClassDefaultNamespace(ClassMetadata $metadata)
504
    {
505 62
        return (isset($metadata->xmlNamespaces[''])?$metadata->xmlNamespaces['']:null);
506
    }
507
508
    /**
509
     * @return bool
510
     */
511 105
    public function isFormatOutput()
512
    {
513 105
        return $this->formatOutput;
514
    }
515
516
    /**
517
     * @param bool $formatOutput
518
     */
519 1
    public function setFormatOutput($formatOutput)
520
    {
521 1
        $this->formatOutput = (boolean) $formatOutput;
522 1
    }
523
}
524