Completed
Push — master ( e1c4bf...7b4b35 )
by Johannes
10s
created

XmlSerializationVisitor::createElement()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 5

Importance

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