1<?php
2
3declare(strict_types=1);
4
5namespace JMS\Serializer\GraphNavigator;
6
7use JMS\Serializer\Accessor\AccessorStrategyInterface;
8use JMS\Serializer\Construction\ObjectConstructorInterface;
9use JMS\Serializer\DeserializationContext;
10use JMS\Serializer\EventDispatcher\EventDispatcher;
11use JMS\Serializer\EventDispatcher\EventDispatcherInterface;
12use JMS\Serializer\EventDispatcher\ObjectEvent;
13use JMS\Serializer\EventDispatcher\PreDeserializeEvent;
14use JMS\Serializer\Exception\ExpressionLanguageRequiredException;
15use JMS\Serializer\Exception\LogicException;
16use JMS\Serializer\Exception\NotAcceptableException;
17use JMS\Serializer\Exception\RuntimeException;
18use JMS\Serializer\Exclusion\ExpressionLanguageExclusionStrategy;
19use JMS\Serializer\Expression\ExpressionEvaluatorInterface;
20use JMS\Serializer\GraphNavigator;
21use JMS\Serializer\GraphNavigatorInterface;
22use JMS\Serializer\Handler\HandlerRegistryInterface;
23use JMS\Serializer\Metadata\ClassMetadata;
24use JMS\Serializer\NullAwareVisitorInterface;
25use JMS\Serializer\Visitor\DeserializationVisitorInterface;
26use Metadata\MetadataFactoryInterface;
27
28/**
29 * Handles traversal along the object graph.
30 *
31 * This class handles traversal along the graph, and calls different methods
32 * on visitors, or custom handlers to process its nodes.
33 *
34 * @author Johannes M. Schmitt <schmittjoh@gmail.com>
35 */
36final class DeserializationGraphNavigator extends GraphNavigator implements GraphNavigatorInterface
37{
38    /**
39     * @var DeserializationVisitorInterface
40     */
41    protected $visitor;
42
43    /**
44     * @var DeserializationContext
45     */
46    protected $context;
47
48    /**
49     * @var ExpressionLanguageExclusionStrategy
50     */
51    private $expressionExclusionStrategy;
52
53    /**
54     * @var EventDispatcherInterface
55     */
56    private $dispatcher;
57
58    /**
59     * @var MetadataFactoryInterface
60     */
61    private $metadataFactory;
62
63    /**
64     * @var HandlerRegistryInterface
65     */
66    private $handlerRegistry;
67
68    /**
69     * @var ObjectConstructorInterface
70     */
71    private $objectConstructor;
72    /**
73     * @var AccessorStrategyInterface
74     */
75    private $accessor;
76
77    public function __construct(
78        MetadataFactoryInterface $metadataFactory,
79        HandlerRegistryInterface $handlerRegistry,
80        ObjectConstructorInterface $objectConstructor,
81        AccessorStrategyInterface $accessor,
82        ?EventDispatcherInterface $dispatcher = null,
83        ?ExpressionEvaluatorInterface $expressionEvaluator = null
84    ) {
85        $this->dispatcher = $dispatcher ?: new EventDispatcher();
86        $this->metadataFactory = $metadataFactory;
87        $this->handlerRegistry = $handlerRegistry;
88        $this->objectConstructor = $objectConstructor;
89        $this->accessor = $accessor;
90        if ($expressionEvaluator) {
91            $this->expressionExclusionStrategy = new ExpressionLanguageExclusionStrategy($expressionEvaluator);
92        }
93    }
94
95    /**
96     * Called for each node of the graph that is being traversed.
97     *
98     * @param mixed $data the data depends on the direction, and type of visitor
99     * @param array|null $type array has the format ["name" => string, "params" => array]
100     *
101     * @return mixed the return value depends on the direction, and type of visitor
102     */
103    public function accept($data, ?array $type = null)
104    {
105        // If the type was not given, we infer the most specific type from the
106        // input data in serialization mode.
107        if (null === $type) {
108            throw new RuntimeException('The type must be given for all properties when deserializing.');
109        }
110        // Sometimes data can convey null but is not of a null type.
111        // Visitors can have the power to add this custom null evaluation
112        if ($this->visitor instanceof NullAwareVisitorInterface && true === $this->visitor->isNull($data)) {
113            $type = ['name' => 'NULL', 'params' => []];
114        }
115
116        switch ($type['name']) {
117            case 'NULL':
118                return $this->visitor->visitNull($data, $type);
119
120            case 'string':
121                return $this->visitor->visitString($data, $type);
122
123            case 'int':
124            case 'integer':
125                return $this->visitor->visitInteger($data, $type);
126
127            case 'bool':
128            case 'boolean':
129                return $this->visitor->visitBoolean($data, $type);
130
131            case 'double':
132            case 'float':
133                return $this->visitor->visitDouble($data, $type);
134
135            case 'array':
136                return $this->visitor->visitArray($data, $type);
137
138            case 'resource':
139                throw new RuntimeException('Resources are not supported in serialized data.');
140
141            default:
142                $this->context->increaseDepth();
143
144                // Trigger pre-serialization callbacks, and listeners if they exist.
145                // Dispatch pre-serialization event before handling data to have ability change type in listener
146                if ($this->dispatcher->hasListeners('serializer.pre_deserialize', $type['name'], $this->format)) {
147                    $this->dispatcher->dispatch('serializer.pre_deserialize', $type['name'], $this->format, $event = new PreDeserializeEvent($this->context, $data, $type));
148                    $type = $event->getType();
149                    $data = $event->getData();
150                }
151
152                // First, try whether a custom handler exists for the given type. This is done
153                // before loading metadata because the type name might not be a class, but
154                // could also simply be an artifical type.
155                if (null !== $handler = $this->handlerRegistry->getHandler(GraphNavigatorInterface::DIRECTION_DESERIALIZATION, $type['name'], $this->format)) {
156                    $rs = \call_user_func($handler, $this->visitor, $data, $type, $this->context);
157                    $this->context->decreaseDepth();
158
159                    return $rs;
160                }
161
162                /** @var ClassMetadata $metadata */
163                $metadata = $this->metadataFactory->getMetadataForClass($type['name']);
164
165                if ($metadata->usingExpression && !$this->expressionExclusionStrategy) {
166                    throw new ExpressionLanguageRequiredException(sprintf('To use conditional exclude/expose in %s you must configure the expression language.', $metadata->name));
167                }
168
169                if (!empty($metadata->discriminatorMap) && $type['name'] === $metadata->discriminatorBaseClass) {
170                    $metadata = $this->resolveMetadata($data, $metadata);
171                }
172
173                if (null !== $this->exclusionStrategy && $this->exclusionStrategy->shouldSkipClass($metadata, $this->context)) {
174                    $this->context->decreaseDepth();
175
176                    return null;
177                }
178
179                $this->context->pushClassMetadata($metadata);
180
181                $object = $this->objectConstructor->construct($this->visitor, $metadata, $data, $type, $this->context);
182
183                $this->visitor->startVisitingObject($metadata, $object, $type);
184                foreach ($metadata->propertyMetadata as $propertyMetadata) {
185                    if (null !== $this->exclusionStrategy && $this->exclusionStrategy->shouldSkipProperty($propertyMetadata, $this->context)) {
186                        continue;
187                    }
188
189                    if (null !== $this->expressionExclusionStrategy && $this->expressionExclusionStrategy->shouldSkipProperty($propertyMetadata, $this->context)) {
190                        continue;
191                    }
192
193                    if ($propertyMetadata->readOnly) {
194                        continue;
195                    }
196
197                    $this->context->pushPropertyMetadata($propertyMetadata);
198                    try {
199                        $v = $this->visitor->visitProperty($propertyMetadata, $data);
200                        $this->accessor->setValue($object, $v, $propertyMetadata, $this->context);
201                    } catch (NotAcceptableException $e) {
202                    }
203                    $this->context->popPropertyMetadata();
204                }
205
206                $rs = $this->visitor->endVisitingObject($metadata, $data, $type);
207                $this->afterVisitingObject($metadata, $rs, $type);
208
209                return $rs;
210        }
211    }
212
213    /**
214     * @param mixed $data
215     */
216    private function resolveMetadata($data, ClassMetadata $metadata): ?ClassMetadata
217    {
218        $typeValue = $this->visitor->visitDiscriminatorMapProperty($data, $metadata);
219
220        if (!isset($metadata->discriminatorMap[$typeValue])) {
221            throw new LogicException(sprintf(
222                'The type value "%s" does not exist in the discriminator map of class "%s". Available types: %s',
223                $typeValue,
224                $metadata->name,
225                implode(', ', array_keys($metadata->discriminatorMap))
226            ));
227        }
228
229        return $this->metadataFactory->getMetadataForClass($metadata->discriminatorMap[$typeValue]);
230    }
231
232    private function afterVisitingObject(ClassMetadata $metadata, object $object, array $type): void
233    {
234        $this->context->decreaseDepth();
235        $this->context->popClassMetadata();
236
237        foreach ($metadata->postDeserializeMethods as $method) {
238            $method->invoke($object);
239        }
240
241        if ($this->dispatcher->hasListeners('serializer.post_deserialize', $metadata->name, $this->format)) {
242            $this->dispatcher->dispatch('serializer.post_deserialize', $metadata->name, $this->format, new ObjectEvent($this->context, $object, $type));
243        }
244    }
245}
246