Serializer.php 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\Messenger\Transport\Serialization;
  11. use Symfony\Component\Messenger\Envelope;
  12. use Symfony\Component\Messenger\Exception\LogicException;
  13. use Symfony\Component\Messenger\Exception\MessageDecodingFailedException;
  14. use Symfony\Component\Messenger\Stamp\NonSendableStampInterface;
  15. use Symfony\Component\Messenger\Stamp\SerializerStamp;
  16. use Symfony\Component\Messenger\Stamp\StampInterface;
  17. use Symfony\Component\Serializer\Encoder\JsonEncoder;
  18. use Symfony\Component\Serializer\Encoder\XmlEncoder;
  19. use Symfony\Component\Serializer\Exception\ExceptionInterface;
  20. use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
  21. use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
  22. use Symfony\Component\Serializer\Serializer as SymfonySerializer;
  23. use Symfony\Component\Serializer\SerializerInterface as SymfonySerializerInterface;
  24. /**
  25. * @author Samuel Roze <samuel.roze@gmail.com>
  26. */
  27. class Serializer implements SerializerInterface
  28. {
  29. public const MESSENGER_SERIALIZATION_CONTEXT = 'messenger_serialization';
  30. private const STAMP_HEADER_PREFIX = 'X-Message-Stamp-';
  31. private $serializer;
  32. private $format;
  33. private $context;
  34. public function __construct(SymfonySerializerInterface $serializer = null, string $format = 'json', array $context = [])
  35. {
  36. $this->serializer = $serializer ?? self::create()->serializer;
  37. $this->format = $format;
  38. $this->context = $context + [self::MESSENGER_SERIALIZATION_CONTEXT => true];
  39. }
  40. public static function create(): self
  41. {
  42. if (!class_exists(SymfonySerializer::class)) {
  43. throw new LogicException(sprintf('The "%s" class requires Symfony\'s Serializer component. Try running "composer require symfony/serializer" or use "%s" instead.', __CLASS__, PhpSerializer::class));
  44. }
  45. $encoders = [new XmlEncoder(), new JsonEncoder()];
  46. $normalizers = [new ArrayDenormalizer(), new ObjectNormalizer()];
  47. $serializer = new SymfonySerializer($normalizers, $encoders);
  48. return new self($serializer);
  49. }
  50. /**
  51. * {@inheritdoc}
  52. */
  53. public function decode(array $encodedEnvelope): Envelope
  54. {
  55. if (empty($encodedEnvelope['body']) || empty($encodedEnvelope['headers'])) {
  56. throw new MessageDecodingFailedException('Encoded envelope should have at least a "body" and some "headers".');
  57. }
  58. if (empty($encodedEnvelope['headers']['type'])) {
  59. throw new MessageDecodingFailedException('Encoded envelope does not have a "type" header.');
  60. }
  61. $stamps = $this->decodeStamps($encodedEnvelope);
  62. $serializerStamp = $this->findFirstSerializerStamp($stamps);
  63. $context = $this->context;
  64. if (null !== $serializerStamp) {
  65. $context = $serializerStamp->getContext() + $context;
  66. }
  67. try {
  68. $message = $this->serializer->deserialize($encodedEnvelope['body'], $encodedEnvelope['headers']['type'], $this->format, $context);
  69. } catch (ExceptionInterface $e) {
  70. throw new MessageDecodingFailedException('Could not decode message: '.$e->getMessage(), $e->getCode(), $e);
  71. }
  72. return new Envelope($message, $stamps);
  73. }
  74. /**
  75. * {@inheritdoc}
  76. */
  77. public function encode(Envelope $envelope): array
  78. {
  79. $context = $this->context;
  80. /** @var SerializerStamp|null $serializerStamp */
  81. if ($serializerStamp = $envelope->last(SerializerStamp::class)) {
  82. $context = $serializerStamp->getContext() + $context;
  83. }
  84. $envelope = $envelope->withoutStampsOfType(NonSendableStampInterface::class);
  85. $headers = ['type' => \get_class($envelope->getMessage())] + $this->encodeStamps($envelope) + $this->getContentTypeHeader();
  86. return [
  87. 'body' => $this->serializer->serialize($envelope->getMessage(), $this->format, $context),
  88. 'headers' => $headers,
  89. ];
  90. }
  91. private function decodeStamps(array $encodedEnvelope): array
  92. {
  93. $stamps = [];
  94. foreach ($encodedEnvelope['headers'] as $name => $value) {
  95. if (0 !== strpos($name, self::STAMP_HEADER_PREFIX)) {
  96. continue;
  97. }
  98. try {
  99. $stamps[] = $this->serializer->deserialize($value, substr($name, \strlen(self::STAMP_HEADER_PREFIX)).'[]', $this->format, $this->context);
  100. } catch (ExceptionInterface $e) {
  101. throw new MessageDecodingFailedException('Could not decode stamp: '.$e->getMessage(), $e->getCode(), $e);
  102. }
  103. }
  104. if ($stamps) {
  105. $stamps = array_merge(...$stamps);
  106. }
  107. return $stamps;
  108. }
  109. private function encodeStamps(Envelope $envelope): array
  110. {
  111. if (!$allStamps = $envelope->all()) {
  112. return [];
  113. }
  114. $headers = [];
  115. foreach ($allStamps as $class => $stamps) {
  116. $headers[self::STAMP_HEADER_PREFIX.$class] = $this->serializer->serialize($stamps, $this->format, $this->context);
  117. }
  118. return $headers;
  119. }
  120. /**
  121. * @param StampInterface[] $stamps
  122. */
  123. private function findFirstSerializerStamp(array $stamps): ?SerializerStamp
  124. {
  125. foreach ($stamps as $stamp) {
  126. if ($stamp instanceof SerializerStamp) {
  127. return $stamp;
  128. }
  129. }
  130. return null;
  131. }
  132. private function getContentTypeHeader(): array
  133. {
  134. $mimeType = $this->getMimeTypeForFormat();
  135. return null === $mimeType ? [] : ['Content-Type' => $mimeType];
  136. }
  137. private function getMimeTypeForFormat(): ?string
  138. {
  139. switch ($this->format) {
  140. case 'json':
  141. return 'application/json';
  142. case 'xml':
  143. return 'application/xml';
  144. case 'yml':
  145. case 'yaml':
  146. return 'application/x-yaml';
  147. case 'csv':
  148. return 'text/csv';
  149. }
  150. return null;
  151. }
  152. }