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

XmlDeserializationVisitor   C

Complexity

Total Complexity 66

Size/Duplication

Total Lines 347
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 8

Test Coverage

Coverage 87.88%

Importance

Changes 11
Bugs 2 Features 1
Metric Value
wmc 66
c 11
b 2
f 1
lcom 2
cbo 8
dl 0
loc 347
ccs 174
cts 198
cp 0.8788
rs 5.7474

23 Methods

Rating   Name   Duplication   Size   Complexity  
A setNavigator() 0 8 1
B prepare() 0 26 4
B visitBoolean() 0 18 6
F visitArray() 0 64 19
A startVisitingObject() 0 8 2
A enableExternalEntities() 0 4 1
A getNavigator() 0 4 1
A visitNull() 0 4 1
A visitString() 0 10 2
A visitInteger() 0 10 2
A visitDouble() 0 10 2
C visitProperty() 0 66 11
A endVisitingObject() 0 8 1
A setCurrentObject() 0 5 1
A getCurrentObject() 0 4 1
A revertCurrentObject() 0 4 1
A setCurrentMetadata() 0 5 1
A getCurrentMetadata() 0 4 1
A revertCurrentMetadata() 0 4 1
A getResult() 0 4 1
A setDoctypeWhitelist() 0 4 1
A getDoctypeWhitelist() 0 4 1
A getDomDocumentTypeEntitySubset() 0 21 4

How to fix   Complexity   

Complex Class

