Completed
Push — master ( 2b1686...37cc39 )
by Johannes
11s
created

XmlSerializationVisitor::setAttributeOnNode()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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