Completed
Pull Request — master (#743)
by Asmir
07:41
created

XmlSerializationVisitor::revertCurrentMetadata()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
c 0
b 0
f 0
ccs 2
cts 2
cp 1
rs 10
cc 1
eloc 2
nc 1
nop 0
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\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\AdvancedNamingStrategyInterface;
26
27
/**
28
 * XmlSerializationVisitor.
29
 *
30
 * @author Johannes M. Schmitt <[email protected]>
31
 */
32
class XmlSerializationVisitor extends AbstractVisitor implements SerializationVisitorInterface
33
{
34
    /**
35
     * @var \DOMDocument
36
     */
37
    private $document;
38
39
    private $navigator;
40
    private $defaultRootName = 'result';
41
    private $defaultRootNamespace;
42
    private $defaultVersion = '1.0';
43
    private $defaultEncoding = 'UTF-8';
44
    private $stack;
45
    private $metadataStack;
46
    private $currentNode;
47
    private $currentMetadata;
48
    private $hasValue;
49
    private $nullWasVisited;
50
    private $objectMetadataStack;
51
52
    /** @var boolean */
53
    private $formatOutput;
54
55 381
    public function __construct($namingStrategy, AccessorStrategyInterface $accessorStrategy = null)
56
    {
57 381
        parent::__construct($namingStrategy, $accessorStrategy);
58 381
        $this->objectMetadataStack = new \SplStack;
59 381
        $this->formatOutput = true;
60 381
    }
61
62
    public function setDefaultRootName($name, $namespace = null)
63
    {
64
        $this->defaultRootName = $name;
65
        $this->defaultRootNamespace = $namespace;
66
    }
67
68
    /**
69
     * @return boolean
70
     */
71
    public function hasDefaultRootName()
72
    {
73
        return 'result' === $this->defaultRootName;
74
    }
75
76
    public function setDefaultVersion($version)
77
    {
78
        $this->defaultVersion = $version;
79
    }
80
81
    public function setDefaultEncoding($encoding)
82
    {
83
        $this->defaultEncoding = $encoding;
84
    }
85
86 105
    public function setNavigator(GraphNavigatorInterface $navigator): void
87
    {
88 105
        $this->navigator = $navigator;
89 105
        $this->stack = new \SplStack;
90 105
        $this->metadataStack = new \SplStack;
91
92 105
        $this->currentNode = null;
93 105
        $this->nullWasVisited = false;
94
95 105
        $this->document = new \DOMDocument($this->defaultVersion, $this->defaultEncoding);
96 105
        $this->document->formatOutput = $this->isFormatOutput();
97 105
    }
98
99 101
    public function createRoot(ClassMetadata $metadata = null, $rootName = null, $rootNamespace = null)
100
    {
101 101
        if ($metadata !== null && !empty($metadata->xmlRootName)) {
102 20
            $rootName = $metadata->xmlRootName;
103 20
            $rootNamespace = $metadata->xmlRootNamespace ?: $this->getClassDefaultNamespace($metadata);
104
        } else {
105 81
            $rootName = $rootName ?: $this->defaultRootName;
106 81
            $rootNamespace = $rootNamespace ?: $this->defaultRootNamespace;
107
        }
108
109 101
        if ($rootNamespace) {
110 8
            $rootNode = $this->document->createElementNS($rootNamespace, $rootName);
111
        } else {
112 93
            $rootNode = $this->document->createElement($rootName);
113
        }
114 101
        $this->document->appendChild($rootNode);
115 101
        $this->setCurrentNode($rootNode);
116
117 101
        return $rootNode;
118
    }
119
120 9
    public function visitNull($data, array $type, SerializationContext $context)
121
    {
122 9
        $node = $this->document->createAttribute('xsi:nil');
123 9
        $node->value = 'true';
124 9
        $this->nullWasVisited = true;
125
126 9
        return $node;
127
    }
128
129 73
    public function visitString(string $data, array $type, SerializationContext $context)
130
    {
131 73
        $doCData = null !== $this->currentMetadata ? $this->currentMetadata->xmlElementCData : true;
132
133 73
        return $doCData ? $this->document->createCDATASection($data) : $this->document->createTextNode((string)$data);
134
    }
135
136 2
    public function visitSimpleString($data, array $type, SerializationContext $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...
137
    {
138 2
        return $this->document->createTextNode((string)$data);
139
    }
140
141 5
    public function visitBoolean(bool $data, array $type, SerializationContext $context)
142
    {
143 5
        return $this->document->createTextNode($data ? 'true' : 'false');
144
    }
145
146 20
    public function visitInteger(int $data, array $type, SerializationContext $context)
147
    {
148 20
        return $this->visitNumeric($data, $type);
149
    }
150
151 9
    public function visitDouble(float $data, array $type, SerializationContext $context)
152
    {
153 9
        return $this->visitNumeric($data, $type);
154
    }
155
156 28
    public function visitArray(array $data, array $type, SerializationContext $context)
157
    {
158 28
        if ($this->currentNode === null) {
159 9
            $this->createRoot();
160
        }
161
162 28
        $entryName = (null !== $this->currentMetadata && null !== $this->currentMetadata->xmlEntryName) ? $this->currentMetadata->xmlEntryName : 'entry';
163 28
        $keyAttributeName = (null !== $this->currentMetadata && null !== $this->currentMetadata->xmlKeyAttribute) ? $this->currentMetadata->xmlKeyAttribute : null;
164 28
        $namespace = (null !== $this->currentMetadata && null !== $this->currentMetadata->xmlEntryNamespace) ? $this->currentMetadata->xmlEntryNamespace : null;
165
166 28
        $elType = $this->getElementType($type);
167 28
        foreach ($data as $k => $v) {
168
169 27
            if (null === $v && $context->shouldSerializeNull() !== true) {
170 1
                continue;
171
            }
172
173 27
            $tagName = (null !== $this->currentMetadata && $this->currentMetadata->xmlKeyValuePairs && $this->isElementNameValid($k)) ? $k : $entryName;
174
175 27
            $entryNode = $this->createElement($tagName, $namespace);
176 27
            $this->currentNode->appendChild($entryNode);
0 ignored issues
show
Bug introduced by
The method appendChild cannot be called on $this->currentNode (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...
177 27
            $this->setCurrentNode($entryNode);
178
179 27
            if (null !== $keyAttributeName) {
180 7
                $entryNode->setAttribute($keyAttributeName, (string)$k);
181
            }
182
183 27
            if (null !== $node = $this->navigator->accept($v, $elType, $context)) {
184 18
                $this->currentNode->appendChild($node);
0 ignored issues
show
Bug introduced by
The method appendChild cannot be called on $this->currentNode (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...
185
            }
186
187 27
            $this->revertCurrentNode();
188
        }
189 28
    }
190
191 73
    public function startVisitingObject(ClassMetadata $metadata, $data, array $type, SerializationContext $context): void
192
    {
193 73
        $this->objectMetadataStack->push($metadata);
194
195 73
        if ($this->currentNode === null) {
196 71
            $this->createRoot($metadata);
197
        }
198
199 73
        $this->addNamespaceAttributes($metadata, $this->currentNode);
0 ignored issues
show
Documentation introduced by
$this->currentNode is of type null, but the function expects a object<DOMElement>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
200
201 73
        $this->hasValue = false;
202 73
    }
203
204 73
    public function visitProperty(PropertyMetadata $metadata, $object, SerializationContext $context): void
205
    {
206 73
        $v = $this->accessor->getValue($object, $metadata);
207
208 72
        if (null === $v && $context->shouldSerializeNull() !== true) {
209 9
            return;
210
        }
211
212 72
        if ($metadata->xmlAttribute) {
213 13
            $this->setCurrentMetadata($metadata);
214 13
            $node = $this->navigator->accept($v, $metadata->type, $context);
215 13
            $this->revertCurrentMetadata();
216
217 13
            if (!$node instanceof \DOMCharacterData) {
218
                throw new RuntimeException(sprintf('Unsupported value for XML attribute for %s. Expected character data, but got %s.', $metadata->name, json_encode($v)));
219
            }
220 13
            if ($this->namingStrategy instanceof AdvancedNamingStrategyInterface) {
221
                $attributeName = $this->namingStrategy->getPropertyName($metadata, $context);
222
            } else {
223 13
                $attributeName = $this->namingStrategy->translateName($metadata);
224
            }
225 13
            $this->setAttributeOnNode($this->currentNode, $attributeName, $node->nodeValue, $metadata->xmlNamespace);
0 ignored issues
show
Documentation introduced by
$this->currentNode is of type null, but the function expects a object<DOMElement>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
226
227 13
            return;
228
        }
229
230 70
        if (($metadata->xmlValue && $this->currentNode->childNodes->length > 0)
231 70
            || (!$metadata->xmlValue && $this->hasValue)
232
        ) {
233 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));
234
        }
235
236 70
        if ($metadata->xmlValue) {
237 9
            $this->hasValue = true;
238
239 9
            $this->setCurrentMetadata($metadata);
240 9
            $node = $this->navigator->accept($v, $metadata->type, $context);
241 9
            $this->revertCurrentMetadata();
242
243 9
            if (!$node instanceof \DOMCharacterData) {
244
                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)));
245
            }
246
247 9
            $this->currentNode->appendChild($node);
0 ignored issues
show
Bug introduced by
The method appendChild cannot be called on $this->currentNode (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...
248
249 9
            return;
250
        }
251
252 66
        if ($metadata->xmlAttributeMap) {
253 2
            if (!\is_array($v)) {
254 1
                throw new RuntimeException(sprintf('Unsupported value type for XML attribute map. Expected array but got %s.', \gettype($v)));
255
            }
256
257 1
            foreach ($v as $key => $value) {
258 1
                $this->setCurrentMetadata($metadata);
259 1
                $node = $this->navigator->accept($value, null, $context);
260 1
                $this->revertCurrentMetadata();
261
262 1
                if (!$node instanceof \DOMCharacterData) {
263
                    throw new RuntimeException(sprintf('Unsupported value for a XML attribute map value. Expected character data, but got %s.', json_encode($v)));
264
                }
265
266 1
                $this->setAttributeOnNode($this->currentNode, $key, $node->nodeValue, $metadata->xmlNamespace);
0 ignored issues
show
Documentation introduced by
$this->currentNode is of type null, but the function expects a object<DOMElement>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
267
            }
268
269 1
            return;
270
        }
271
272 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...
273 63
            if ($this->namingStrategy instanceof AdvancedNamingStrategyInterface) {
274
                $elementName = $this->namingStrategy->getPropertyName($metadata, $context);
275
            } else {
276 63
                $elementName = $this->namingStrategy->translateName($metadata);
277
            }
278
279 63
            $namespace = null !== $metadata->xmlNamespace
280 9
                ? $metadata->xmlNamespace
281 63
                : $this->getClassDefaultNamespace($this->objectMetadataStack->top());
282
283 63
            $element = $this->createElement($elementName, $namespace);
284 63
            $this->currentNode->appendChild($element);
0 ignored issues
show
Bug introduced by
The method appendChild cannot be called on $this->currentNode (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...
285 63
            $this->setCurrentNode($element);
286
        }
287
288 64
        $this->setCurrentMetadata($metadata);
289
290 64
        if (null !== $node = $this->navigator->accept($v, $metadata->type, $context)) {
291 52
            $this->currentNode->appendChild($node);
0 ignored issues
show
Bug introduced by
The method appendChild cannot be called on $this->currentNode (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...
292
        }
293
294 64
        $this->revertCurrentMetadata();
295
296 64
        if ($addEnclosingElement) {
297 63
            $this->revertCurrentNode();
298
299 63
            if ($this->isElementEmpty($element) && ($v === null || $this->isSkippableCollection($metadata) || $this->isSkippableEmptyObject($node, $metadata) || $this->isCircularRef($context, $v))) {
300 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...
Bug introduced by
The method removeChild cannot be called on $this->currentNode (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...
301
            }
302
        }
303
304 64
        $this->hasValue = false;
305 64
    }
306
307 64
    private function isInLineCollection(PropertyMetadata $metadata)
308
    {
309 64
        return $metadata->xmlCollection && $metadata->xmlCollectionInline;
310
    }
311
312 4
    private function isCircularRef(SerializationContext $context, $v)
313
    {
314 4
        return $context->isVisiting($v);
315
    }
316
317 6
    private function isSkippableEmptyObject($node, PropertyMetadata $metadata)
318
    {
319 6
        return $node === null && !$metadata->xmlCollection && $metadata->skipWhenEmpty;
320
    }
321
322 7
    private function isSkippableCollection(PropertyMetadata $metadata)
323
    {
324 7
        return $metadata->xmlCollection && $metadata->xmlCollectionSkipWhenEmpty;
325
    }
326
327 63
    private function isElementEmpty(\DOMElement $element)
328
    {
329 63
        return !$element->hasChildNodes() && !$element->hasAttributes();
330
    }
331
332 70
    public function endVisitingObject(ClassMetadata $metadata, $data, array $type, SerializationContext $context)
333
    {
334 70
        $this->objectMetadataStack->pop();
335 70
    }
336
337 100
    public function getResult($node)
338
    {
339 100
        if ($this->document->documentElement === null) {
340 21
            if ($node instanceof \DOMElement) {
341 1
                $this->document->appendChild($node);
342
            } else {
343 20
                $this->createRoot();
344 20
                if ($node) {
345 20
                    $this->document->documentElement->appendChild($node);
346
                }
347
            }
348
        }
349
350 100
        if ($this->nullWasVisited) {
351 9
            $this->document->documentElement->setAttributeNS(
352 9
                'http://www.w3.org/2000/xmlns/',
353 9
                'xmlns:xsi',
354 9
                'http://www.w3.org/2001/XMLSchema-instance'
355
            );
356
        }
357 100
        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 4
    public function getDocument()
371
    {
372 4
        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 102
    public function setCurrentNode(\DOMNode $node)
382
    {
383 102
        $this->stack->push($this->currentNode);
384 102
        $this->currentNode = $node;
385 102
    }
386
387 1
    public function setCurrentAndRootNode(\DOMNode $node)
388
    {
389 1
        $this->setCurrentNode($node);
390 1
        $this->document->appendChild($node);
391 1
    }
392
393 71
    public function revertCurrentNode()
394
    {
395 71
        return $this->currentNode = $this->stack->pop();
396
    }
397
398 71
    public function revertCurrentMetadata()
399
    {
400 71
        return $this->currentMetadata = $this->metadataStack->pop();
401
    }
402
403 105
    public function prepare($data)
404
    {
405 105
        $this->nullWasVisited = false;
406
407 105
        return $data;
408
    }
409
410 29
    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 29
        return $this->document->createTextNode((string)$data);
413
    }
414
415
    /**
416
     * Checks that the name is a valid XML element name.
417
     *
418
     * @param string $name
419
     *
420
     * @return boolean
421
     */
422 2
    private function isElementNameValid($name)
423
    {
424 2
        return $name && false === strpos($name, ' ') && preg_match('#^[\pL_][\pL0-9._-]*$#ui', $name);
425
    }
426
427
    /**
428
     * Adds namespace attributes to the XML root element
429
     *
430
     * @param \JMS\Serializer\Metadata\ClassMetadata $metadata
431
     * @param \DOMElement $element
432
     */
433 73
    private function addNamespaceAttributes(ClassMetadata $metadata, \DOMElement $element)
434
    {
435 73
        foreach ($metadata->xmlNamespaces as $prefix => $uri) {
436 10
            $attribute = 'xmlns';
437 10
            if ($prefix !== '') {
438 10
                $attribute .= ':' . $prefix;
439 7
            } elseif ($element->namespaceURI === $uri) {
440 5
                continue;
441
            }
442 10
            $element->setAttributeNS('http://www.w3.org/2000/xmlns/', $attribute, $uri);
443
        }
444 73
    }
445
446 71
    private function createElement($tagName, $namespace = null)
447
    {
448 71
        if (null === $namespace) {
449 65
            return $this->document->createElement($tagName);
450
        }
451 10
        if ($this->currentNode->isDefaultNamespace($namespace)) {
452 6
            return $this->document->createElementNS($namespace, $tagName);
453
        }
454 9
        if (!($prefix = $this->currentNode->lookupPrefix($namespace)) && !($prefix = $this->document->lookupPrefix($namespace))) {
455 3
            $prefix = 'ns-' . substr(sha1($namespace), 0, 8);
456
        }
457 9
        return $this->document->createElementNS($namespace, $prefix . ':' . $tagName);
458
    }
459
460 14
    private function setAttributeOnNode(\DOMElement $node, $name, $value, $namespace = null)
461
    {
462 14
        if (null !== $namespace) {
463 4
            if (!$prefix = $node->lookupPrefix($namespace)) {
464 2
                $prefix = 'ns-' . substr(sha1($namespace), 0, 8);
465
            }
466 4
            $node->setAttributeNS($namespace, $prefix . ':' . $name, $value);
467
        } else {
468 12
            $node->setAttribute($name, $value);
469
        }
470 14
    }
471
472 62
    private function getClassDefaultNamespace(ClassMetadata $metadata)
473
    {
474 62
        return (isset($metadata->xmlNamespaces['']) ? $metadata->xmlNamespaces[''] : null);
475
    }
476
477
    /**
478
     * @return bool
479
     */
480 105
    public function isFormatOutput()
481
    {
482 105
        return $this->formatOutput;
483
    }
484
485
    /**
486
     * @param bool $formatOutput
487
     */
488 1
    public function setFormatOutput($formatOutput)
489
    {
490 1
        $this->formatOutput = (boolean)$formatOutput;
491 1
    }
492
}
493