Complex classes like XmlDeserializationVisitor 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 XmlDeserializationVisitor, 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\XmlErrorException;
22
use JMS\Serializer\Exception\LogicException;
23
use JMS\Serializer\Exception\InvalidArgumentException;
24
use JMS\Serializer\Exception\RuntimeException;
25
use JMS\Serializer\Metadata\PropertyMetadata;
26
use JMS\Serializer\Metadata\ClassMetadata;
27
28
class XmlDeserializationVisitor extends AbstractVisitor
29
{
30
    private $objectStack;
31
    private $metadataStack;
32
    private $objectMetadataStack;
33
    private $currentObject;
34
    private $currentMetadata;
35
    private $result;
36
    private $navigator;
37
    private $disableExternalEntities = true;
38
    private $doctypeWhitelist = array();
39
40
    public function enableExternalEntities()
41
    {
42
        $this->disableExternalEntities = false;
43
    }
44
45 49
    public function setNavigator(GraphNavigator $navigator)
46
    {
47 49
        $this->navigator = $navigator;
48 49
        $this->objectStack = new \SplStack;
49 49
        $this->metadataStack = new \SplStack;
50 49
        $this->objectMetadataStack = new \SplStack;
51 49
        $this->result = null;
52 49
    }
53
54 1
    public function getNavigator()
55
    {
56 1
        return $this->navigator;
57
    }
58
59 51
    public function prepare($data)
60
    {
61 51
        $previous = libxml_use_internal_errors(true);
62 51
        $previousEntityLoaderState = libxml_disable_entity_loader($this->disableExternalEntities);
63
64 51
        if (false !== stripos($data, '<!doctype')) {
65 3
            $internalSubset = $this->getDomDocumentTypeEntitySubset($data);
66 3
            if (!in_array($internalSubset, $this->doctypeWhitelist, true)) {
67 2
                throw new InvalidArgumentException(sprintf(
68 2
                    'The document type "%s" is not allowed. If it is safe, you may add it to the whitelist configuration.',
69
                    $internalSubset
70 2
                ));
71
            }
72 1
        }
73
74 49
        $doc = simplexml_load_string($data);
75
76 49
        libxml_use_internal_errors($previous);
77 49
        libxml_disable_entity_loader($previousEntityLoaderState);
78
79 49
        if (false === $doc) {
80
            throw new XmlErrorException(libxml_get_last_error());
81
        }
82
83 49
        return $doc;
84
    }
85
86 1
    public function visitNull($data, array $type, Context $context)
87
    {
88 1
        return null;
89
    }
90
91 17
    public function visitString($data, array $type, Context $context)
92
    {
93 17
        $data = (string) $data;
94
95 17
        if (null === $this->result) {
96 3
            $this->result = $data;
97 3
        }
98
99 17
        return $data;
100
    }
101
102 8
    public function visitBoolean($data, array $type, Context $context)
103
    {
104 8
        $data = (string) $data;
105
106 8
        if ('true' === $data || '1' === $data) {
107 4
            $data = true;
108 8
        } elseif ('false' === $data || '0' === $data) {
109 5
            $data = false;
110 5
        } else {
111
            throw new RuntimeException(sprintf('Could not convert data to boolean. Expected "true", "false", "1" or "0", but got %s.', json_encode($data)));
112
        }
113
114 8
        if (null === $this->result) {
115 6
            $this->result = $data;
116 6
        }
117
118 8
        return $data;
119
    }
120
121 8
    public function visitInteger($data, array $type, Context $context)
122
    {
123 8
        $data = (integer) $data;
124
125 8
        if (null === $this->result) {
126 2
            $this->result = $data;
127 2
        }
128
129 8
        return $data;
130
    }
131
132 12
    public function visitDouble($data, array $type, Context $context)
133
    {
134 12
        $data = (double) $data;
135
136 12
        if (null === $this->result) {
137 6
            $this->result = $data;
138 6
        }
139
140 12
        return $data;
141
    }
142
143 12
    public function visitArray($data, array $type, Context $context)
144
    {
145 12
        $entryName = null !== $this->currentMetadata && $this->currentMetadata->xmlEntryName ? $this->currentMetadata->xmlEntryName : 'entry';
146 12
        $namespace = null !== $this->currentMetadata && $this->currentMetadata->xmlEntryNamespace ? $this->currentMetadata->xmlEntryNamespace : null;
147
148 12
        if ($namespace === null && $this->objectMetadataStack->count()) {
149 7
            $classMetadata = $this->objectMetadataStack->top();
150 7
            $namespace = isset($classMetadata->xmlNamespaces[''])?$classMetadata->xmlNamespaces['']:$namespace;
151 7
        }
152
153 12
        if ( ! isset($data->$entryName) ) {
154
            if (null === $this->result) {
155
                return $this->result = array();
156
            }
157
158
            return array();
159
        }
160
161 12
        switch (count($type['params'])) {
162 12
            case 0:
163
                throw new RuntimeException(sprintf('The array type must be specified either as "array<T>", or "array<K,V>".'));
164
165 12
            case 1:
166 12
                $result = array();
167
168 12
                if (null === $this->result) {
169 5
                    $this->result = &$result;
170 5
                }
171
172 12
                $nodes = $data->children($namespace)->$entryName;
173 12
                foreach ($nodes as $v) {
174 12
                    $result[] = $this->navigator->accept($v, $type['params'][0], $context);
175 12
                }
176
177 12
                return $result;
178
179 3
            case 2:
180 3
                if (null === $this->currentMetadata) {
181
                    throw new RuntimeException('Maps are not supported on top-level without metadata.');
182
                }
183
184 3
                list($keyType, $entryType) = $type['params'];
185 3
                $result = array();
186 3
                if (null === $this->result) {
187
                    $this->result = &$result;
188
                }
189
190 3
                $nodes = $data->children($namespace)->$entryName;
191 3
                foreach ($nodes as $v) {
192 3
                    $attrs = $v->attributes();
193 3
                    if ( ! isset($attrs[$this->currentMetadata->xmlKeyAttribute])) {
194
                        throw new RuntimeException(sprintf('The key attribute "%s" must be set for each entry of the map.', $this->currentMetadata->xmlKeyAttribute));
195
                    }
196
197 3
                    $k = $this->navigator->accept($attrs[$this->currentMetadata->xmlKeyAttribute], $keyType, $context);
198 3
                    $result[$k] = $this->navigator->accept($v, $entryType, $context);
199 3
                }
200
201 3
                return $result;
202
203
            default:
204
                throw new LogicException(sprintf('The array type does not support more than 2 parameters, but got %s.', json_encode($type['params'])));
205
        }
206
    }
207
208 22
    public function startVisitingObject(ClassMetadata $metadata, $object, array $type, Context $context)
209
    {
210 22
        $this->setCurrentObject($object);
211 22
        $this->objectMetadataStack->push($metadata);
212 22
        if (null === $this->result) {
213 21
            $this->result = $this->currentObject;
214 21
        }
215 22
    }
216
217 21
    public function visitProperty(PropertyMetadata $metadata, $data, Context $context)
218
    {
219 21
        $name = $this->namingStrategy->translateName($metadata);
220
221 21
        if ( ! $metadata->type) {
222
            throw new RuntimeException(sprintf('You must define a type for %s::$%s.', $metadata->reflection->class, $metadata->name));
223
        }
224
225 21
       if ($metadata->xmlAttribute) {
226
227 4
            $attributes = $data->attributes($metadata->xmlNamespace);
228 4
            if (isset($attributes[$name])) {
229 4
                $v = $this->navigator->accept($attributes[$name], $metadata->type, $context);
230 4
                $metadata->reflection->setValue($this->currentObject, $v);
231 4
            }
232
233 4
            return;
234
        }
235
236 21
        if ($metadata->xmlValue) {
237 5
            $v = $this->navigator->accept($data, $metadata->type, $context);
238 5
            $metadata->reflection->setValue($this->currentObject, $v);
239
240 5
            return;
241
        }
242
243 19
        if ($metadata->xmlCollection) {
244 3
            $enclosingElem = $data;
245 3
            if (!$metadata->xmlCollectionInline) {
246 3
                $enclosingElem = $data->children($metadata->xmlNamespace)->$name;
247 3
            }
248
249 3
            $this->setCurrentMetadata($metadata);
250 3
            $v = $this->navigator->accept($enclosingElem, $metadata->type, $context);
251 3
            $this->revertCurrentMetadata();
252 3
            $metadata->reflection->setValue($this->currentObject, $v);
253
254 3
            return;
255
        }
256
257 19
        if ($metadata->xmlNamespace) {
258 3
            $node = $data->children($metadata->xmlNamespace)->$name;
259 3
            if (!$node->count()) {
260
                return;
261
            }
262 3
        } else {
263
264 17
            $namespaces = $data->getDocNamespaces();
265
266 17
            if (isset($namespaces[''])) {
267 1
                $prefix = uniqid('ns-');
268 1
                $data->registerXPathNamespace($prefix, $namespaces['']);
269 1
                $nodes = $data->xpath('./'.$prefix. ':'.$name );
270 1
            } else {
271 16
                $nodes = $data->xpath('./'. $name );
272
            }
273 17
            if (empty($nodes)) {
274 1
                return;
275
            }
276 17
            $node = reset($nodes);
277
        }
278
279 19
        $v = $this->navigator->accept($node, $metadata->type, $context);
280
281 19
        $metadata->setValue($this->currentObject, $v);
282 19
    }
283
284 22
    public function endVisitingObject(ClassMetadata $metadata, $data, array $type, Context $context)
285
    {
286 22
        $rs = $this->currentObject;
287 22
        $this->objectMetadataStack->pop();
288 22
        $this->revertCurrentObject();
289
290 22
        return $rs;
291
    }
292
293 22
    public function setCurrentObject($object)
294
    {
295 22
        $this->objectStack->push($this->currentObject);
296 22
        $this->currentObject = $object;
297 22
    }
298
299
    public function getCurrentObject()
300
    {
301
        return $this->currentObject;
302
    }
303
304 22
    public function revertCurrentObject()
305
    {
306 22
        return $this->currentObject = $this->objectStack->pop();
307
    }
308
309 3
    public function setCurrentMetadata(PropertyMetadata $metadata)
310
    {
311 3
        $this->metadataStack->push($this->currentMetadata);
312 3
        $this->currentMetadata = $metadata;
313 3
    }
314
315
    public function getCurrentMetadata()
316
    {
317
        return $this->currentMetadata;
318
    }
319
320 3
    public function revertCurrentMetadata()
321
    {
322 3
        return $this->currentMetadata = $this->metadataStack->pop();
323
    }
324
325 48
    public function getResult()
326
    {
327 48
        return $this->result;
328
    }
329
330
    /**
331
     * @param array<string> $doctypeWhitelist
332
     */
333 1
    public function setDoctypeWhitelist(array $doctypeWhitelist)
334
    {
335 1
        $this->doctypeWhitelist = $doctypeWhitelist;
336 1
    }
337
338
    /**
339
     * @return array<string>
340
     */
341
    public function getDoctypeWhitelist()
342
    {
343
        return $this->doctypeWhitelist;
344
    }
345
346
    /**
347
     * Retrieves internalSubset even in bugfixed php versions
348
     *
349
     * @param \DOMDocumentType $child
0 ignored issues
show
Bug introduced by
There is no parameter named $child. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
350
     * @param string $data
351
     * @return string
352
     */
353 3
    private function getDomDocumentTypeEntitySubset($data)
354
    {
355 3
        $startPos = $endPos = stripos($data, '<!doctype');
356 3
        $braces = 0;
357
        do {
358 3
            $char = $data[$endPos++];
359 3
            if ($char === '<') {
360 3
                ++$braces;
361 3
            }
362 3
            if ($char === '>') {
363 3
                --$braces;
364 3
            }
365 3
        } while ($braces > 0);
366
367 3
        $internalSubset = substr($data, $startPos, $endPos - $startPos);
368 3
        $internalSubset = str_replace(array("\n", "\r"), '', $internalSubset);
369 3
        $internalSubset = preg_replace('/\s{2,}/', ' ', $internalSubset);
370 3
        $internalSubset = str_replace(array("[ <!", "> ]>"), array('[<!', '>]>'), $internalSubset);
371
372 3
        return $internalSubset;
373
    }
374
}
375