SerializationGraphNavigator::initialize()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 2
dl 0
loc 7
ccs 3
cts 3
cp 1
crap 1
rs 10
c 0
b 0
f 0
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\Exception\UninitializedPropertyException;
21
use JMS\Serializer\Exclusion\ExpressionLanguageExclusionStrategy;
22
use JMS\Serializer\Expression\ExpressionEvaluatorInterface;
23
use JMS\Serializer\Functions;
24
use JMS\Serializer\GraphNavigator;
25
use JMS\Serializer\GraphNavigatorInterface;
26
use JMS\Serializer\Handler\HandlerRegistryInterface;
27
use JMS\Serializer\Metadata\ClassMetadata;
28
use JMS\Serializer\NullAwareVisitorInterface;
29
use JMS\Serializer\SerializationContext;
30
use JMS\Serializer\Type\Type;
31
use JMS\Serializer\Visitor\SerializationVisitorInterface;
32
use JMS\Serializer\VisitorInterface;
33
use Metadata\MetadataFactoryInterface;
34
35
use function assert;
36
37
/**
38
 * Handles traversal along the object graph.
39
 *
40
 * This class handles traversal along the graph, and calls different methods
41
 * on visitors, or custom handlers to process its nodes.
42
 *
43
 * @author Johannes M. Schmitt <[email protected]>
44
 *
45
 * @phpstan-import-type TypeArray from Type
46
 */
