Issues (163)

src/GraphNavigator/SerializationGraphNavigator.php (1 issue)

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

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