Completed
Pull Request — master (#840)
by
unknown
03:40
created

GraphNavigator::accept()   F

Complexity

Conditions 59
Paths > 20000

Size

Total Lines 188
Code Lines 104

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 116
CRAP Score 59.6412

Importance

Changes 0
Metric Value
dl 0
loc 188
ccs 116
cts 123
cp 0.9431
rs 2
c 0
b 0
f 0
cc 59
eloc 104
nc 32121
nop 3
crap 59.6412

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\Construction\ObjectConstructorInterface;
22
use JMS\Serializer\EventDispatcher\EventDispatcherInterface;
23
use JMS\Serializer\EventDispatcher\ObjectEvent;
24
use JMS\Serializer\EventDispatcher\PreDeserializeEvent;
25
use JMS\Serializer\EventDispatcher\PreSerializeEvent;
26
use JMS\Serializer\Exception\ExpressionLanguageRequiredException;
27
use JMS\Serializer\Exception\InvalidArgumentException;
28
use JMS\Serializer\Exception\RuntimeException;
29
use JMS\Serializer\Exclusion\ExpressionLanguageExclusionStrategy;
30
use JMS\Serializer\Expression\ExpressionEvaluatorInterface;
31
use JMS\Serializer\Handler\HandlerRegistryInterface;
32
use JMS\Serializer\Metadata\ClassMetadata;
33
use Metadata\MetadataFactoryInterface;
34
35
/**
36
 * Handles traversal along the object graph.
37
 *
38
 * This class handles traversal along the graph, and calls different methods
39
 * on visitors, or custom handlers to process its nodes.
40
 *
41
 * @author Johannes M. Schmitt <[email protected]>
42
 */
43
final class GraphNavigator
44
{
45
    const DIRECTION_SERIALIZATION = 1;
46
    const DIRECTION_DESERIALIZATION = 2;
47
48
    /**
49
     * @var ExpressionLanguageExclusionStrategy
50
     */
51
    private $expressionExclusionStrategy;
52
53
    private $dispatcher;
54
    private $metadataFactory;
55
    private $handlerRegistry;
56
    private $objectConstructor;
57
58
    /**
59
     * Parses a direction string to one of the direction constants.
60
     *
61
     * @param string $dirStr
62
     *
63
     * @return integer
64
     */
65 6
    public static function parseDirection($dirStr)
66
    {
67 6
        switch (strtolower($dirStr)) {
68 6
            case 'serialization':
69 6
                return self::DIRECTION_SERIALIZATION;
70
71 3
            case 'deserialization':
72 3
                return self::DIRECTION_DESERIALIZATION;
73
74
            default:
75
                throw new InvalidArgumentException(sprintf('The direction "%s" does not exist.', $dirStr));
76 2
        }
77
    }
78
79 402
    public function __construct(
80
        MetadataFactoryInterface $metadataFactory,
81
        HandlerRegistryInterface $handlerRegistry,
82
        ObjectConstructorInterface $objectConstructor,
83
        EventDispatcherInterface $dispatcher = null,
84
        ExpressionEvaluatorInterface $expressionEvaluator = null
85
    )
86
    {
87 402
        $this->dispatcher = $dispatcher;
88 402
        $this->metadataFactory = $metadataFactory;
89 402
        $this->handlerRegistry = $handlerRegistry;
90 402
        $this->objectConstructor = $objectConstructor;
91 402
        if ($expressionEvaluator) {
92 33
            $this->expressionExclusionStrategy = new ExpressionLanguageExclusionStrategy($expressionEvaluator);
93 33
        }
94 402
    }
95
96
    /**
97
     * 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
     * @param null|array $type array has the format ["name" => string, "params" => array]
101
     * @param Context $context
102
     * @return mixed the return value depends on the direction, and type of visitor
103
     */
104 375
    public function accept($data, array $type = null, Context $context)
105
    {
106 375
        $visitor = $context->getVisitor();
107
108
        // If the type was not given, we infer the most specific type from the
109
        // input data in serialization mode.
110 375
        if (null === $type) {
111 324
            if ($context instanceof DeserializationContext) {
112
                throw new RuntimeException('The type must be given for all properties when deserializing.');
113 8
            }
114
115 324
            $typeName = gettype($data);
116 324
            if ('object' === $typeName) {
117 235
                $typeName = get_class($data);
118 235
            }
119
120 324
            $type = array('name' => $typeName, 'params' => array());
121 324
        }
122
        // If the data is null, we have to force the type to null regardless of the input in order to
123
        // guarantee correct handling of null values, and not have any internal auto-casting behavior.
124 268
        else if ($context instanceof SerializationContext && null === $data) {
125 20
            $type = array('name' => 'NULL', 'params' => array());
126 20
        }
127
        // Sometimes data can convey null but is not of a null type.
128
        // Visitors can have the power to add this custom null evaluation
129 375
        if ($visitor instanceof NullAwareVisitorInterface && $visitor->isNull($data) === true) {
130 7
            $type = array('name' => 'NULL', 'params' => array());
131 7
        }
132
133 375
        switch ($type['name']) {
134 375
            case 'NULL':
135 48
                return $visitor->visitNull($data, $type, $context);
136
137 362
            case 'string':
138 214
                return $visitor->visitString($data, $type, $context);
139
140 356
            case 'int':
141 356
            case 'integer':
142 65
                return $visitor->visitInteger($data, $type, $context);
143
144 351
            case 'bool':
145 351
            case 'boolean':
146 22
                return $visitor->visitBoolean($data, $type, $context);
147
148 340
            case 'double':
149 340
            case 'float':
150 34
                return $visitor->visitDouble($data, $type, $context);
151
152 325
            case 'array':
153 141
                $metadata = $context->getMetadataStack()->count() ? $context->getMetadataStack()->top() : null;
154 141
                if ($visitor instanceof XmlDeserializationVisitor && null !== $metadata && $metadata->xmlKeyValuePairs) {
155 1
                    $visitor->setCurrentMetadata($metadata);
156 1
                    $result = $visitor->visitArray($data, $type, $context);
157 1
                    $visitor->revertCurrentMetadata();
158
159 1
                    return $result;
160
                }
161
162 141
                return $visitor->visitArray($data, $type, $context);
163
164 259
            case 'resource':
165 1
                $msg = 'Resources are not supported in serialized data.';
166 1
                if ($context instanceof SerializationContext && null !== $path = $context->getPath()) {
167
                    $msg .= ' Path: ' . $path;
168
                }
169
170 1
                throw new RuntimeException($msg);
171
172 258
            default:
173
                // TODO: The rest of this method needs some refactoring.
174 258
                if ($context instanceof SerializationContext) {
175 239
                    if (null !== $data) {
176 239
                        if ($context->isVisiting($data)) {
177 3
                            return null;
178
                        }
179 239
                        $context->startVisiting($data);
180 239
                    }
181
182
                    // If we're serializing a polymorphic type, then we'll be interested in the
183
                    // metadata for the actual type of the object, not the base class.
184 239
                    if (class_exists($type['name'], false) || interface_exists($type['name'], false)) {
185 239
                        if (is_subclass_of($data, $type['name'], false)) {
0 ignored issues
show
Bug introduced by
Due to PHP Bug #53727, is_subclass_of might return inconsistent results on some PHP versions if $type['name'] can be an interface. If so, you could instead use ReflectionClass::implementsInterface.
Loading history...
186 6
                            $type = array('name' => get_class($data), 'params' => array());
187 6
                        }
188 239
                    }
189 258
                } elseif ($context instanceof DeserializationContext) {
190 74
                    $context->increaseDepth();
191 74
                }
192
193
                // Trigger pre-serialization callbacks, and listeners if they exist.
194
                // Dispatch pre-serialization event before handling data to have ability change type in listener
195 258
                if ($context instanceof SerializationContext) {
196 239
                    if (null !== $this->dispatcher && $this->dispatcher->hasListeners('serializer.pre_serialize', $type['name'], $context->getFormat())) {
197 234
                        $this->dispatcher->dispatch('serializer.pre_serialize', $type['name'], $context->getFormat(), $event = new PreSerializeEvent($context, $data, $type));
198 234
                        $type = $event->getType();
199 234
                    }
200 258
                } elseif ($context instanceof DeserializationContext) {
201 74
                    if (null !== $this->dispatcher && $this->dispatcher->hasListeners('serializer.pre_deserialize', $type['name'], $context->getFormat())) {
202
                        $this->dispatcher->dispatch('serializer.pre_deserialize', $type['name'], $context->getFormat(), $event = new PreDeserializeEvent($context, $data, $type));
203
                        $type = $event->getType();
204
                        $data = $event->getData();
205
                    }
206 74
                }
207
208
                // First, try whether a custom handler exists for the given type. This is done
209
                // before loading metadata because the type name might not be a class, but
210
                // could also simply be an artifical type.
211 258
                if (null !== $handler = $this->handlerRegistry->getHandler($context->getDirection(), $type['name'], $context->getFormat())) {
212 62
                    $rs = call_user_func($handler, $visitor, $data, $type, $context);
213 62
                    $this->leaveScope($context, $data);
214
215 62
                    return $rs;
216
                }
217
218 230
                $exclusionStrategy = $context->getExclusionStrategy();
219
220
                /** @var $metadata ClassMetadata */
221 230
                $metadata = $this->metadataFactory->getMetadataForClass($type['name']);
222
223 227
                if ($metadata->usingExpression && !$this->expressionExclusionStrategy) {
224 3
                    throw new ExpressionLanguageRequiredException("To use conditional exclude/expose in {$metadata->name} you must configure the expression language.");
225
                }
226
227 224
                if ($context instanceof DeserializationContext && !empty($metadata->discriminatorMap) && $type['name'] === $metadata->discriminatorBaseClass) {
228 13
                    $metadata = $this->resolveMetadata($data, $metadata);
229 11
                }
230
231 222
                if (null !== $exclusionStrategy && $exclusionStrategy->shouldSkipClass($metadata, $context)) {
0 ignored issues
show
Documentation introduced by
$metadata is of type object<Metadata\ClassHie...ableClassMetadata>|null, but the function expects a object<JMS\Serializer\Metadata\ClassMetadata>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
232 6
                    $this->leaveScope($context, $data);
233
234 6
                    return null;
235
                }
236
237 222
                $context->pushClassMetadata($metadata);
0 ignored issues
show
Documentation introduced by
$metadata is of type object<Metadata\ClassHie...ableClassMetadata>|null, but the function expects a object<JMS\Serializer\Metadata\ClassMetadata>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
238
239 222
                if ($context instanceof SerializationContext) {
240 206
                    foreach ($metadata->preSerializeMethods as $method) {
241 3
                        $method->invoke($data);
242 206
                    }
243 206
                }
244
245 222
                $object = $data;
246 222
                if ($context instanceof DeserializationContext) {
247 63
                    $object = $this->objectConstructor->construct($visitor, $metadata, $data, $type, $context);
0 ignored issues
show
Documentation introduced by
$metadata is of type object<Metadata\ClassHie...ableClassMetadata>|null, but the function expects a object<JMS\Serializer\Metadata\ClassMetadata>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
248 63
                }
249
250 222
                if (isset($metadata->handlerCallbacks[$context->getDirection()][$context->getFormat()])) {
251 3
                    $rs = $object->{$metadata->handlerCallbacks[$context->getDirection()][$context->getFormat()]}(
252 3
                        $visitor,
253 3
                        $context instanceof SerializationContext ? null : $data,
254
                        $context
255 3
                    );
256 3
                    $this->afterVisitingObject($metadata, $object, $type, $context);
0 ignored issues
show
Documentation introduced by
$metadata is of type object<Metadata\ClassHie...MergeableClassMetadata>, but the function expects a object<JMS\Serializer\Metadata\ClassMetadata>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
257
258 3
                    return $context instanceof SerializationContext ? $rs : $object;
259
                }
260
261 219
                $visitor->startVisitingObject($metadata, $object, $type, $context);
0 ignored issues
show
Documentation introduced by
$metadata is of type object<Metadata\ClassHie...MergeableClassMetadata>, but the function expects a object<JMS\Serializer\Metadata\ClassMetadata>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
262 219
                foreach ($metadata->propertyMetadata as $propertyMetadata) {
263 217
                    if (null !== $exclusionStrategy && $exclusionStrategy->shouldSkipProperty($propertyMetadata, $context)) {
264 17
                        continue;
265
                    }
266
267 217
                    if (null !== $this->expressionExclusionStrategy && $this->expressionExclusionStrategy->shouldSkipProperty($propertyMetadata, $context)) {
268 23
                        continue;
269
                    }
270
271 217
                    if ($context instanceof DeserializationContext && $propertyMetadata->readOnly) {
272 17
                        continue;
273
                    }
274
275 217
                    $context->pushPropertyMetadata($propertyMetadata);
276 217
                    $visitor->visitProperty($propertyMetadata, $data, $context);
277 213
                    $context->popPropertyMetadata();
278 215
                }
279
280 213
                if ($context instanceof SerializationContext) {
281 198
                    $this->afterVisitingObject($metadata, $data, $type, $context);
0 ignored issues
show
Documentation introduced by
$metadata is of type object<Metadata\ClassHie...MergeableClassMetadata>, but the function expects a object<JMS\Serializer\Metadata\ClassMetadata>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
282
283 198
                    return $visitor->endVisitingObject($metadata, $data, $type, $context);
0 ignored issues
show
Documentation introduced by
$metadata is of type object<Metadata\ClassHie...MergeableClassMetadata>, but the function expects a object<JMS\Serializer\Metadata\ClassMetadata>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
284
                }
285
286 63
                $rs = $visitor->endVisitingObject($metadata, $data, $type, $context);
0 ignored issues
show
Documentation introduced by
$metadata is of type object<Metadata\ClassHie...MergeableClassMetadata>, but the function expects a object<JMS\Serializer\Metadata\ClassMetadata>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
287 63
                $this->afterVisitingObject($metadata, $rs, $type, $context);
0 ignored issues
show
Documentation introduced by
$metadata is of type object<Metadata\ClassHie...MergeableClassMetadata>, but the function expects a object<JMS\Serializer\Metadata\ClassMetadata>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
288
289 63
                return $rs;
290 258
        }
291
    }
292
293 13
    private function resolveMetadata($data, ClassMetadata $metadata)
294
    {
295 13
        switch (true) {
296 13
            case is_array($data) && isset($data[$metadata->discriminatorFieldName]):
297 5
                $typeValue = (string)$data[$metadata->discriminatorFieldName];
298 5
                break;
299
300
            // Check XML attribute for discriminatorFieldName
301 8
            case is_object($data) && $metadata->xmlDiscriminatorAttribute && isset($data[$metadata->discriminatorFieldName]):
302 1
                $typeValue = (string)$data[$metadata->discriminatorFieldName];
303 1
                break;
304
305
            // Check XML element with namespace for discriminatorFieldName
306 7
            case is_object($data) && !$metadata->xmlDiscriminatorAttribute && null !== $metadata->xmlDiscriminatorNamespace && isset($data->children($metadata->xmlDiscriminatorNamespace)->{$metadata->discriminatorFieldName}):
307 1
                $typeValue = (string)$data->children($metadata->xmlDiscriminatorNamespace)->{$metadata->discriminatorFieldName};
308 1
                break;
309
310
            // Check XML element for discriminatorFieldName
311 6
            case is_object($data) && isset($data->{$metadata->discriminatorFieldName}):
312 4
                $typeValue = (string)$data->{$metadata->discriminatorFieldName};
313 4
                break;
314
315 2
            default:
316 2
                throw new \LogicException(sprintf(
317 2
                    'The discriminator field name "%s" for base-class "%s" was not found in input data.',
318 2
                    $metadata->discriminatorFieldName,
319 2
                    $metadata->name
320 2
                ));
321 2
        }
322
323 11
        if (!isset($metadata->discriminatorMap[$typeValue])) {
324
            throw new \LogicException(sprintf(
325
                'The type value "%s" does not exist in the discriminator map of class "%s". Available types: %s',
326
                $typeValue,
327
                $metadata->name,
328
                implode(', ', array_keys($metadata->discriminatorMap))
329
            ));
330
        }
331
332 11
        return $this->metadataFactory->getMetadataForClass($metadata->discriminatorMap[$typeValue]);
333
    }
334
335 244
    private function leaveScope(Context $context, $data)
336
    {
337 244
        if ($context instanceof SerializationContext) {
338 228
            $context->stopVisiting($data);
339 244
        } elseif ($context instanceof DeserializationContext) {
340 71
            $context->decreaseDepth();
341 71
        }
342 244
    }
343
344 216
    private function afterVisitingObject(ClassMetadata $metadata, $object, array $type, Context $context)
345
    {
346 216
        $this->leaveScope($context, $object);
347 216
        $context->popClassMetadata();
348
349 216
        if ($context instanceof SerializationContext) {
350 201
            foreach ($metadata->postSerializeMethods as $method) {
351 3
                $method->invoke($object);
352 201
            }
353
354 201
            if (null !== $this->dispatcher && $this->dispatcher->hasListeners('serializer.post_serialize', $metadata->name, $context->getFormat())) {
355 2
                $this->dispatcher->dispatch('serializer.post_serialize', $metadata->name, $context->getFormat(), new ObjectEvent($context, $object, $type));
356 2
            }
357
358 201
            return;
359
        }
360
361 65
        foreach ($metadata->postDeserializeMethods as $method) {
362 4
            $method->invoke($object);
363 65
        }
364
365 65
        if (null !== $this->dispatcher && $this->dispatcher->hasListeners('serializer.post_deserialize', $metadata->name, $context->getFormat())) {
366 1
            $this->dispatcher->dispatch('serializer.post_deserialize', $metadata->name, $context->getFormat(), new ObjectEvent($context, $object, $type));
367 1
        }
368 65
    }
369
}
370