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