Completed
Push — master ( 7b4b35...e0704d )
by Johannes
9s
created

XmlSerializationVisitor   D

Complexity

Total Complexity 107

Size/Duplication

Total Lines 449
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Test Coverage

Coverage 90.32%

Importance

Changes 18
Bugs 5 Features 3
Metric Value
wmc 107
c 18
b 5
f 3
lcom 1
cbo 7
dl 0
loc 449
ccs 224
cts 248
cp 0.9032
rs 4.8717

35 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A setDefaultRootName() 0 5 1
B visitString() 0 18 5
A visitSimpleString() 0 11 2
F visitArray() 0 28 14
B startVisitingObject() 0 27 5
A hasDefaultRootName() 0 4 1
A setDefaultVersion() 0 4 1
A setDefaultEncoding() 0 4 1
A setNavigator() 0 7 1
A getNavigator() 0 4 1
A visitNull() 0 19 2
A visitBoolean() 0 11 4
A visitInteger() 0 4 1
A visitDouble() 0 4 1
D visitProperty() 0 93 28
A nodeNotEmpty() 0 4 2
A endVisitingObject() 0 4 1
A getResult() 0 4 1
A getCurrentNode() 0 4 1
A getCurrentMetadata() 0 4 1
A getDocument() 0 4 1
A setCurrentMetadata() 0 5 1
A setCurrentNode() 0 5 1
A revertCurrentNode() 0 4 1
A revertCurrentMetadata() 0 4 1
B createDocument() 0 17 5
A prepare() 0 6 1
A visitNumeric() 0 11 2
A isElementNameValid() 0 4 3
A attachNullNamespace() 0 11 2
A addNamespaceAttributes() 0 12 4
B createElement() 0 14 5
A setAttributeOnNode() 0 11 3
A getClassDefaultNamespace() 0 4 2

How to fix   Complexity   

Complex Class

Complex classes like XmlSerializationVisitor often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use XmlSerializationVisitor, and based on these observations, apply Extract Interface, too.

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
            } elseif ($element->namespaceURI === $uri) {
440 3
                continue;
441
            }
442 4
            $element->setAttributeNS('http://www.w3.org/2000/xmlns/', $attribute, $uri);
443 50
        }
444 50
    }
445
446 49
    private function createElement($tagName, $namespace = null)
447
    {
448 49
        if (null === $namespace) {
449 47
            return $this->document->createElement($tagName);
450
        }
451 4
        if ($this->currentNode->isDefaultNamespace($namespace)) {
452 2
            return $this->document->createElementNS($namespace, $tagName);
453
        }
454 4
        if (!($prefix = $this->currentNode->lookupPrefix($namespace)) && !($prefix = $this->document->lookupPrefix($namespace))) {
455 1
            $prefix = 'ns-'.  substr(sha1($namespace), 0, 8);
456 1
            return $this->document->createElementNS($namespace, $prefix . ':' . $tagName);
457
        }
458 4
        return $this->document->createElement($prefix . ':' . $tagName);
459
    }
460
461 11
    private function setAttributeOnNode(\DOMElement $node, $name, $value, $namespace = null)
462
    {
463 11
        if (null !== $namespace) {
464 3
            if (!$prefix = $node->lookupPrefix($namespace)) {
465 2
                $prefix = 'ns-'.  substr(sha1($namespace), 0, 8);
466 2
            }
467 3
            $node->setAttributeNS($namespace, $prefix.':'.$name, $value);
468 3
        } else {
469 10
            $node->setAttribute($name, $value);
470
        }
471 11
    }
472
473 44
    private function getClassDefaultNamespace(ClassMetadata $metadata)
474
    {
475 44
        return (isset($metadata->xmlNamespaces[''])?$metadata->xmlNamespaces['']:null);
476
    }
477
478
}
479