Completed
Push — master ( 3d958d...523004 )
by Asmir
12s
created

XmlSerializationVisitor::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1.125

Importance

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