vendor/jms/serializer/src/GraphNavigator/SerializationGraphNavigator.php line 199

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