Completed
Pull Request — master (#859)
by
unknown
05:02
created

XmlDeserializationVisitor::visitProperty()   C

Complexity

Conditions 12
Paths 24

Size

Total Lines 69
Code Lines 42

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 43
CRAP Score 12.0399

Importance

Changes 0
Metric Value
dl 0
loc 69
ccs 43
cts 46
cp 0.9348
rs 5.7089
c 0
b 0
f 0
cc 12
eloc 42
nc 24
nop 3
crap 12.0399

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