Completed
Pull Request — master (#1194)
by Ivan
38:34 queued 23:35
created

DeserializationGraphNavigator   A

Complexity

Total Complexity 40

Size/Duplication

Total Lines 221
Duplicated Lines 0 %

Test Coverage

Coverage 82.14%

Importance

Changes 0
Metric Value
eloc 97
dl 0
loc 221
ccs 69
cts 84
cp 0.8214
rs 9.2
c 0
b 0
f 0
wmc 40

4 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 15 3
F accept() 0 121 32
A resolveMetadata() 0 14 2
A afterVisitingObject() 0 11 3

How to fix   Complexity   

Complex Class

Complex classes like DeserializationGraphNavigator 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 DeserializationGraphNavigator, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace JMS\Serializer\GraphNavigator;
6
7
use JMS\Serializer\Accessor\AccessorStrategyInterface;
8
use JMS\Serializer\Construction\ObjectConstructorInterface;
9
use JMS\Serializer\DeserializationContext;
10
use JMS\Serializer\EventDispatcher\EventDispatcher;
11
use JMS\Serializer\EventDispatcher\EventDispatcherInterface;
12
use JMS\Serializer\EventDispatcher\ObjectEvent;
13
use JMS\Serializer\EventDispatcher\PreDeserializeEvent;
14
use JMS\Serializer\Exception\ExpressionLanguageRequiredException;
15
use JMS\Serializer\Exception\LogicException;
16
use JMS\Serializer\Exception\NotAcceptableException;
17
use JMS\Serializer\Exception\RuntimeException;
18
use JMS\Serializer\Exclusion\ExpressionLanguageExclusionStrategy;
19
use JMS\Serializer\Expression\ExpressionEvaluatorInterface;
20
use JMS\Serializer\GraphNavigator;
21
use JMS\Serializer\GraphNavigatorInterface;
22
use JMS\Serializer\Handler\HandlerRegistryInterface;
23
use JMS\Serializer\Metadata\ClassMetadata;
24
use JMS\Serializer\NullAwareVisitorInterface;
25
use JMS\Serializer\Visitor\DeserializationVisitorInterface;
26
use Metadata\MetadataFactoryInterface;
27
28
/**
29
 * Handles traversal along the object graph.
30
 *
31
 * This class handles traversal along the graph, and calls different methods
32
 * on visitors, or custom handlers to process its nodes.
33
 *
34
 * @author Johannes M. Schmitt <[email protected]>
35
 */
36
final class DeserializationGraphNavigator extends GraphNavigator implements GraphNavigatorInterface
37
{
38
    /**
39
     * @var DeserializationVisitorInterface
40
     */
41
    protected $visitor;
42
43
    /**
44
     * @var DeserializationContext
45
     */
46
    protected $context;
47
48
    /**
49
     * @var ExpressionLanguageExclusionStrategy
50
     */
51
    private $expressionExclusionStrategy;
52
53
    /**
54
     * @var EventDispatcherInterface
55
     */
56
    private $dispatcher;
57
58
    /**
59
     * @var MetadataFactoryInterface
60
     */
61
    private $metadataFactory;
62 141
63
    /**
64
     * @var HandlerRegistryInterface
65
     */
66
    private $handlerRegistry;
67
68
    /**
69
     * @var ObjectConstructorInterface
70 141
     */
71 141
    private $objectConstructor;
72 141
    /**
73 141
     * @var AccessorStrategyInterface
74 141
     */
75 141
    private $accessor;
76 1
77
    public function __construct(
78 141
        MetadataFactoryInterface $metadataFactory,
79
        HandlerRegistryInterface $handlerRegistry,
80
        ObjectConstructorInterface $objectConstructor,
81
        AccessorStrategyInterface $accessor,
82
        ?EventDispatcherInterface $dispatcher = null,
83
        ?ExpressionEvaluatorInterface $expressionEvaluator = null
84
    ) {
85
        $this->dispatcher = $dispatcher ?: new EventDispatcher();
86
        $this->metadataFactory = $metadataFactory;
87 136
        $this->handlerRegistry = $handlerRegistry;
88
        $this->objectConstructor = $objectConstructor;
89
        $this->accessor = $accessor;
90
        if ($expressionEvaluator) {
91 136
            $this->expressionExclusionStrategy = new ExpressionLanguageExclusionStrategy($expressionEvaluator);
92
        }
93
    }
94
95
    /**
96 136
     * Called for each node of the graph that is being traversed.
97 13
     *
98
     * @param mixed $data the data depends on the direction, and type of visitor
99
     * @param array|null $type array has the format ["name" => string, "params" => array]
100 136
     *
101 136
     * @return mixed the return value depends on the direction, and type of visitor
102 15
     */
103
    public function accept($data, ?array $type = null)
104 122
    {
105 51
        // If the type was not given, we infer the most specific type from the
106
        // input data in serialization mode.
107 117
        if (null === $type) {
108 117
            throw new RuntimeException('The type must be given for all properties when deserializing.');
109 17
        }
110
        // Sometimes data can convey null but is not of a null type.
111 113
        // Visitors can have the power to add this custom null evaluation
112 113
        if ($this->visitor instanceof NullAwareVisitorInterface && true === $this->visitor->isNull($data)) {
113 13
            $type = ['name' => 'NULL', 'params' => []];
114
        }
115 105
116 99
        switch ($type['name']) {
117 25
            case 'NULL':
118
                return $this->visitor->visitNull($data, $type);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->visitor->visitNull($data, $type) targeting JMS\Serializer\Visitor\D...rInterface::visitNull() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
119 93
120 36
            case 'string':
121
                return $this->visitor->visitString($data, $type);
122 81
123
            case 'int':
124
            case 'integer':
125
                return $this->visitor->visitInteger($data, $type);
126
127 81
            case 'bool':
128
            case 'boolean':
129
                return $this->visitor->visitBoolean($data, $type);
130
131 81
            case 'double':
132
            case 'float':
133
                return $this->visitor->visitDouble($data, $type);
134
135
            case 'iterable':
136
                return $this->visitor->visitArray($data, $type);
137
138
            case 'array':
139
                return $this->visitor->visitArray($data, $type);
140 81
141 26
            case 'resource':
142 26
                throw new RuntimeException('Resources are not supported in serialized data.');
143
144 26
            default:
145
                $this->context->increaseDepth();
146
147
                // Trigger pre-serialization callbacks, and listeners if they exist.
148 70
                // Dispatch pre-serialization event before handling data to have ability change type in listener
149
                if ($this->dispatcher->hasListeners('serializer.pre_deserialize', $type['name'], $this->format)) {
150 70
                    $this->dispatcher->dispatch('serializer.pre_deserialize', $type['name'], $this->format, $event = new PreDeserializeEvent($this->context, $data, $type));
151
                    $type = $event->getType();
152
                    $data = $event->getData();
153
                }
154 70
155 14
                // First, try whether a custom handler exists for the given type. This is done
156
                // before loading metadata because the type name might not be a class, but
157
                // could also simply be an artifical type.
158 68
                if (null !== $handler = $this->handlerRegistry->getHandler(GraphNavigatorInterface::DIRECTION_DESERIALIZATION, $type['name'], $this->format)) {
0 ignored issues
show
introduced by
The condition null !== $handler = $thi...'name'], $this->format) is always true.
Loading history...
159
                    $rs = \call_user_func($handler, $this->visitor, $data, $type, $this->context);
0 ignored issues
show
Bug introduced by
It seems like $handler can also be of type object; however, parameter $function of call_user_func() does only seem to accept callable, 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

159
                    $rs = \call_user_func(/** @scrutinizer ignore-type */ $handler, $this->visitor, $data, $type, $this->context);
Loading history...
160
                    $this->context->decreaseDepth();
161
162
                    return $rs;
163
                }
164 68
165
                /** @var ClassMetadata $metadata */
166 68
                $metadata = $this->metadataFactory->getMetadataForClass($type['name']);
167
168 68
                if ($metadata->usingExpression && !$this->expressionExclusionStrategy) {
169 68
                    throw new ExpressionLanguageRequiredException(sprintf('To use conditional exclude/expose in %s you must configure the expression language.', $metadata->name));
170 68
                }
171
172
                if (!empty($metadata->discriminatorMap) && $type['name'] === $metadata->discriminatorBaseClass) {
173
                    $metadata = $this->resolveMetadata($data, $metadata);
174 68
                }
175
176
                if (null !== $this->exclusionStrategy && $this->exclusionStrategy->shouldSkipClass($metadata, $this->context)) {
177
                    $this->context->decreaseDepth();
178 68
179 18
                    return null;
180
                }
181
182 64
                $this->context->pushClassMetadata($metadata);
183
184 64
                $object = $this->objectConstructor->construct($this->visitor, $metadata, $data, $type, $this->context);
185 64
186 4
                if (null === $object) {
187
                    $this->context->popClassMetadata();
188
                    $this->context->decreaseDepth();
189 64
190
                    return $this->visitor->visitNull($data, $type);
191
                }
192 67
193 67
                foreach ($metadata->preDeserializeMethods as $method) {
194
                    $method->invoke($object, [$data]);
195 67
                }
196
197
                $this->visitor->startVisitingObject($metadata, $object, $type);
198
                foreach ($metadata->propertyMetadata as $propertyMetadata) {
199 14
                    if (null !== $this->exclusionStrategy && $this->exclusionStrategy->shouldSkipProperty($propertyMetadata, $this->context)) {
200
                        continue;
201 14
                    }
202
203 12
                    if (null !== $this->expressionExclusionStrategy && $this->expressionExclusionStrategy->shouldSkipProperty($propertyMetadata, $this->context)) {
204
                        continue;
205
                    }
206
207
                    if ($propertyMetadata->readOnly) {
208
                        continue;
209
                    }
210
211
                    $this->context->pushPropertyMetadata($propertyMetadata);
212 12
                    try {
213
                        $v = $this->visitor->visitProperty($propertyMetadata, $data);
214
                        $this->accessor->setValue($object, $v, $propertyMetadata, $this->context);
215 67
                    } catch (NotAcceptableException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
216
                    }
217 67
                    $this->context->popPropertyMetadata();
218 67
                }
219
220 67
                $rs = $this->visitor->endVisitingObject($metadata, $data, $type);
221 4
                $this->afterVisitingObject($metadata, $rs, $type);
222
223
                return $rs;
224 67
        }
225 1
    }
