Completed
Pull Request — master (#840)
by
unknown
37:55
created

XmlDeserializationVisitor::visitProperty()   C

Complexity

Conditions 11
Paths 12

Size

Total Lines 66
Code Lines 40

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 42
CRAP Score 11.0015

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 66
ccs 42
cts 43
cp 0.9767
rs 5.9447
cc 11
eloc 40
nc 12
nop 3
crap 11.0015

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 16
        $metadata = $this->currentMetadata;
155 16
        if (null === $metadata && $context->getMetadataStack()->count()) {
156 5
            $metadata = $context->getMetadataStack()->top();
157 5
        }
158
159
        // handle key-value-pairs
160 16
        if (null !== $metadata && $metadata->xmlKeyValuePairs) {
161 1
            if (2 !== count($type['params'])) {
162
                throw new RuntimeException(sprintf('The array type must be specified as "array<K,V>" for Key-Value-Pairs.'));
163
            }
164
165 1
            list($keyType, $entryType) = $type['params'];
166
167 1
            $result = [];
168 1
            foreach ($data as $key => $v) {
169 1
                $k = $this->navigator->accept($key, $keyType, $context);
170 1
                $result[$k] = $this->navigator->accept($v, $entryType, $context);
171 1
            }
172
173 1
            return $result;
174
        }
175
176 15
        $entryName = null !== $this->currentMetadata && $this->currentMetadata->xmlEntryName ? $this->currentMetadata->xmlEntryName : 'entry';
177 15
        $namespace = null !== $this->currentMetadata && $this->currentMetadata->xmlEntryNamespace ? $this->currentMetadata->xmlEntryNamespace : null;
178
179 15
        if ($namespace === null && $this->objectMetadataStack->count()) {
180 10
            $classMetadata = $this->objectMetadataStack->top();
181 10
            $namespace = isset($classMetadata->xmlNamespaces['']) ? $classMetadata->xmlNamespaces[''] : $namespace;
182 10
        }
183
184 15
        if (null !== $namespace) {
185 4
            $prefix = uniqid('ns-');
186 4
            $data->registerXPathNamespace($prefix, $namespace);
187 4
            $nodes = $data->xpath("$prefix:$entryName");
188 4
        } else {
189 12
            $nodes = $data->xpath($entryName);
190
        }
191
192 15
        if (!count($nodes)) {
193 2
            if (null === $this->result) {
194
                return $this->result = array();
195
            }
196
197 2
            return array();
198
        }
199
200 15
        switch (count($type['params'])) {
201 15
            case 0:
202
                throw new RuntimeException(sprintf('The array type must be specified either as "array<T>", or "array<K,V>".'));
203
204 15
            case 1:
205 15
                $result = array();
206
207 15
                if (null === $this->result) {
208 5
                    $this->result = &$result;
209 5
                }
210
211 15
                foreach ($nodes as $v) {
212 15
                    $result[] = $this->navigator->accept($v, $type['params'][0], $context);
213 15
                }
214
215 15
                return $result;
216
217 4
            case 2:
218 4
                if (null === $this->currentMetadata) {
219
                    throw new RuntimeException('Maps are not supported on top-level without metadata.');
220
                }
221
222 4
                list($keyType, $entryType) = $type['params'];
223 4
                $result = array();
224 4
                if (null === $this->result) {
225
                    $this->result = &$result;
226
                }
227
228 4
                $nodes = $data->children($namespace)->$entryName;
229 4
                foreach ($nodes as $v) {
230 4
                    $attrs = $v->attributes();
231 4
                    if (!isset($attrs[$this->currentMetadata->xmlKeyAttribute])) {
232
                        throw new RuntimeException(sprintf('The key attribute "%s" must be set for each entry of the map.', $this->currentMetadata->xmlKeyAttribute));
233
                    }
234
235 4
                    $k = $this->navigator->accept($attrs[$this->currentMetadata->xmlKeyAttribute], $keyType, $context);
236 4
                    $result[$k] = $this->navigator->accept($v, $entryType, $context);
237 4
                }
238
239 4
                return $result;
240
241
            default:
242
                throw new LogicException(sprintf('The array type does not support more than 2 parameters, but got %s.', json_encode($type['params'])));
243
        }
244
    }
245
246 31
    public function startVisitingObject(ClassMetadata $metadata, $object, array $type, Context $context)
247
    {
248 31
        $this->setCurrentObject($object);
249 31
        $this->objectMetadataStack->push($metadata);
250 31
        if (null === $this->result) {
251 30
            $this->result = $this->currentObject;
252 30
        }
253 31
    }
254
255 27
    public function visitProperty(PropertyMetadata $metadata, $data, Context $context)
