Passed
Push — master ( 4fe470...0edb56 )
by Asmir
27:13 queued 12:14
created

SerializationGraphNavigator   C

Complexity

Total Complexity 53

Size/Duplication

Total Lines 238
Duplicated Lines 0 %

Test Coverage

Coverage 97.78%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 104
dl 0
loc 238
ccs 88
cts 90
cp 0.9778
rs 6.96
c 2
b 0
f 0
wmc 53

5 Methods

Rating   Name   Duplication   Size   Complexity  
A initialize() 0 4 1
A __construct() 0 14 3
A isRootNullAllowed() 0 3 3
A afterVisitingObject() 0 11 3
F accept() 0 147 43

How to fix   Complexity   

Complex Class

Complex classes like SerializationGraphNavigator 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 SerializationGraphNavigator, 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\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\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\Functions;
22
use JMS\Serializer\GraphNavigator;
23
use JMS\Serializer\GraphNavigatorInterface;
24
use JMS\Serializer\Handler\HandlerRegistryInterface;
25
use JMS\Serializer\Metadata\ClassMetadata;
26
use JMS\Serializer\NullAwareVisitorInterface;
27
use JMS\Serializer\SerializationContext;
28
use JMS\Serializer\Visitor\SerializationVisitorInterface;
29
use JMS\Serializer\VisitorInterface;
30
use Metadata\MetadataFactoryInterface;
31
32
/**
33
 * Handles traversal along the object graph.
34
 *
35
 * This class handles traversal along the graph, and calls different methods
36
 * on visitors, or custom handlers to process its nodes.
37
 *
38
 * @author Johannes M. Schmitt <[email protected]>
39
 */
