Passed
Push — master ( 622cfa...f34591 )
by Asmir
31:36 queued 28:57
created

XmlDeserializationVisitor   F

Complexity

Total Complexity 81

Size/Duplication

Total Lines 375
Duplicated Lines 0 %

Test Coverage

Coverage 93.62%

Importance

Changes 0
Metric Value
wmc 81
dl 0
loc 375
ccs 176
cts 188
cp 0.9362
rs 1.5789
c 0
b 0
f 0

22 Methods

Rating   Name   Duplication   Size   Complexity  
A startVisitingObject() 0 4 1
C visitProperty() 0 61 12
A getDomDocumentTypeEntitySubset() 0 20 4
A getCurrentObject() 0 3 1
A getResult() 0 3 1
A revertCurrentObject() 0 3 1
A getCurrentMetadata() 0 3 1
A revertCurrentMetadata() 0 3 1
A setCurrentObject() 0 4 1
B isNull() 0 20 6
A endVisitingObject() 0 7 1
A setCurrentMetadata() 0 4 1
A __construct() 0 8 1
B prepare() 0 29 4
A visitNull() 0 2 1
A visitInteger() 0 3 1
B visitBoolean() 0 10 5
A visitString() 0 3 1
A emptyStringToSpaceCharacter() 0 3 2
F visitArray() 0 82 23
A visitDouble() 0 3 1
C visitDiscriminatorMapProperty() 0 23 11

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.

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