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

SerializationGraphNavigator::accept()   F

Complexity

Conditions 48
Paths 5016

Size

Total Lines 160
Code Lines 86

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 68
CRAP Score 48.0538

Importance

Changes 4
Bugs 0 Features 0
Metric Value
cc 48
eloc 86
c 4
b 0
f 0
nc 5016
nop 2
dl 0
loc 160
ccs 68
cts 70
cp 0.9714
crap 48.0538
rs 0

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\GraphNavigator;
6
7
use JMS\Serializer\Accessor\AccessorStrategyInterface;
8
use JMS\Serializer\Context;
9
use JMS\Serializer\EventDispatcher\EventDispatcher;
10
use JMS\Serializer\EventDispatcher\EventDispatcherInterface;
11
use JMS\Serializer\EventDispatcher\ObjectEvent;
12
use JMS\Serializer\EventDispatcher\PreSerializeEvent;
13
use JMS\Serializer\Exception\CircularReferenceDetectedException;
14
use JMS\Serializer\Exception\ExcludedClassException;
15
use JMS\Serializer\Exception\ExpressionLanguageRequiredException;
16
use JMS\Serializer\Exception\InvalidArgumentException;
17
use JMS\Serializer\Exception\NotAcceptableException;
18
use JMS\Serializer\Exception\RuntimeException;
19
use JMS\Serializer\Exception\SkipHandlerException;
20
use JMS\Serializer\Exclusion\ExpressionLanguageExclusionStrategy;
21
use JMS\Serializer\Expression\ExpressionEvaluatorInterface;
22
use JMS\Serializer\Functions;
23
use JMS\Serializer\GraphNavigator;
24
use JMS\Serializer\GraphNavigatorInterface;
25
use JMS\Serializer\Handler\HandlerRegistryInterface;
26
use JMS\Serializer\Metadata\ClassMetadata;
27
use JMS\Serializer\NullAwareVisitorInterface;
28
use JMS\Serializer\SerializationContext;
29
use JMS\Serializer\Visitor\SerializationVisitorInterface;
30
use JMS\Serializer\VisitorInterface;
31
use Metadata\MetadataFactoryInterface;
32
33
use function assert;
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 SerializationGraphNavigator extends GraphNavigator
44
{
45
    /**
46
     * @var SerializationVisitorInterface
47
     */
48
    protected $visitor;
49
50
    /**
51
     * @var SerializationContext
52
     */
53
    protected $context;
54
55
    /**
56
     * @var ExpressionLanguageExclusionStrategy
57
     */
58
    private $expressionExclusionStrategy;
59
60
    /**
61
     * @var EventDispatcherInterface
62
     */
63
    private $dispatcher;
64
65
    /**
66
     * @var MetadataFactoryInterface
67
     */
68 290
    private $metadataFactory;
69
70
    /**
71
     * @var HandlerRegistryInterface
72
     */
73
    private $handlerRegistry;
74
    /**
75 290
     * @var AccessorStrategyInterface
76 290
     */
77 290
    private $accessor;
78 290
79
    /**
80 290
     * @var bool
81 25
     */
82
    private $shouldSerializeNull;
83 290
84
    public function __construct(
85 290
        MetadataFactoryInterface $metadataFactory,
86
        HandlerRegistryInterface $handlerRegistry,
87 290
        AccessorStrategyInterface $accessor,
88 290
        ?EventDispatcherInterface $dispatcher = null,
89 290
        ?ExpressionEvaluatorInterface $expressionEvaluator = null
90
    ) {
91
        $this->dispatcher = $dispatcher ?: new EventDispatcher();
92
        $this->metadataFactory = $metadataFactory;
93
        $this->handlerRegistry = $handlerRegistry;
94
        $this->accessor = $accessor;
95
96
        if ($expressionEvaluator) {
97
            $this->expressionExclusionStrategy = new ExpressionLanguageExclusionStrategy($expressionEvaluator);
98 289
        }
99
    }
100
101
    public function initialize(VisitorInterface $visitor, Context $context): void
102 289
    {
103
        assert($context instanceof SerializationContext);
104 267
105 267
        parent::initialize($visitor, $context);
106 193
        $this->shouldSerializeNull = $context->shouldSerializeNull();
107
    }
108
109 267
    /**
110
     * Called for each node of the graph that is being traversed.
111
     *
112
     * @param mixed $data the data depends on the direction, and type of visitor
113 154
     * @param array|null $type array has the format ["name" => string, "params" => array]
114 5
     *
115
     * @return mixed the return value depends on the direction, and type of visitor
116
     */
117
    public function accept($data, ?array $type = null)
118 289
    {
119
        // If the type was not given, we infer the most specific type from the
120
        // input data in serialization mode.
121
        if (null === $type) {
122 289
            $typeName = \gettype($data);
123 289
            if ('object' === $typeName) {
124 38
                $typeName = \get_class($data);
125 15
            }
126
127 23
            $type = ['name' => $typeName, 'params' => []];
128
        } elseif (null === $data) {
129 265
            // If the data is null, we have to force the type to null regardless of the input in order to
130 163
            // guarantee correct handling of null values, and not have any internal auto-casting behavior.
131
            $type = ['name' => 'NULL', 'params' => []];
132 261
        }
133 261
134 48
        // Sometimes data can convey null but is not of a null type.
135
        // Visitors can have the power to add this custom null evaluation
136 258
        if ($this->visitor instanceof NullAwareVisitorInterface && true === $this->visitor->isNull($data)) {
137 258
            $type = ['name' => 'NULL', 'params' => []];
138 13
        }
139
140 253
        switch ($type['name']) {
141 244
            case 'NULL':
142 20
                if (!$this->shouldSerializeNull && !$this->isRootNullAllowed()) {
143
                    throw new NotAcceptableException();
144 244
                }
145 106
146
                return $this->visitor->visitNull($data, $type);
147 206
148 1
            case 'string':
149 1
                return $this->visitor->visitString((string) $data, $type);
150
151
            case 'int':
152
            case 'integer':
153 1
                return $this->visitor->visitInteger((int) $data, $type);
154
155
            case 'bool':
156
            case 'boolean':
157 205
                return $this->visitor->visitBoolean((bool) $data, $type);
158 205
159 4
            case 'double':
160
            case 'float':
161 205
                return $this->visitor->visitDouble((float) $data, $type);
162
163
            case 'iterable':
164
                return $this->visitor->visitArray(Functions::iterableToArray($data), $type);
165
166 205
            case 'array':
167 203
            case 'list':
168 4
                return $this->visitor->visitArray((array) $data, $type);
169
170
            case 'resource':
171
                $msg = 'Resources are not supported in serialized data.';
172
                if (null !== $path = $this->context->getPath()) {
173
                    $msg .= ' Path: ' . $path;
174 205
                }
175 204
176 204
                throw new RuntimeException($msg);
177
178
            default:
179
                if (null !== $data) {
180
                    if ($this->context->isVisiting($data)) {
181
                        throw new CircularReferenceDetectedException();
182 205
                    }
183 49
184 49
                    $this->context->startVisiting($data);
185
                }
186 49
187
                // If we're serializing a polymorphic type, then we'll be interested in the
188
                // metadata for the actual type of the object, not the base class.
189
                if (class_exists($type['name'], false) || interface_exists($type['name'], false)) {
190 181
                    if (is_subclass_of($data, $type['name'], false) && null === $this->handlerRegistry->getHandler(GraphNavigatorInterface::DIRECTION_SERIALIZATION, $type['name'], $this->format)) {
0 ignored issues
show
introduced by
The condition null === $this->handlerR...'name'], $this->format) is always false.
Loading history...
191
                        $type = ['name' => \get_class($data), 'params' => $type['params'] ?? []];
192 179
                    }
193 2
                }
194
195
                // Trigger pre-serialization callbacks, and listeners if they exist.
196 177
                // Dispatch pre-serialization event before handling data to have ability change type in listener
197 10
                if ($this->dispatcher->hasListeners('serializer.pre_serialize', $type['name'], $this->format)) {
198
                    $this->dispatcher->dispatch('serializer.pre_serialize', $type['name'], $this->format, $event = new PreSerializeEvent($this->context, $data, $type));
199 10
                    $type = $event->getType();
200
                }
201
202 173
                // First, try whether a custom handler exists for the given type. This is done
203
                // before loading metadata because the type name might not be a class, but
204 173
                // could also simply be an artifical type.
205 2
                if (null !== $handler = $this->handlerRegistry->getHandler(GraphNavigatorInterface::DIRECTION_SERIALIZATION, $type['name'], $this->format)) {
0 ignored issues
show
introduced by
The condition null !== $handler = $thi...'name'], $this->format) is always true.
Loading history...
206
                    try {
207
                        $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

207
                        $rs = \call_user_func(/** @scrutinizer ignore-type */ $handler, $this->visitor, $data, $type, $this->context);
Loading history...
208 173
                        $this->context->stopVisiting($data);
209 173
210 172
                        return $rs;
211 16
                    } catch (SkipHandlerException $e) {
212
                        // Skip handler, fallback to default behavior
213
                    } catch (NotAcceptableException $e) {
214 172
                        $this->context->stopVisiting($data);
215 16
216
                        throw $e;
217
                    }