47
final class SerializationGraphNavigator extends GraphNavigator
48
{
49
    /**
50
     * @var SerializationVisitorInterface
51
     */
52
    protected $visitor;
53
54
    /**
55
     * @var SerializationContext
56
     */
57
    protected $context;
58
59
    /**
60
     * @var ExpressionLanguageExclusionStrategy
61
     */
62
    private $expressionExclusionStrategy;
63
64
    /**
65
     * @var EventDispatcherInterface
66
     */
67
    private $dispatcher;
68 290
69
    /**
70
     * @var MetadataFactoryInterface
71
     */
72
    private $metadataFactory;
73
74
    /**
75 290
     * @var HandlerRegistryInterface
76 290
     */
77 290
    private $handlerRegistry;
78 290
    /**
79
     * @var AccessorStrategyInterface
80 290
     */
81 25
    private $accessor;
82
83 290
    /**
84
     * @var bool
85 290
     */
86
    private $shouldSerializeNull;
87 290
88 290
    public function __construct(
89 290
        MetadataFactoryInterface $metadataFactory,
90
        HandlerRegistryInterface $handlerRegistry,
91
        AccessorStrategyInterface $accessor,
92
        ?EventDispatcherInterface $dispatcher = null,
93
        ?ExpressionEvaluatorInterface $expressionEvaluator = null
94
    ) {
95
        $this->dispatcher = $dispatcher ?: new EventDispatcher();
96
        $this->metadataFactory = $metadataFactory;
97
        $this->handlerRegistry = $handlerRegistry;
98 289
        $this->accessor = $accessor;
99
100
        if ($expressionEvaluator) {
101
            $this->expressionExclusionStrategy = new ExpressionLanguageExclusionStrategy($expressionEvaluator);
102 289
        }
103
    }
104 267
105 267
    public function initialize(VisitorInterface $visitor, Context $context): void
106 193
    {
107
        assert($context instanceof SerializationContext);
108
109 267
        parent::initialize($visitor, $context);
110
111
        $this->shouldSerializeNull = $context->shouldSerializeNull();
112
    }
113 154
114 5
    /**
115
     * Called for each node of the graph that is being traversed.
116
     *
117
     * @param mixed $data the data depends on the direction, and type of visitor
118 289
     * @param TypeArray|null $type array has the format ["name" => string, "params" => array]
0 ignored issues
show
Bug introduced by
The type JMS\Serializer\GraphNavigator\TypeArray was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
119
     *
120
     * @return mixed the return value depends on the direction, and type of visitor
121
     */
122 289
    public function accept($data, ?array $type = null)
123 289
    {
124 38
        // If the type was not given, we infer the most specific type from the
125 15
        // input data in serialization mode.
126
        if (null === $type) {
127 23
            $typeName = \gettype($data);
128
            if ('object' === $typeName) {
129 265
                $typeName = \get_class($data);
130 163
            }
131
132 261
            $type = ['name' => $typeName, 'params' => []];
133 261
        } elseif (null === $data) {
134 48
            // If the data is null, we have to force the type to null regardless of the input in order to
135
            // guarantee correct handling of null values, and not have any internal auto-casting behavior.
136 258
            $type = ['name' => 'NULL', 'params' => []];
137 258
        }
138 13
139
        // Sometimes data can convey null but is not of a null type.
140 253
        // Visitors can have the power to add this custom null evaluation
141 244
        if ($this->visitor instanceof NullAwareVisitorInterface && true === $this->visitor->isNull($data)) {
142 20
            $type = ['name' => 'NULL', 'params' => []];
143
        }
144 244
145 106
        switch ($type['name']) {
146
            case 'NULL':
147 206
                if (!$this->shouldSerializeNull && !$this->isRootNullAllowed()) {
148 1
                    throw new NotAcceptableException();
149 1
                }
150
151
                return $this->visitor->visitNull($data, $type);
152
153 1
            case 'string':
154
                return $this->visitor->visitString((string) $data, $type);
155
156
            case 'int':
157 205
            case 'integer':
158 205
                return $this->visitor->visitInteger((int) $data, $type);
159 4
160
            case 'bool':
161 205
            case 'boolean':
162
            case 'true':
163
            case 'false':
164
                return $this->visitor->visitBoolean((bool) $data, $type);
165
166 205
            case 'double':
167 203
            case 'float':
168 4
                return $this->visitor->visitDouble((float) $data, $type);
169
170
            case 'iterable':
171
                return $this->visitor->visitArray(Functions::iterableToArray($data), $type);
172
173
            case 'array':
174 205
            case 'list':
175 204
                return $this->visitor->visitArray((array) $data, $type);
176 204
177
            case 'resource':
178
                $msg = 'Resources are not supported in serialized data.';
179
                if (null !== $path = $this->context->getPath()) {
180
                    $msg .= ' Path: ' . $path;
181
                }
182 205
183 49
                throw new RuntimeException($msg);
184 49
185
            case 'union':
186 49
                if (null !== $handler = $this->handlerRegistry->getHandler(GraphNavigatorInterface::DIRECTION_SERIALIZATION, $type['name'], $this->format)) {
187
                    try {
188
                        return \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

188
                        return \call_user_func(/** @scrutinizer ignore-type */ $handler, $this->visitor, $data, $type, $this->context);
Loading history...
189
                    } catch (SkipHandlerException $e) {
190 181
                        // Skip handler, fallback to default behavior
191
                    }
192 179
                }
193 2
194
                break;
195
            default:
196 177
                if (null !== $data) {
197 10
                    if ($this->context->isVisiting($data)) {
198
                        throw new CircularReferenceDetectedException();
199 10
                    }
200
201
                    $this->context->startVisiting($data);
202 173
                }
203
204 173
                // If we're serializing a polymorphic type, then we'll be interested in the
205 2
                // metadata for the actual type of the object, not the base class.
206
                if (class_exists($type['name'], false) || interface_exists($type['name'], false)) {
207
                    if (is_subclass_of($data, $type['name'], false) && null === $this->handlerRegistry->getHandler(GraphNavigatorInterface::DIRECTION_SERIALIZATION, $type['name'], $this->format)) {
208 173
                        $type = ['name' => \get_class($data), 'params' => $type['params'] ?? []];
209 173
                    }
210 172
                }
211 16
212
                // Trigger pre-serialization callbacks, and listeners if they exist.
213
                // Dispatch pre-serialization event before handling data to have ability change type in listener
214 172
                if ($this->dispatcher->hasListeners('serializer.pre_serialize', $type['name'], $this->format)) {
215 16
                    $this->dispatcher->dispatch('serializer.pre_serialize', $type['name'], $this->format, $event = new PreSerializeEvent($this->context, $data, $type));
216
                    $type = $event->getType();
217
                }
218 172
219
                // First, try whether a custom handler exists for the given type. This is done
220 170
                // before loading metadata because the type name might not be a class, but
221 24
                // could also simply be an artifical type.
222
                if (null !== $handler = $this->handlerRegistry->getHandler(GraphNavigatorInterface::DIRECTION_SERIALIZATION, $type['name'], $this->format)) {
223
                    try {
224 168
                        $rs = \call_user_func($handler, $this->visitor, $data, $type, $this->context);
225 168
                        $this->context->stopVisiting($data);
226 167
227
                        return $rs;
228
                    } catch (SkipHandlerException $e) {
229 169
                        // Skip handler, fallback to default behavior
230
                    } catch (NotAcceptableException $e) {
231 169
                        $this->context->stopVisiting($data);
232
233
                        throw $e;
234
                    }
235 169
                }
236
237 169
                $metadata = $this->metadataFactory->getMetadataForClass($type['name']);
238 169
                \assert($metadata instanceof ClassMetadata);
239
240 169
                if ($metadata->usingExpression && null === $this->expressionExclusionStrategy) {
241 2
                    throw new ExpressionLanguageRequiredException(sprintf('To use conditional exclude/expose in %s you must configure the expression language.', $metadata->name));
242
                }
243
244 169
                if (null !== $this->exclusionStrategy && $this->exclusionStrategy->shouldSkipClass($metadata, $this->context)) {
245 2
                    $this->context->stopVisiting($data);
246
247 169
                    throw new ExcludedClassException();
248
                }
249
250
                if (null !== $this->expressionExclusionStrategy && $this->expressionExclusionStrategy->shouldSkipClass($metadata, $this->context)) {
251
                    $this->context->stopVisiting($data);
252
253
                    throw new ExcludedClassException();
254
                }
255
256
                if (!is_object($data)) {
257
                    throw new InvalidArgumentException('Value at ' . $this->context->getPath() . ' is expected to be an object of class ' . $type['name'] . ' but is of type ' . gettype($data));
258
                }
259
260
                $this->context->pushClassMetadata($metadata);
261
262
                foreach ($metadata->preSerializeMethods as $method) {
263
                    $method->invoke($data);
264
                }
265
266
                $this->visitor->startVisitingObject($metadata, $data, $type);
267
                foreach ($metadata->propertyMetadata as $propertyMetadata) {
268
                    if (null !== $this->exclusionStrategy && $this->exclusionStrategy->shouldSkipProperty($propertyMetadata, $this->context)) {
269
                        continue;
270
                    }
271
272
                    if (null !== $this->expressionExclusionStrategy && $this->expressionExclusionStrategy->shouldSkipProperty($propertyMetadata, $this->context)) {
273
                        continue;
274
                    }
275
276
                    try {
277
                        $v = $this->accessor->getValue($data, $propertyMetadata, $this->context);
278
                    } catch (UninitializedPropertyException $e) {
279
                        continue;
280
                    }
281
282
                    if (null === $v && true !== $this->shouldSerializeNull) {
283
                        continue;
284
                    }
285
286
                    $this->context->pushPropertyMetadata($propertyMetadata);
287
                    $this->visitor->visitProperty($propertyMetadata, $v);
288
                    $this->context->popPropertyMetadata();
289
                }
290
291
                $this->afterVisitingObject($metadata, $data, $type);
292
293
                return $this->visitor->endVisitingObject($metadata, $data, $type);
294
        }
295
    }
296
297
    private function isRootNullAllowed(): bool
298
    {
299
        return $this->context->hasAttribute('allows_root_null') && $this->context->getAttribute('allows_root_null') && 0 === $this->context->getVisitingSet()->count();
300
    }
301
302
    /**
303
     * @param TypeArray $type
304
     */
305
    private function afterVisitingObject(ClassMetadata $metadata, object $object, array $type): void
306
    {
307
        $this->context->stopVisiting($object);
308
        $this->context->popClassMetadata();
309
310
        foreach ($metadata->postSerializeMethods as $method) {
311
            $method->invoke($object);
312
        }
313
314
        if ($this->dispatcher->hasListeners('serializer.post_serialize', $metadata->name, $this->format)) {
315
            $this->dispatcher->dispatch('serializer.post_serialize', $metadata->name, $this->format, new ObjectEvent($this->context, $object, $type));
316
        }
317
    }
318
}
319