226
227 67
    /**
228
     * @param mixed $data
229
     */
230
    private function resolveMetadata($data, ClassMetadata $metadata): ?ClassMetadata
231
    {
232
        $typeValue = $this->visitor->visitDiscriminatorMapProperty($data, $metadata);
233
234
        if (!isset($metadata->discriminatorMap[$typeValue])) {
235
            throw new LogicException(sprintf(
236
                'The type value "%s" does not exist in the discriminator map of class "%s". Available types: %s',
237
                $typeValue,
238
                $metadata->name,
239
                implode(', ', array_keys($metadata->discriminatorMap))
240
            ));
241
        }
242
243
        return $this->metadataFactory->getMetadataForClass($metadata->discriminatorMap[$typeValue]);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->metadataFa...minatorMap[$typeValue]) could return the type Metadata\ClassHierarchyMetadata which is incompatible with the type-hinted return JMS\Serializer\Metadata\ClassMetadata|null. Consider adding an additional type-check to rule them out.
Loading history...
244
    }
245
246
    private function afterVisitingObject(ClassMetadata $metadata, object $object, array $type): void
247
    {
248
        $this->context->decreaseDepth();
249
        $this->context->popClassMetadata();
250
251
        foreach ($metadata->postDeserializeMethods as $method) {
252
            $method->invoke($object);
253
        }
254
255
        if ($this->dispatcher->hasListeners('serializer.post_deserialize', $metadata->name, $this->format)) {
256
            $this->dispatcher->dispatch('serializer.post_deserialize', $metadata->name, $this->format, new ObjectEvent($this->context, $object, $type));
257
        }
258
    }
259
}
260