218 172
                }
219
220 170
                $metadata = $this->metadataFactory->getMetadataForClass($type['name']);
221 24
                \assert($metadata instanceof ClassMetadata);
222
223
                if ($metadata->usingExpression && null === $this->expressionExclusionStrategy) {
224 168
                    throw new ExpressionLanguageRequiredException(sprintf('To use conditional exclude/expose in %s you must configure the expression language.', $metadata->name));
225 168
                }
226 167
227
                if (null !== $this->exclusionStrategy && $this->exclusionStrategy->shouldSkipClass($metadata, $this->context)) {
228
                    $this->context->stopVisiting($data);
229 169
230
                    throw new ExcludedClassException();
231 169
                }
232
233
                if (null !== $this->expressionExclusionStrategy && $this->expressionExclusionStrategy->shouldSkipClass($metadata, $this->context)) {
234
                    $this->context->stopVisiting($data);
235 169
236
                    throw new ExcludedClassException();
237 169
                }
238 169
239
                if (!is_object($data)) {
240 169
                    throw new InvalidArgumentException('Value at ' . $this->context->getPath() . ' is expected to be an object of class ' . $type['name'] . ' but is of type ' . gettype($data));
241 2
                }
242
243
                $this->context->pushClassMetadata($metadata);
244 169
245 2
                foreach ($metadata->preSerializeMethods as $method) {
246
                    $method->invoke($data);
247 169
                }
