Completed
Pull Request — master (#1303)
by Ayrton
06:25
created

DeserializationGraphNavigator   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 232
Duplicated Lines 0 %

Test Coverage

Coverage 82.14%

Importance

Changes 0
Metric Value
eloc 103
c 0
b 0
f 0
dl 0
loc 232
ccs 69
cts 84
cp 0.8214
rs 8.96
wmc 43

4 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 15 3
F accept() 0 128 34
A resolveMetadata() 0 18 3
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\Exception\SkipHandlerException;
19
use JMS\Serializer\Exclusion\ExpressionLanguageExclusionStrategy;
20
use JMS\Serializer\Expression\ExpressionEvaluatorInterface;
21
use JMS\Serializer\GraphNavigator;
22
use JMS\Serializer\GraphNavigatorInterface;
23
use JMS\Serializer\Handler\HandlerRegistryInterface;
24
use JMS\Serializer\Metadata\ClassMetadata;
25
use JMS\Serializer\NullAwareVisitorInterface;
26
use JMS\Serializer\Visitor\DeserializationVisitorInterface;
27
use Metadata\MetadataFactoryInterface;
28
29
/**
30
 * Handles traversal along the object graph.
31
 *
32
 * This class handles traversal along the graph, and calls different methods
33
 * on visitors, or custom handlers to process its nodes.
34
 *
35
 * @author Johannes M. Schmitt <[email protected]>
36
 */
37
final class DeserializationGraphNavigator extends GraphNavigator implements GraphNavigatorInterface
38
{
39
    /**
40
     * @var DeserializationVisitorInterface
41
     */
42
    protected $visitor;
43
44
    /**
45
     * @var DeserializationContext
46
     */
47
    protected $context;
48
49
    /**
50
     * @var ExpressionLanguageExclusionStrategy
51
     */
52
    private $expressionExclusionStrategy;
53
54
    /**
55
     * @var EventDispatcherInterface
56
     */
57
    private $dispatcher;
58
59
    /**
60
     * @var MetadataFactoryInterface
61
     */
62 141
    private $metadataFactory;
63
64
    /**
65
     * @var HandlerRegistryInterface
66
     */
67
    private $handlerRegistry;
68
69
    /**
70 141
     * @var ObjectConstructorInterface
71 141
     */
72 141
    private $objectConstructor;
73 141
    /**
74 141
     * @var AccessorStrategyInterface
75 141
     */
76 1
    private $accessor;
77
78 141
    public function __construct(
79
        MetadataFactoryInterface $metadataFactory,
80
        HandlerRegistryInterface $handlerRegistry,
81
        ObjectConstructorInterface $objectConstructor,
82
        AccessorStrategyInterface $accessor,
83
        ?EventDispatcherInterface $dispatcher = null,
84
        ?ExpressionEvaluatorInterface $expressionEvaluator = null
85
    ) {
86
        $this->dispatcher = $dispatcher ?: new EventDispatcher();
87 136
        $this->metadataFactory = $metadataFactory;
88
        $this->handlerRegistry = $handlerRegistry;
89
        $this->objectConstructor = $objectConstructor;
90
        $this->accessor = $accessor;
91 136
        if ($expressionEvaluator) {
92
            $this->expressionExclusionStrategy = new ExpressionLanguageExclusionStrategy($expressionEvaluator);
93
        }
94
    }
95
96 136
    /**
97 13
     * Called for each node of the graph that is being traversed.
98
     *
99
     * @param mixed $data the data depends on the direction, and type of visitor
100 136
     * @param array|null $type array has the format ["name" => string, "params" => array]
101 136
     *
102 15
     * @return mixed the return value depends on the direction, and type of visitor
103
     */
104 122
    public function accept($data, ?array $type = null)
105 51
    {
106
        // If the type was not given, we infer the most specific type from the
107 117
        // input data in serialization mode.
108 117
        if (null === $type) {
109 17
            throw new RuntimeException('The type must be given for all properties when deserializing.');
110
        }
111 113
112 113
        // Sometimes data can convey null but is not of a null type.
113 13
        // Visitors can have the power to add this custom null evaluation
114
        if ($this->visitor instanceof NullAwareVisitorInterface && true === $this->visitor->isNull($data)) {
115 105
            $type = ['name' => 'NULL', 'params' => []];
116 99
        }
117 25
118
        switch ($type['name']) {
119 93
            case 'NULL':
120 36
                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...
121
122 81
            case 'string':
123
                return $this->visitor->visitString($data, $type);
124
125
            case 'int':
126
            case 'integer':
127 81
                return $this->visitor->visitInteger($data, $type);
128
129
            case 'bool':
130
            case 'boolean':
131 81
                return $this->visitor->visitBoolean($data, $type);
132
133
            case 'double':
134
            case 'float':
135
                return $this->visitor->visitDouble($data, $type);
136
137
            case 'iterable':
138
                return $this->visitor->visitArray($data, $type);
139
140 81
            case 'array':
141 26
                return $this->visitor->visitArray($data, $type);
142 26
143
            case 'resource':
144 26
                throw new RuntimeException('Resources are not supported in serialized data.');
145
146
            default:
147
                $this->context->increaseDepth();
148 70
149
                // Trigger pre-serialization callbacks, and listeners if they exist.
150 70
                // Dispatch pre-serialization event before handling data to have ability change type in listener
151
                if ($this->dispatcher->hasListeners('serializer.pre_deserialize', $type['name'], $this->format)) {
152
                    $this->dispatcher->dispatch('serializer.pre_deserialize', $type['name'], $this->format, $event = new PreDeserializeEvent($this->context, $data, $type));
153
                    $type = $event->getType();
154 70
                    $data = $event->getData();
155 14
                }
156
157
                // First, try whether a custom handler exists for the given type. This is done
158 68
                // before loading metadata because the type name might not be a class, but
159
                // could also simply be an artifical type.
160
                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...
161
                    try {
162
                        $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 $callback 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

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