40
final class SerializationGraphNavigator extends GraphNavigator implements GraphNavigatorInterface
41
{
42
    /**
43
     * @var SerializationVisitorInterface
44
     */
45
    protected $visitor;
46
47
    /**
48
     * @var SerializationContext
49
     */
50
    protected $context;
51
52
    /**
53
     * @var ExpressionLanguageExclusionStrategy
54
     */
55
    private $expressionExclusionStrategy;
56
57
    /**
58
     * @var EventDispatcherInterface
59
     */
60
    private $dispatcher;
61
62
    /**
63
     * @var MetadataFactoryInterface
64
     */
65
    private $metadataFactory;
66
67
    /**
68 290
     * @var HandlerRegistryInterface
69
     */
70
    private $handlerRegistry;
71
    /**
72
     * @var AccessorStrategyInterface
73
     */
74
    private $accessor;
75 290
76 290
    /**
77 290
     * @var bool
78 290
     */
79
    private $shouldSerializeNull;
80 290
81 25
    public function __construct(
82
        MetadataFactoryInterface $metadataFactory,
83 290
        HandlerRegistryInterface $handlerRegistry,
84
        AccessorStrategyInterface $accessor,
85 290
        ?EventDispatcherInterface $dispatcher = null,
86
        ?ExpressionEvaluatorInterface $expressionEvaluator = null
87 290
    ) {
88 290
        $this->dispatcher = $dispatcher ?: new EventDispatcher();
89 290
        $this->metadataFactory = $metadataFactory;
90
        $this->handlerRegistry = $handlerRegistry;
91
        $this->accessor = $accessor;
92
93
        if ($expressionEvaluator) {
94
            $this->expressionExclusionStrategy = new ExpressionLanguageExclusionStrategy($expressionEvaluator);
95
        }
96
    }
97
98 289
    public function initialize(VisitorInterface $visitor, Context $context): void
99
    {
100
        parent::initialize($visitor, $context);
101
        $this->shouldSerializeNull = $context->shouldSerializeNull();
0 ignored issues
show
Bug introduced by
The method shouldSerializeNull() does not exist on JMS\Serializer\Context. It seems like you code against a sub-type of JMS\Serializer\Context such as JMS\Serializer\SerializationContext. ( Ignorable by Annotation )

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

101
        /** @scrutinizer ignore-call */ 
102
        $this->shouldSerializeNull = $context->shouldSerializeNull();
Loading history...
102 289
    }
103
104 267
    /**
105 267
     * Called for each node of the graph that is being traversed.
106 193
     *
107
     * @param mixed $data the data depends on the direction, and type of visitor
108
     * @param array|null $type array has the format ["name" => string, "params" => array]
109 267
     *
110
     * @return mixed the return value depends on the direction, and type of visitor
111
     */
112
    public function accept($data, ?array $type = null)
113 154
    {
114 5
        // If the type was not given, we infer the most specific type from the
115
        // input data in serialization mode.
116
        if (null === $type) {
117
            $typeName = \gettype($data);
118 289
            if ('object' === $typeName) {
119
                $typeName = \get_class($data);
120
            }
121
122 289
            $type = ['name' => $typeName, 'params' => []];
123 289
        } elseif (null === $data) {
124 38
            // If the data is null, we have to force the type to null regardless of the input in order to
125 15
            // guarantee correct handling of null values, and not have any internal auto-casting behavior.
126
            $type = ['name' => 'NULL', 'params' => []];
127 23
        }
128
        // Sometimes data can convey null but is not of a null type.
129 265
        // Visitors can have the power to add this custom null evaluation
130 163
        if ($this->visitor instanceof NullAwareVisitorInterface && true === $this->visitor->isNull($data)) {
131
            $type = ['name' => 'NULL', 'params' => []];
132 261
        }
133 261
134 48
        switch ($type['name']) {
135
            case 'NULL':
136 258
                if (!$this->shouldSerializeNull && !$this->isRootNullAllowed()) {
137 258
                    throw new NotAcceptableException();
138 13
                }
139
                return $this->visitor->visitNull($data, $type);
140 253
141 244
            case 'string':
142 20
                return $this->visitor->visitString((string) $data, $type);
143
144 244
            case 'int':
145 106
            case 'integer':
146
                return $this->visitor->visitInteger((int) $data, $type);
147 206
148 1
            case 'bool':
149 1
            case 'boolean':
150
                return $this->visitor->visitBoolean((bool) $data, $type);
151
152
            case 'double':
153 1
            case 'float':
154
                return $this->visitor->visitDouble((float) $data, $type);
155
156
            case 'iterable':
157 205
                return $this->visitor->visitArray(Functions::iterableToArray($data), $type);
158 205
159 4
            case 'array':
160
                return $this->visitor->visitArray((array) $data, $type);
161 205
162
            case 'resource':
163
                $msg = 'Resources are not supported in serialized data.';
164
                if (null !== $path = $this->context->getPath()) {
165
                    $msg .= ' Path: ' . $path;
166 205
                }
167 203
168 4
                throw new RuntimeException($msg);
169
170
            default:
171
                if (null !== $data) {
172
                    if ($this->context->isVisiting($data)) {
173
                        throw new CircularReferenceDetectedException();
174 205
                    }
175 204
                    $this->context->startVisiting($data);
176 204
                }
177
178
                // If we're serializing a polymorphic type, then we'll be interested in the
179
                // metadata for the actual type of the object, not the base class.
180
                if (class_exists($type['name'], false) || interface_exists($type['name'], false)) {
181
                    if (is_subclass_of($data, $type['name'], false)) {
182 205
                        $type = ['name' => \get_class($data), 'params' => $type['params'] ?? []];
183 49
                    }
184 49
                }
185
186 49
                // Trigger pre-serialization callbacks, and listeners if they exist.
187
                // Dispatch pre-serialization event before handling data to have ability change type in listener
188
                if ($this->dispatcher->hasListeners('serializer.pre_serialize', $type['name'], $this->format)) {
189
                    $this->dispatcher->dispatch('serializer.pre_serialize', $type['name'], $this->format, $event = new PreSerializeEvent($this->context, $data, $type));
190 181
                    $type = $event->getType();
191
                }
192 179
193 2
                // First, try whether a custom handler exists for the given type. This is done
194
                // before loading metadata because the type name might not be a class, but
195
                // could also simply be an artifical type.
196 177
                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...
197 10
                    try {
198
                        $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

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