248
249
                $this->visitor->startVisitingObject($metadata, $data, $type);
250
                foreach ($metadata->propertyMetadata as $propertyMetadata) {
251
                    if (null !== $this->exclusionStrategy && $this->exclusionStrategy->shouldSkipProperty($propertyMetadata, $this->context)) {
252
                        continue;
253
                    }
254
255
                    if (null !== $this->expressionExclusionStrategy && $this->expressionExclusionStrategy->shouldSkipProperty($propertyMetadata, $this->context)) {
256
                        continue;
257
                    }
258
259
                    $v = $this->accessor->getValue($data, $propertyMetadata, $this->context);
260
261
                    if (null === $v && true !== $this->shouldSerializeNull) {
262
                        continue;
263
                    }
264
265
                    if (null !== $this->exclusionStrategy && $this->exclusionStrategy->shouldSkipPropertyWithValue($propertyMetadata, $this->context, $v)) {
266
                        continue;
267
                    }
268
269
                    $this->context->pushPropertyMetadata($propertyMetadata);
270
                    $this->visitor->visitProperty($propertyMetadata, $v);
271
                    $this->context->popPropertyMetadata();
272
                }
273
274
                $this->afterVisitingObject($metadata, $data, $type);
275
276
                return $this->visitor->endVisitingObject($metadata, $data, $type);
277
        }
278
    }
279
280
    private function isRootNullAllowed(): bool
281
    {
282
        return $this->context->hasAttribute('allows_root_null') && $this->context->getAttribute('allows_root_null') && 0 === $this->context->getVisitingSet()->count();
283
    }
284
285
    private function afterVisitingObject(ClassMetadata $metadata, object $object, array $type): void
286
    {
287
        $this->context->stopVisiting($object);
288
        $this->context->popClassMetadata();
289
290
        foreach ($metadata->postSerializeMethods as $method) {
291
            $method->invoke($object);
292
        }
293
294
        if ($this->dispatcher->hasListeners('serializer.post_serialize', $metadata->name, $this->format)) {
295
            $this->dispatcher->dispatch('serializer.post_serialize', $metadata->name, $this->format, new ObjectEvent($this->context, $object, $type));
296
        }
297
    }
298
}
299