Completed
Pull Request — master (#840)
by
unknown
03:40
created

XmlDeserializationVisitor::visitArray()   F

Complexity

Conditions 24
Paths 1443

Size

Total Lines 88
Code Lines 52

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 49
CRAP Score 26.8049

Importance

Changes 0
Metric Value
dl 0
loc 88
ccs 49
cts 59
cp 0.8305
rs 2.025
c 0
b 0
f 0
cc 24
eloc 52
nc 1443
nop 3
crap 26.8049

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\InvalidArgumentException;
22
use JMS\Serializer\Exception\LogicException;
23
use JMS\Serializer\Exception\RuntimeException;
24
use JMS\Serializer\Exception\XmlErrorException;
25
use JMS\Serializer\Metadata\ClassMetadata;
26
use JMS\Serializer\Metadata\PropertyMetadata;
27
28
class XmlDeserializationVisitor extends AbstractVisitor implements NullAwareVisitorInterface
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 60
    public function setNavigator(GraphNavigator $navigator)
46
    {
47 60
        $this->navigator = $navigator;
48 60
        $this->objectStack = new \SplStack;
49 60
        $this->metadataStack = new \SplStack;
50 60
        $this->objectMetadataStack = new \SplStack;
51 60
        $this->result = null;
52 60
    }
53
54 1
    public function getNavigator()
55
    {
56 1
        return $this->navigator;
57
    }
58
59 63
    public function prepare($data)
60
    {
61 63
        $data = $this->emptyStringToSpaceCharacter($data);
62
63 63
        $previous = libxml_use_internal_errors(true);
64 63
        libxml_clear_errors();
65
66 63
        $previousEntityLoaderState = libxml_disable_entity_loader($this->disableExternalEntities);
67
68 63
        if (false !== stripos($data, '<!doctype')) {
69 3
            $internalSubset = $this->getDomDocumentTypeEntitySubset($data);
70 3
            if (!in_array($internalSubset, $this->doctypeWhitelist, true)) {
71 2
                throw new InvalidArgumentException(sprintf(
72 2
                    'The document type "%s" is not allowed. If it is safe, you may add it to the whitelist configuration.',
73
                    $internalSubset
74 2
                ));
75
            }
76 1
        }
77
78 61
        $doc = simplexml_load_string($data);
79
80 61
        libxml_use_internal_errors($previous);
81 61
        libxml_disable_entity_loader($previousEntityLoaderState);
82
83 61
        if (false === $doc) {
84 1
            throw new XmlErrorException(libxml_get_last_error());
85
        }
86
87 60
        return $doc;
88
    }
89
90 63
    private function emptyStringToSpaceCharacter($data)
91
    {
92 63
        return $data === '' ? ' ' : (string)$data;
93
    }
94
95 7
    public function visitNull($data, array $type, Context $context)
96
    {
97 7
        return null;
98
    }
99
100 22
    public function visitString($data, array $type, Context $context)
101
    {
102 22
        $data = (string)$data;
103
104 22
        if (null === $this->result) {
105 2
            $this->result = $data;
106 2
        }
107
108 22
        return $data;
109
    }
110
111 8
    public function visitBoolean($data, array $type, Context $context)
112
    {
113 8
        $data = (string)$data;
114
115 8
        if ('true' === $data || '1' === $data) {
116 4
            $data = true;
117 8
        } elseif ('false' === $data || '0' === $data) {
118 5
            $data = false;
119 5
        } else {
120
            throw new RuntimeException(sprintf('Could not convert data to boolean. Expected "true", "false", "1" or "0", but got %s.', json_encode($data)));
121
        }
122
123 8
        if (null === $this->result) {
124 6
            $this->result = $data;
125 6
        }
126
127 8
        return $data;
128
    }
129
130 7
    public function visitInteger($data, array $type, Context $context)
131
    {
132 7
        $data = (integer)$data;
133
134 7
        if (null === $this->result) {
135 1
            $this->result = $data;
136 1
        }
137
138 7
        return $data;
139
    }
140
141 10
    public function visitDouble($data, array $type, Context $context)
142
    {
143 10
        $data = (double)$data;
144
145 10
        if (null === $this->result) {
146 4
            $this->result = $data;
147 4
        }
148
149 10
        return $data;
150
    }
151
152 16
    public function visitArray($data, array $type, Context $context)
153
    {
154
        // handle key-value-pairs
155 16
        if (null !== $this->currentMetadata && $this->currentMetadata->xmlKeyValuePairs) {
156 1
            if (2 !== count($type['params'])) {
157
                throw new RuntimeException('The array type must be specified as "array<K,V>" for Key-Value-Pairs.');
158
            }
159
160 1
            list($keyType, $entryType) = $type['params'];
161
162 1
            $result = [];
163 1
            foreach ($data as $key => $v) {
164 1
                $k = $this->navigator->accept($key, $keyType, $context);
165 1
                $result[$k] = $this->navigator->accept($v, $entryType, $context);
166 1
            }
167
168 1
            return $result;
169
        }
170
171 16
        $entryName = null !== $this->currentMetadata && $this->currentMetadata->xmlEntryName ? $this->currentMetadata->xmlEntryName : 'entry';
172 16
        $namespace = null !== $this->currentMetadata && $this->currentMetadata->xmlEntryNamespace ? $this->currentMetadata->xmlEntryNamespace : null;
173
174 16
        if ($namespace === null && $this->objectMetadataStack->count()) {
175 11
            $classMetadata = $this->objectMetadataStack->top();
176 11
            $namespace = isset($classMetadata->xmlNamespaces['']) ? $classMetadata->xmlNamespaces[''] : $namespace;
177 11
        }
178
179 16
        if (null !== $namespace) {
180 4
            $prefix = uniqid('ns-');
181 4
            $data->registerXPathNamespace($prefix, $namespace);
182 4
            $nodes = $data->xpath("$prefix:$entryName");
183 4
        } else {
184 13
            $nodes = $data->xpath($entryName);
185
        }
186
187 16
        if (!count($nodes)) {
188 3
            if (null === $this->result) {
189
                return $this->result = array();
190
            }
191
192 3
            return array();
193
        }
194
195 16
        switch (count($type['params'])) {
196 16
            case 0:
197
                throw new RuntimeException(sprintf('The array type must be specified either as "array<T>", or "array<K,V>".'));
198
199 16
            case 1:
200 16
                $result = array();
201
202 16
                if (null === $this->result) {
203 5
                    $this->result = &$result;
204 5
                }
205
206 16
                foreach ($nodes as $v) {
207 16
                    $result[] = $this->navigator->accept($v, $type['params'][0], $context);
208 16
                }
209
210 16
                return $result;
211
212 4
            case 2:
213 4
                if (null === $this->currentMetadata) {
214
                    throw new RuntimeException('Maps are not supported on top-level without metadata.');
215
                }
216
217 4
                list($keyType, $entryType) = $type['params'];
218 4
                $result = array();
219 4
                if (null === $this->result) {
220
                    $this->result = &$result;
221
                }
222
223 4
                $nodes = $data->children($namespace)->$entryName;
224 4
                foreach ($nodes as $v) {
225 4
                    $attrs = $v->attributes();
226 4
                    if (!isset($attrs[$this->currentMetadata->xmlKeyAttribute])) {
227
                        throw new RuntimeException(sprintf('The key attribute "%s" must be set for each entry of the map.', $this->currentMetadata->xmlKeyAttribute));
228
                    }
229
230 4
                    $k = $this->navigator->accept($attrs[$this->currentMetadata->xmlKeyAttribute], $keyType, $context);
231 4
                    $result[$k] = $this->navigator->accept($v, $entryType, $context);
232 4
                }
233
234 4
                return $result;
235
236
            default:
237
                throw new LogicException(sprintf('The array type does not support more than 2 parameters, but got %s.', json_encode($type['params'])));
238
        }
239
    }
240
241 31
    public function startVisitingObject(ClassMetadata $metadata, $object, array $type, Context $context)
242
    {
243 31
        $this->setCurrentObject($object);
244 31
        $this->objectMetadataStack->push($metadata);
245 31
        if (null === $this->result) {
246 30
            $this->result = $this->currentObject;
247 30
        }
248 31
    }
249
250 27
    public function visitProperty(PropertyMetadata $metadata, $data, Context $context)
251
    {
252 27
        $name = $this->namingStrategy->translateName($metadata);
253
254 27
        if (!$metadata->type) {
255
            throw new RuntimeException(sprintf('You must define a type for %s::$%s.', $metadata->reflection->class, $metadata->name));
256
        }
257
258 27
        if ($metadata->xmlAttribute) {
259
260 5
            $attributes = $data->attributes($metadata->xmlNamespace);
261 5
            if (isset($attributes[$name])) {
262 5
                $v = $this->navigator->accept($attributes[$name], $metadata->type, $context);
263 5
                $this->accessor->setValue($this->currentObject, $v, $metadata);
264 5
            }
265
266 5
            return;
267
        }
268
269 27
        if ($metadata->xmlValue) {
270 6
            $v = $this->navigator->accept($data, $metadata->type, $context);
271 6
            $this->accessor->setValue($this->currentObject, $v, $metadata);
272
273 6
            return;
274
        }
275
276 25
        if ($metadata->xmlCollection) {
277 6
            $enclosingElem = $data;
278 6
            if (!$metadata->xmlCollectionInline) {
279 6
                $enclosingElem = $data->children($metadata->xmlNamespace)->$name;
280 6
            }
281
282 6
            $this->setCurrentMetadata($metadata);
283 6
            $v = $this->navigator->accept($enclosingElem, $metadata->type, $context);
284 6
            $this->revertCurrentMetadata();
285 6
            $this->accessor->setValue($this->currentObject, $v, $metadata);
286
287 6
            return;
288
        }
289
290 24
        if ($metadata->xmlNamespace) {
291 4
            $node = $data->children($metadata->xmlNamespace)->$name;
292 4
            if (!$node->count()) {
293 1
                return;
294
            }
295 3
        } else {
296
297 21
            $namespaces = $data->getDocNamespaces();
298
299 21
            if (isset($namespaces[''])) {
300 1
                $prefix = uniqid('ns-');
301 1
                $data->registerXPathNamespace($prefix, $namespaces['']);
302 1
                $nodes = $data->xpath('./' . $prefix . ':' . $name);
303 1
            } else {
304 20
                $nodes = $data->xpath('./' . $name);
305
            }
306 21
            if (empty($nodes)) {
307 1
                return;
308
            }
309 21
            $node = reset($nodes);
310
        }
311
312 23
        $v = $this->navigator->accept($node, $metadata->type, $context);
313
314 23
        $this->accessor->setValue($this->currentObject, $v, $metadata);
315 23
    }
316
317 31
    public function endVisitingObject(ClassMetadata $metadata, $data, array $type, Context $context)
318
    {
319 31
        $rs = $this->currentObject;
320 31
        $this->objectMetadataStack->pop();
321 31
        $this->revertCurrentObject();
322
323 31
        return $rs;
324
    }
325
326 31
    public function setCurrentObject($object)
327
    {
328 31
        $this->objectStack->push($this->currentObject);
329 31
        $this->currentObject = $object;
330 31
    }
331
332
    public function getCurrentObject()
333
    {
334
        return $this->currentObject;
335
    }
336
337 31
    public function revertCurrentObject()
338
    {
339 31
        return $this->currentObject = $this->objectStack->pop();
340
    }
341
342 7
    public function setCurrentMetadata(PropertyMetadata $metadata)
343
    {
344 7
        $this->metadataStack->push($this->currentMetadata);
345 7
        $this->currentMetadata = $metadata;
346 7
    }
347
348
    public function getCurrentMetadata()
349
    {
350
        return $this->currentMetadata;
351
    }
352
353 7
    public function revertCurrentMetadata()
354
    {
355 7
        return $this->currentMetadata = $this->metadataStack->pop();
356
    }
357
358 59
    public function getResult()
359
    {
360 59
        return $this->result;
361
    }
362
363
    /**
364
     * @param array <string> $doctypeWhitelist
365
     */
366 1
    public function setDoctypeWhitelist(array $doctypeWhitelist)
367
    {
368 1
        $this->doctypeWhitelist = $doctypeWhitelist;
369 1
    }
370
371
    /**
372
     * @return array<string>
373
     */
374
    public function getDoctypeWhitelist()
375
    {
376
        return $this->doctypeWhitelist;
377
    }
378
379
    /**
380
     * Retrieves internalSubset even in bugfixed php versions
381
     *
382
     * @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...
383
     * @param string $data
384
     * @return string
385
     */
386 3
    private function getDomDocumentTypeEntitySubset($data)
387
    {
388 3
        $startPos = $endPos = stripos($data, '<!doctype');
389 3
        $braces = 0;
390
        do {
391 3
            $char = $data[$endPos++];
392 3
            if ($char === '<') {
393 3
                ++$braces;
394 3
            }
395 3
            if ($char === '>') {
396 3
                --$braces;
397 3
            }
398 3
        } while ($braces > 0);
399
400 3
        $internalSubset = substr($data, $startPos, $endPos - $startPos);
401 3
        $internalSubset = str_replace(array("\n", "\r"), '', $internalSubset);
402 3
        $internalSubset = preg_replace('/\s{2,}/', ' ', $internalSubset);
403 3
        $internalSubset = str_replace(array("[ <!", "> ]>"), array('[<!', '>]>'), $internalSubset);
404
405 3
        return $internalSubset;
406
    }
407
408
    /**
409
     * @param mixed $value
410
     *
411
     * @return bool
412
     */
413 61
    public function isNull($value)
414
    {
415 61
        if ($value instanceof \SimpleXMLElement) {
416
            // Workaround for https://bugs.php.net/bug.php?id=75168 and https://github.com/schmittjoh/serializer/issues/817
417
            // If the "name" is empty means that we are on an not-existent node and subsequent operations on the object will trigger the warning:
418
            // "Node no longer exists"
419 61
            if ($value->getName() === "") {
420
                // @todo should be "true", but for collections needs a default collection value. maybe something for the 2.0
421 2
                return false;
422
            }
423
424 61
            $xsiAttributes = $value->attributes('http://www.w3.org/2001/XMLSchema-instance');
425 61
            if (isset($xsiAttributes['nil'])
426 61
                && ((string) $xsiAttributes['nil'] === 'true' || (string) $xsiAttributes['nil'] === '1')
427 61
            ) {
428 8
                return true;
429
            }
430 54
        }
431
432 55
        return $value === null;
433
    }
434
}
435