Completed
Pull Request — master (#859)
by
unknown
04:25
created

XmlSerializationVisitor::endVisitingObject()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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