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
Bug
introduced
by
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 |