Serializer.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * This file is part of Hyperf.
  5. *
  6. * @link https://www.hyperf.io
  7. * @document https://hyperf.wiki
  8. * @contact group@hyperf.io
  9. * @license https://github.com/hyperf/hyperf/blob/master/LICENSE
  10. */
  11. namespace Hyperf\Serializer;
  12. use Countable;
  13. use Hyperf\Contract\NormalizerInterface as Normalizer;
  14. use Symfony\Component\Serializer\Encoder\ChainDecoder;
  15. use Symfony\Component\Serializer\Encoder\ChainEncoder;
  16. use Symfony\Component\Serializer\Encoder\ContextAwareDecoderInterface;
  17. use Symfony\Component\Serializer\Encoder\ContextAwareEncoderInterface;
  18. use Symfony\Component\Serializer\Encoder\DecoderInterface;
  19. use Symfony\Component\Serializer\Encoder\EncoderInterface;
  20. use Symfony\Component\Serializer\Exception\InvalidArgumentException;
  21. use Symfony\Component\Serializer\Exception\LogicException;
  22. use Symfony\Component\Serializer\Exception\NotEncodableValueException;
  23. use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
  24. use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface;
  25. use Symfony\Component\Serializer\Normalizer\ContextAwareDenormalizerInterface;
  26. use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface;
  27. use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface;
  28. use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
  29. use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
  30. use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
  31. use Symfony\Component\Serializer\SerializerAwareInterface;
  32. use Symfony\Component\Serializer\SerializerInterface;
  33. use Traversable;
  34. use function get_class;
  35. use function gettype;
  36. use function is_array;
  37. use function is_object;
  38. use function is_resource;
  39. /**
  40. * Serializer serializes and deserializes data.
  41. *
  42. * objects are turned into arrays by normalizers.
  43. * arrays are turned into various output formats by encoders.
  44. *
  45. * $serializer->serialize($obj, 'xml')
  46. * $serializer->decode($data, 'xml')
  47. * $serializer->denormalize($data, 'Class', 'xml')
  48. */
  49. class Serializer implements Normalizer, SerializerInterface, ContextAwareNormalizerInterface, ContextAwareDenormalizerInterface, ContextAwareEncoderInterface, ContextAwareDecoderInterface
  50. {
  51. protected const SCALAR_TYPES = [
  52. 'int' => true,
  53. 'bool' => true,
  54. 'float' => true,
  55. 'string' => true,
  56. ];
  57. protected ChainEncoder $encoder;
  58. protected ChainDecoder $decoder;
  59. protected array $normalizers = [];
  60. protected array $denormalizerCache = [];
  61. protected array $normalizerCache = [];
  62. /**
  63. * @param (DenormalizerInterface|mixed|NormalizerInterface)[] $normalizers
  64. * @param (DecoderInterface|EncoderInterface|mixed)[] $encoders
  65. */
  66. public function __construct(array $normalizers = [], array $encoders = [])
  67. {
  68. foreach ($normalizers as $normalizer) {
  69. if ($normalizer instanceof SerializerAwareInterface) {
  70. $normalizer->setSerializer($this);
  71. }
  72. if ($normalizer instanceof DenormalizerAwareInterface) {
  73. $normalizer->setDenormalizer($this);
  74. }
  75. if ($normalizer instanceof NormalizerAwareInterface) {
  76. $normalizer->setNormalizer($this);
  77. }
  78. if (! ($normalizer instanceof NormalizerInterface || $normalizer instanceof DenormalizerInterface)) {
  79. throw new InvalidArgumentException(sprintf('The class "%s" neither implements "%s" nor "%s".', get_debug_type($normalizer), NormalizerInterface::class, DenormalizerInterface::class));
  80. }
  81. }
  82. $this->normalizers = $normalizers;
  83. $decoders = [];
  84. $realEncoders = [];
  85. foreach ($encoders as $encoder) {
  86. if ($encoder instanceof SerializerAwareInterface) {
  87. $encoder->setSerializer($this);
  88. }
  89. if ($encoder instanceof DecoderInterface) {
  90. $decoders[] = $encoder;
  91. }
  92. if ($encoder instanceof EncoderInterface) {
  93. $realEncoders[] = $encoder;
  94. }
  95. if (! ($encoder instanceof EncoderInterface || $encoder instanceof DecoderInterface)) {
  96. throw new InvalidArgumentException(sprintf('The class "%s" neither implements "%s" nor "%s".', get_debug_type($encoder), EncoderInterface::class, DecoderInterface::class));
  97. }
  98. }
  99. $this->encoder = new ChainEncoder($realEncoders);
  100. $this->decoder = new ChainDecoder($decoders);
  101. }
  102. final public function serialize($data, string $format, array $context = []): string
  103. {
  104. if (! $this->supportsEncoding($format, $context)) {
  105. throw new NotEncodableValueException(sprintf('Serialization for the format "%s" is not supported.', $format));
  106. }
  107. if ($this->encoder->needsNormalization($format, $context)) {
  108. $data = $this->normalize($data, $format, $context);
  109. }
  110. return $this->encode($data, $format, $context);
  111. }
  112. /**
  113. * @phpstan-ignore-next-line
  114. * @param mixed $data
  115. */
  116. final public function deserialize($data, string $type, string $format, array $context = []): mixed
  117. {
  118. if (! $this->supportsDecoding($format, $context)) {
  119. throw new NotEncodableValueException(sprintf('Deserialization for the format "%s" is not supported.', $format));
  120. }
  121. $data = $this->decode($data, $format, $context);
  122. return $this->denormalize($data, $type, $format, $context);
  123. }
  124. public function normalize($object, ?string $format = null, array $context = [])
  125. {
  126. // If a normalizer supports the given data, use it
  127. if ($normalizer = $this->getNormalizer($object, $format, $context)) {
  128. return $normalizer->normalize($object, $format, $context);
  129. }
  130. if ($object === null || is_scalar($object)) {
  131. return $object;
  132. }
  133. if (is_array($object) || $object instanceof Traversable) {
  134. if ($object instanceof Countable && $object->count() === 0) {
  135. return $object;
  136. }
  137. $normalized = [];
  138. foreach ($object as $key => $val) {
  139. $normalized[$key] = $this->normalize($val, $format, $context);
  140. }
  141. return $normalized;
  142. }
  143. if (is_object($object)) {
  144. if (! $this->normalizers) {
  145. throw new LogicException('You must register at least one normalizer to be able to normalize objects.');
  146. }
  147. throw new NotNormalizableValueException(sprintf('Could not normalize object of type "%s", no supporting normalizer found.', get_debug_type($object)));
  148. }
  149. throw new NotNormalizableValueException('An unexpected value could not be normalized: ' . (! is_resource($object) ? var_export($object, true) : sprintf('%s resource', get_resource_type($object))));
  150. }
  151. /**
  152. * @param mixed $data
  153. * @throws NotNormalizableValueException
  154. */
  155. public function denormalize($data, string $type, ?string $format = null, array $context = [])
  156. {
  157. if (isset(self::SCALAR_TYPES[$type])) {
  158. if (is_scalar($data)) {
  159. switch ($type) {
  160. case 'int':
  161. return (int) $data;
  162. case 'bool':
  163. return (bool) $data;
  164. case 'float':
  165. return (float) $data;
  166. case 'string':
  167. return (string) $data;
  168. }
  169. }
  170. }
  171. if (! $this->normalizers) {
  172. throw new LogicException('You must register at least one normalizer to be able to denormalize objects.');
  173. }
  174. if ($normalizer = $this->getDenormalizer($data, $type, $format, $context)) {
  175. return $normalizer->denormalize($data, $type, $format, $context);
  176. }
  177. throw new NotNormalizableValueException(sprintf('Could not denormalize object of type "%s", no supporting normalizer found.', $type));
  178. }
  179. public function supportsNormalization($data, ?string $format = null, array $context = []): bool
  180. {
  181. return $this->getNormalizer($data, $format, $context) !== null;
  182. }
  183. public function supportsDenormalization($data, string $type, ?string $format = null, array $context = []): bool
  184. {
  185. return isset(self::SCALAR_TYPES[$type]) || $this->getDenormalizer($data, $type, $format, $context) !== null;
  186. }
  187. final public function encode($data, string $format, array $context = []): string
  188. {
  189. return $this->encoder->encode($data, $format, $context);
  190. }
  191. final public function decode(string $data, string $format, array $context = [])
  192. {
  193. return $this->decoder->decode($data, $format, $context);
  194. }
  195. public function supportsEncoding(string $format, array $context = []): bool
  196. {
  197. return $this->encoder->supportsEncoding($format, $context);
  198. }
  199. public function supportsDecoding(string $format, array $context = []): bool
  200. {
  201. return $this->decoder->supportsDecoding($format, $context);
  202. }
  203. /**
  204. * Returns a matching normalizer.
  205. *
  206. * @param mixed $data Data to get the serializer for
  207. * @param string $format Format name, present to give the option to normalizers to act differently based on formats
  208. * @param array $context Options available to the normalizer
  209. */
  210. private function getNormalizer($data, ?string $format, array $context): ?NormalizerInterface
  211. {
  212. $type = is_object($data) ? get_class($data) : 'native-' . gettype($data);
  213. if (! isset($this->normalizerCache[$format][$type])) {
  214. $this->normalizerCache[$format][$type] = [];
  215. foreach ($this->normalizers as $k => $normalizer) {
  216. if (! $normalizer instanceof NormalizerInterface) {
  217. continue;
  218. }
  219. // TODO: Use getSupportedTypes to rewrite this since Symfony 7.
  220. if (! $normalizer instanceof CacheableSupportsMethodInterface || ! $normalizer->hasCacheableSupportsMethod()) {
  221. $this->normalizerCache[$format][$type][$k] = false;
  222. } elseif ($normalizer->supportsNormalization($data, $format)) {
  223. $this->normalizerCache[$format][$type][$k] = true;
  224. break;
  225. }
  226. }
  227. }
  228. foreach ($this->normalizerCache[$format][$type] as $k => $cached) {
  229. $normalizer = $this->normalizers[$k];
  230. if ($cached || $normalizer->supportsNormalization($data, $format, $context)) {
  231. return $normalizer;
  232. }
  233. }
  234. return null;
  235. }
  236. /**
  237. * Returns a matching denormalizer.
  238. *
  239. * @param mixed $data Data to restore
  240. * @param string $class The expected class to instantiate
  241. * @param string $format Format name, present to give the option to normalizers to act differently based on formats
  242. * @param array $context Options available to the denormalizer
  243. */
  244. private function getDenormalizer($data, string $class, ?string $format, array $context): ?DenormalizerInterface
  245. {
  246. if (! isset($this->denormalizerCache[$format][$class])) {
  247. $this->denormalizerCache[$format][$class] = [];
  248. foreach ($this->normalizers as $k => $normalizer) {
  249. if (! $normalizer instanceof DenormalizerInterface) {
  250. continue;
  251. }
  252. if (! $normalizer instanceof CacheableSupportsMethodInterface || ! $normalizer->hasCacheableSupportsMethod()) {
  253. $this->denormalizerCache[$format][$class][$k] = false;
  254. } elseif ($normalizer->supportsDenormalization(null, $class, $format)) {
  255. $this->denormalizerCache[$format][$class][$k] = true;
  256. break;
  257. }
  258. }
  259. }
  260. foreach ($this->denormalizerCache[$format][$class] as $k => $cached) {
  261. $normalizer = $this->normalizers[$k];
  262. if ($cached || $normalizer->supportsDenormalization($data, $class, $format, $context)) {
  263. return $normalizer;
  264. }
  265. }
  266. return null;
  267. }
  268. }