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