Completed
Push — master ( 730ad5...1d8660 )
by Asmir
13s
created

XmlSerializationVisitor::isElementNameValid()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 3

Importance

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