Passed
Pull Request — master (#1549)
by
unknown
02:36
created

XmlDeserializationVisitor::visitProperty()   D

Complexity

Conditions 19
Paths 31

Size

Total Lines 90
Code Lines 51

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 27
CRAP Score 19.1185

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 19
eloc 51
c 2
b 0
f 0
nc 31
nop 2
dl 0
loc 90
rs 4.5166
ccs 27
cts 29
cp 0.931
crap 19.1185

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
declare(strict_types=1);
4
5
namespace JMS\Serializer;
6
7
use JMS\Serializer\Exception\InvalidArgumentException;
8
use JMS\Serializer\Exception\LogicException;
9
use JMS\Serializer\Exception\NotAcceptableException;
10
use JMS\Serializer\Exception\RuntimeException;
11
use JMS\Serializer\Exception\XmlErrorException;
12
use JMS\Serializer\Metadata\ClassMetadata;
13
use JMS\Serializer\Metadata\PropertyMetadata;
14
use JMS\Serializer\Visitor\DeserializationVisitorInterface;
15
16
final class XmlDeserializationVisitor extends AbstractVisitor implements NullAwareVisitorInterface, DeserializationVisitorInterface
17
{
18
    /**
19
     * @var \SplStack
20
     */
21
    private $objectStack;
22
23
    /**
24
     * @var \SplStack
25
     */
26 72
    private $metadataStack;
27
28
    /**
29 72
     * @var \SplStack
30 72
     */
31 72
    private $objectMetadataStack;
32 72
33 72
    /**
34 72
     * @var object|null
35
     */
36 71
    private $currentObject;
37
38 71
    /**
39
     * @var ClassMetadata|PropertyMetadata|null
40 71
     */
41 71
    private $currentMetadata;
42
43 71
    /**
44
     * @var bool
45 71
     */
46 2
    private $disableExternalEntities;
47 2
48 2
    /**
49 2
     * @var string[]
50 2
     */
51
    private $doctypeAllowList;
52
    /**
53
     * @var int
54
     */
55 69
    private $options;
56
57 69
    /**
58 69
     * THIS IS ONLY USED FOR UNION DESERIALIZATION WHICH IS NOT SUPPORTED IN XML
59
     *
60 69
     * @var bool
61 1
     */
62
    private $requireAllRequiredProperties = false;
63
64 68
    public function __construct(
65
        bool $disableExternalEntities = true,
66
        array $doctypeAllowList = [],
67 71
        int $options = 0
68
    ) {
69 71
        $this->objectStack = new \SplStack();
70
        $this->metadataStack = new \SplStack();
71
        $this->objectMetadataStack = new \SplStack();
72 13
        $this->disableExternalEntities = $disableExternalEntities;
73
        $this->doctypeAllowList = $doctypeAllowList;
74
        $this->options = $options;
75 13
    }
76
77 25
    public function setRequireAllRequiredProperties(bool $requireAllRequiredProperties): void
78
    {
79 25
        $this->requireAllRequiredProperties = $requireAllRequiredProperties;
80
    }
81
82 8
    public function getRequireAllRequiredProperties(): bool
83
    {
84 8
        return $this->requireAllRequiredProperties;
85
    }
86 8
87 4
    /**
88 5
     * {@inheritdoc}
89 5
     */
90
    public function prepare($data)
91
    {
92
        $data = $this->emptyStringToSpaceCharacter($data);
93
94
        $previous = libxml_use_internal_errors(true);
95 8
        libxml_clear_errors();
96
97 8
        $previousEntityLoaderState = null;
98
        if (\LIBXML_VERSION < 20900) {
99
            // phpcs:ignore Generic.PHP.DeprecatedFunctions.Deprecated
100 10
            $previousEntityLoaderState = libxml_disable_entity_loader($this->disableExternalEntities);
101
        }
102 10
103
        if (false !== stripos($data, '<!doctype')) {
104
            $internalSubset = $this->getDomDocumentTypeEntitySubset($data);
105 18
            if (!in_array($internalSubset, $this->doctypeAllowList, true)) {
106
                throw new InvalidArgumentException(sprintf(
107
                    'The document type "%s" is not allowed. If it is safe, you may add it to the allowlist configuration.',
108 18
                    $internalSubset,
109 2
                ));
110
            }
111
        }
112 2
113
        $doc = simplexml_load_string($data, 'SimpleXMLElement', $this->options);
114 2
115
        libxml_use_internal_errors($previous);
116 2
117 2
        if (\LIBXML_VERSION < 20900) {
118 2
            // phpcs:ignore Generic.PHP.DeprecatedFunctions.Deprecated
119 2
            libxml_disable_entity_loader($previousEntityLoaderState);
0 ignored issues
show
Bug introduced by
It seems like $previousEntityLoaderState can also be of type null; however, parameter $disable of libxml_disable_entity_loader() does only seem to accept boolean, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

119
            libxml_disable_entity_loader(/** @scrutinizer ignore-type */ $previousEntityLoaderState);
Loading history...
120
        }
121
122 2
        if (false === $doc) {
123
            throw new XmlErrorException(libxml_get_last_error());
124
        }
125 18
126 18
        return $doc;
127
    }
128 18
129 13
    /**
130 13
     * @param mixed $data
131 13
     */
132 10
    private function emptyStringToSpaceCharacter($data): string
133 10
    {
134 1
        return '' === $data ? ' ' : (string) $data;
135
    }
136
137
    /**
138
     * {@inheritdoc}
139 18
     */
140 5
    public function visitNull($data, array $type)
141 5
    {
142 5
        return null;
143
    }
144 14
145
    /**
146
     * {@inheritdoc}
147 18
     */
148 4
    public function visitString($data, array $type): string
149
    {
150
        $this->assertValueCanBeCastToString($data);
151 18
152 18
        return (string) $data;
153
    }
154
155 18
    /**
156 18
     * {@inheritdoc}
157
     */
158 18
    public function visitBoolean($data, array $type): bool
159 18
    {
160
        $this->assertValueCanBeCastToString($data);
161
162 18
        $data = (string) $data;
163
164 4
        if ('true' === $data || '1' === $data) {
165 4
            return true;
166
        } elseif ('false' === $data || '0' === $data) {
167
            return false;
168
        } else {
169 4
            throw new RuntimeException(sprintf('Could not convert data to boolean. Expected "true", "false", "1" or "0", but got %s.', json_encode($data)));
170 4
        }
171
    }
172 4
173 4
    /**
174 4
     * {@inheritdoc}
175 4
     */
176
    public function visitInteger($data, array $type): int
177
    {
178
        $this->assertValueCanBeCastToInt($data);
179 4
180 4
        return (int) $data;
181
    }
182
183 4
    /**
184
     * {@inheritdoc}
185
     */
186
    public function visitDouble($data, array $type): float
187
    {
188
        $this->assertValueCanCastToFloat($data);
189
190 8
        return (float) $data;
191
    }
192
193
    /**
194 8
     * {@inheritdoc}
195 1
     */
196
    public function visitArray($data, array $type): array
197
    {
198 7
        // handle key-value-pairs
199 1
        if (null !== $this->currentMetadata && $this->currentMetadata->xmlKeyValuePairs) {
0 ignored issues
show
Bug introduced by
The property xmlKeyValuePairs does not seem to exist on JMS\Serializer\Metadata\ClassMetadata.
Loading history...
200
            if (2 !== count($type['params'])) {
201
                throw new RuntimeException('The array type must be specified as "array<K,V>" for Key-Value-Pairs.');
202 6
            }
203 1
204
            $this->revertCurrentMetadata();
205 5
206 4
            [$keyType, $entryType] = $type['params'];
207
208
            $result = [];
209 1
            foreach ($data as $key => $v) {
210 1
                $k = $this->navigator->accept($key, $keyType);
211 1
                $result[$k] = $this->navigator->accept($v, $entryType);
212 1
            }
213
214
            return $result;
215
        }
216
217 34
        $entryName = null !== $this->currentMetadata && $this->currentMetadata->xmlEntryName ? $this->currentMetadata->xmlEntryName : 'entry';
0 ignored issues
show
Bug introduced by
The property xmlEntryName does not seem to exist on JMS\Serializer\Metadata\ClassMetadata.
Loading history...
218
        $namespace = null !== $this->currentMetadata && $this->currentMetadata->xmlEntryNamespace ? $this->currentMetadata->xmlEntryNamespace : null;
0 ignored issues
show
Bug introduced by
The property xmlEntryNamespace does not exist on JMS\Serializer\Metadata\ClassMetadata. Did you mean xmlNamespace?
Loading history...
219 34
220 34
        if (null === $namespace && $this->objectMetadataStack->count()) {
221 34
            $classMetadata = $this->objectMetadataStack->top();
222
            $namespace = $classMetadata->xmlNamespaces[''] ?? $namespace;
223 30
            if (null === $namespace) {
224
                $namespaces = $data->getDocNamespaces();
225 30
                if (isset($namespaces[''])) {
226
                    $namespace = $namespaces[''];
227 30
                }
228
            }
229
        }
230
231 30
        if (null !== $namespace) {
232
            $prefix = uniqid('ns-');
233 6
            $data->registerXPathNamespace($prefix, $namespace);
234 6
            $nodes = $data->xpath(sprintf('%s:%s', $prefix, $entryName));
235 6
        } else {
236
            $nodes = $data->xpath($entryName);
237
        }
238
239
        if (null === $nodes || !\count($nodes)) {
240
            return [];
241 30
        }
242 7
243
        switch (\count($type['params'])) {
244
            case 0:
245 28
                throw new RuntimeException(sprintf('The array type must be specified either as "array<T>", or "array<K,V>".'));
246 7
247 7
            case 1:
248 6
                $result = [];
249
250
                foreach ($nodes as $v) {
251 7
                    $result[] = $this->navigator->accept($v, $type['params'][0]);
252 7
                }
253 7
254 7
                return $result;
255
256
            case 2:
257 27
                if (null === $this->currentMetadata) {
258 4
                    throw new RuntimeException('Maps are not supported on top-level without metadata.');
259 4
                }
260 4
261
                [$keyType, $entryType] = $type['params'];
262
                $result = [];
263
264 24
                $nodes = $data->children($namespace)->$entryName;
265
                foreach ($nodes as $v) {
266 24
                    $attrs = $v->attributes();
267 2
                    if (!isset($attrs[$this->currentMetadata->xmlKeyAttribute])) {
0 ignored issues
show
Bug introduced by
The property xmlKeyAttribute does not exist on JMS\Serializer\Metadata\ClassMetadata. Did you mean xmlAttribute?
Loading history...
268 2
                        throw new RuntimeException(sprintf('The key attribute "%s" must be set for each entry of the map.', $this->currentMetadata->xmlKeyAttribute));
269 2
                    }
270
271 22
                    $k = $this->navigator->accept($attrs[$this->currentMetadata->xmlKeyAttribute], $keyType);
272
                    $result[$k] = $this->navigator->accept($v, $entryType);
273 24
                }
274 2
275
                return $result;
276 24
277
            default:
278
                throw new LogicException(sprintf('The array type does not support more than 2 parameters, but got %s.', json_encode($type['params'])));
279 26
        }
280 2
    }
281
282
    /**
283 26
     * {@inheritdoc}
284
     */
285
    public function visitDiscriminatorMapProperty($data, ClassMetadata $metadata): string
286
    {
287
        switch (true) {
288
            // Check XML attribute without namespace for discriminatorFieldName
289
            case $metadata->xmlDiscriminatorAttribute && null === $metadata->xmlDiscriminatorNamespace && isset($data->attributes()->{$metadata->discriminatorFieldName}):
290
                return (string) $data->attributes()->{$metadata->discriminatorFieldName};
291
292 34
            // Check XML attribute with namespace for discriminatorFieldName
293
            case $metadata->xmlDiscriminatorAttribute && null !== $metadata->xmlDiscriminatorNamespace && isset($data->attributes($metadata->xmlDiscriminatorNamespace)->{$metadata->discriminatorFieldName}):
294 34
                return (string) $data->attributes($metadata->xmlDiscriminatorNamespace)->{$metadata->discriminatorFieldName};
295 34
296 34
            // Check XML element with namespace for discriminatorFieldName
297
            case !$metadata->xmlDiscriminatorAttribute && null !== $metadata->xmlDiscriminatorNamespace && isset($data->children($metadata->xmlDiscriminatorNamespace)->{$metadata->discriminatorFieldName}):
298 34
                return (string) $data->children($metadata->xmlDiscriminatorNamespace)->{$metadata->discriminatorFieldName};
299
300
            // Check XML element for discriminatorFieldName
301 34
            case isset($data->{$metadata->discriminatorFieldName}):
302
                return (string) $data->{$metadata->discriminatorFieldName};
303 34
304 34
            default:
305 34
                throw new LogicException(sprintf(
306
                    'The discriminator field name "%s" for base-class "%s" was not found in input data.',
307
                    $metadata->discriminatorFieldName,
308
                    $metadata->name,
309
                ));
310
        }
311
    }
312 34
313
    public function startVisitingObject(ClassMetadata $metadata, object $object, array $type): void
314 34
    {
315
        $this->setCurrentObject($object);
316
        $this->objectMetadataStack->push($metadata);
317 9
    }
318
319 9
    /**
320 9
     * {@inheritdoc}
321 9
     */
322
    public function visitProperty(PropertyMetadata $metadata, $data)
323
    {
324
        $name = $metadata->serializedName;
325
326
        if (true === $metadata->inline) {
327
            if (!$metadata->type) {
328 9
                throw RuntimeException::noMetadataForProperty($metadata->class, $metadata->name);
329
            }
330 9
331
            return $this->navigator->accept($data, $metadata->type);
332
        }
333 67
334
        if ($metadata->xmlAttribute) {
335 67
            $attributes = $data->attributes($metadata->xmlNamespace);
336
            if (isset($attributes[$name])) {
337
                if (!$metadata->type) {
338
                    throw RuntimeException::noMetadataForProperty($metadata->class, $metadata->name);
339
                }
340
341
                return $this->navigator->accept($attributes[$name], $metadata->type);
342
            }
343
344 2
            throw new NotAcceptableException();
345
        }
346 2
347 2
        if ($metadata->xmlValue) {
348
            if (!$metadata->type) {
349 2
                throw RuntimeException::noMetadataForProperty($metadata->class, $metadata->name);
350 2
            }
351 2
352
            return $this->navigator->accept($data, $metadata->type);
353 2
        }
354 2
355
        if ($metadata->xmlCollection) {
356 2
            $enclosingElem = $data;
357
            if (!$metadata->xmlCollectionInline) {
358 2
                $enclosingElem = $data->children($metadata->xmlNamespace)->$name;
359 2
            }
360 2
361 2
            $this->setCurrentMetadata($metadata);
362
            if (!$metadata->type) {
363 2
                throw RuntimeException::noMetadataForProperty($metadata->class, $metadata->name);
364
            }
365
366
            $v = $this->navigator->accept($enclosingElem, $metadata->type);
367
            $this->revertCurrentMetadata();
368
369
            return $v;
370
        }
371 69
372
        if ($metadata->xmlNamespace) {
373 69
            $node = $data->children($metadata->xmlNamespace)->$name;
374
            if (!$node->count()) {
375
                throw new NotAcceptableException();
376
            }
377 69
        } elseif ('' === $metadata->xmlNamespace) {
378
            // See #1087 - element must be like: <element xmlns="" /> - https://www.w3.org/TR/REC-xml-names/#iri-use
379 2
            // Use of an empty string in a namespace declaration turns it into an "undeclaration".
380
            $nodes = $data->xpath('./' . $name);
381
            if (empty($nodes)) {
382 69
                throw new NotAcceptableException();
383 69
            }
384 69
385
            $node = reset($nodes);
386 14
        } else {
387
            $namespaces = $data->getDocNamespaces();
388
            if (isset($namespaces[''])) {
389
                $prefix = uniqid('ns-');
390 57
                $data->registerXPathNamespace($prefix, $namespaces['']);
391
                $nodes = $data->xpath('./' . $prefix . ':' . $name);
392
            } else {
393
                $nodes = $data->xpath('./' . $name);
394
            }
395
396
            if (empty($nodes)) {
397
                throw new NotAcceptableException();
398
            }
399
400
            $node = reset($nodes);
401
        }
402
403
        if ($metadata->xmlKeyValuePairs) {
404
            $this->setCurrentMetadata($metadata);
405
        }
406
407
        if (!$metadata->type) {
408
            throw RuntimeException::noMetadataForProperty($metadata->class, $metadata->name);
409
        }
410
411
        return $this->navigator->accept($node, $metadata->type);
412
    }
413
414
    /**
415
     * {@inheritdoc}
416
     */
417
    public function endVisitingObject(ClassMetadata $metadata, $data, array $type): object
418
    {
419
        $rs = $this->currentObject;
420
        $this->objectMetadataStack->pop();
421
        $this->revertCurrentObject();
422
423
        return $rs;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $rs could return the type null which is incompatible with the type-hinted return object. Consider adding an additional type-check to rule them out.
Loading history...
424
    }
425
426
    public function setCurrentObject(object $object): void
427
    {
428
        $this->objectStack->push($this->currentObject);
429
        $this->currentObject = $object;
430
    }
431
432
    public function getCurrentObject(): ?object
433
    {
434
        return $this->currentObject;
435
    }
436
437
    public function revertCurrentObject(): ?object
438
    {
439
        return $this->currentObject = $this->objectStack->pop();
440
    }
441
442
    public function setCurrentMetadata(PropertyMetadata $metadata): void
443
    {
444
        $this->metadataStack->push($this->currentMetadata);
445
        $this->currentMetadata = $metadata;
446
    }
447
448
    /**
449
     * @return ClassMetadata|PropertyMetadata|null
450
     */
451
    public function getCurrentMetadata()
452
    {
453
        return $this->currentMetadata;
454
    }
455
456
    /**
457
     * @return ClassMetadata|PropertyMetadata|null
458
     */
459
    public function revertCurrentMetadata()
460
    {
461
        return $this->currentMetadata = $this->metadataStack->pop();
462
    }
463
464
    /**
465
     * {@inheritdoc}
466
     */
467
    public function getResult($data)
468
    {
469
        unset($this->navigator);
470
471
        return $data;
472
    }
473
474
    /**
475
     * Retrieves internalSubset even in bugfixed php versions
476
     */
477
    private function getDomDocumentTypeEntitySubset(string $data): string
478
    {
479
        $startPos = $endPos = stripos($data, '<!doctype');
480
        $braces = 0;
481
        do {
482
            $char = $data[$endPos++];
483
            if ('<' === $char) {
484
                ++$braces;
485
            }
486
487
            if ('>' === $char) {
488
                --$braces;
489
            }
490
        } while ($braces > 0);
491
492
        $internalSubset = substr($data, $startPos, $endPos - $startPos);
493
        $internalSubset = str_replace(["\n", "\r"], '', $internalSubset);
494
        $internalSubset = preg_replace('/\s{2,}/', ' ', $internalSubset);
495
        $internalSubset = str_replace(['[ <!', '> ]>'], ['[<!', '>]>'], $internalSubset);
496
497
        return $internalSubset;
498
    }
499
500
    /**
501
     * {@inheritdoc}
502
     */
503
    public function isNull($value): bool
504
    {
505
        if ($value instanceof \SimpleXMLElement) {
506
            // Workaround for https://bugs.php.net/bug.php?id=75168 and https://github.com/schmittjoh/serializer/issues/817
507
            // If the "name" is empty means that we are on an not-existent node and subsequent operations on the object will trigger the warning:
508
            // "Node no longer exists"
509
            if ('' === $value->getName()) {
510
                // @todo should be "true", but for collections needs a default collection value. maybe something for the 2.0
511
                return false;
512
            }
513
514
            $xsiAttributes = $value->attributes('http://www.w3.org/2001/XMLSchema-instance');
515
            if (
516
                isset($xsiAttributes['nil'])
517
                && ('true' === (string) $xsiAttributes['nil'] || '1' === (string) $xsiAttributes['nil'])
518
            ) {
519
                return true;
520
            }
521
        }
522
523
        return null === $value;
524
    }
525
}
526