256
    {
257 27
        $name = $this->namingStrategy->translateName($metadata);
258
259 27
        if (!$metadata->type) {
260
            throw new RuntimeException(sprintf('You must define a type for %s::$%s.', $metadata->reflection->class, $metadata->name));
261
        }
262
263 27
        if ($metadata->xmlAttribute) {
264
265 5
            $attributes = $data->attributes($metadata->xmlNamespace);
266 5
            if (isset($attributes[$name])) {
267 5
                $v = $this->navigator->accept($attributes[$name], $metadata->type, $context);
268 5
                $this->accessor->setValue($this->currentObject, $v, $metadata);
269 5
            }
270
271 5
            return;
272
        }
273
274 27
        if ($metadata->xmlValue) {
275 6
            $v = $this->navigator->accept($data, $metadata->type, $context);
276 6
            $this->accessor->setValue($this->currentObject, $v, $metadata);
277
278 6
            return;
279
        }
280
281 25
        if ($metadata->xmlCollection) {
282 6
            $enclosingElem = $data;
283 6
            if (!$metadata->xmlCollectionInline) {
284 6
                $enclosingElem = $data->children($metadata->xmlNamespace)->$name;
285 6
            }
286
287 6
            $this->setCurrentMetadata($metadata);
288 6
            $v = $this->navigator->accept($enclosingElem, $metadata->type, $context);
289 6
            $this->revertCurrentMetadata();
290 6
            $this->accessor->setValue($this->currentObject, $v, $metadata);
291
292 6
            return;
293
        }
294
295 24
        if ($metadata->xmlNamespace) {
296 4
            $node = $data->children($metadata->xmlNamespace)->$name;
297 4
            if (!$node->count()) {
298 1
                return;
299
            }
300 3
        } else {
301
302 21
            $namespaces = $data->getDocNamespaces();
303
304 21
            if (isset($namespaces[''])) {
305 1
                $prefix = uniqid('ns-');
306 1
                $data->registerXPathNamespace($prefix, $namespaces['']);
307 1
                $nodes = $data->xpath('./' . $prefix . ':' . $name);
308 1
            } else {
309 20
                $nodes = $data->xpath('./' . $name);
310
            }
311 21
            if (empty($nodes)) {
312 1
                return;
313
            }
314 21
            $node = reset($nodes);
315
        }
316
317 23
        $v = $this->navigator->accept($node, $metadata->type, $context);
318
319 23
        $this->accessor->setValue($this->currentObject, $v, $metadata);
320 23
    }
321
322 31
    public function endVisitingObject(ClassMetadata $metadata, $data, array $type, Context $context)
323
    {
324 31
        $rs = $this->currentObject;
325 31
        $this->objectMetadataStack->pop();
326 31
        $this->revertCurrentObject();
327
328 31
        return $rs;
329
    }
330
331 31
    public function setCurrentObject($object)
332
    {
333 31
        $this->objectStack->push($this->currentObject);
334 31
        $this->currentObject = $object;
335 31
    }
336
337
    public function getCurrentObject()
338
    {
339
        return $this->currentObject;
340
    }
341
342 31
    public function revertCurrentObject()
343
    {
344 31
        return $this->currentObject = $this->objectStack->pop();
345
    }
346
347 6
    public function setCurrentMetadata(PropertyMetadata $metadata)
348
    {
349 6
        $this->metadataStack->push($this->currentMetadata);
350 6
        $this->currentMetadata = $metadata;
351 6
    }
352
353
    public function getCurrentMetadata()
354
    {
355
        return $this->currentMetadata;
356
    }
357
358 6
    public function revertCurrentMetadata()
359
    {
360 6
        return $this->currentMetadata = $this->metadataStack->pop();
361
    }
362
363 59
    public function getResult()
364
    {
365 59
        return $this->result;
366
    }
367
368
    /**
369
     * @param array <string> $doctypeWhitelist
370
     */
371 1
    public function setDoctypeWhitelist(array $doctypeWhitelist)
372
    {
373 1
        $this->doctypeWhitelist = $doctypeWhitelist;
374 1
    }
375
376
    /**
377
     * @return array<string>
378
     */
379
    public function getDoctypeWhitelist()
380
    {
381
        return $this->doctypeWhitelist;
382
    }
383
384
    /**
385
     * Retrieves internalSubset even in bugfixed php versions
386
     *
387
     * @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...
388
     * @param string $data
389
     * @return string
390
     */
391 3
    private function getDomDocumentTypeEntitySubset($data)
392
    {
393 3
        $startPos = $endPos = stripos($data, '<!doctype');
394 3
        $braces = 0;
395
        do {
396 3
            $char = $data[$endPos++];
397 3
            if ($char === '<') {
398 3
                ++$braces;
399 3
            }
400 3
            if ($char === '>') {
401 3
                --$braces;
402 3
            }
403 3
        } while ($braces > 0);
404
405 3
        $internalSubset = substr($data, $startPos, $endPos - $startPos);
406 3
        $internalSubset = str_replace(array("\n", "\r"), '', $internalSubset);
407 3
        $internalSubset = preg_replace('/\s{2,}/', ' ', $internalSubset);
408 3
        $internalSubset = str_replace(array("[ <!", "> ]>"), array('[<!', '>]>'), $internalSubset);
409
410 3
        return $internalSubset;
411
    }
412
413
    /**
414
     * @param mixed $value
415
     *
416
     * @return bool
417
     */
418 61
    public function isNull($value)
419
    {
420 61
        if ($value instanceof \SimpleXMLElement) {
421
            // Workaround for https://bugs.php.net/bug.php?id=75168 and https://github.com/schmittjoh/serializer/issues/817
422
            // If the "name" is empty means that we are on an not-existent node and subsequent operations on the object will trigger the warning:
423
            // "Node no longer exists"
424 61
            if ($value->getName() === "") {
425
                // @todo should be "true", but for collections needs a default collection value. maybe something for the 2.0
426 2
                return false;
427
            }
428
429 61
            $xsiAttributes = $value->attributes('http://www.w3.org/2001/XMLSchema-instance');
430 61
            if (isset($xsiAttributes['nil'])
431 61
                && ((string) $xsiAttributes['nil'] === 'true' || (string) $xsiAttributes['nil'] === '1')
432 61
            ) {
433 8
                return true;
434
            }
435 54
        }
436
437 55
        return $value === null;
438
    }
439
}
440