From af0fb00f5a1b6c86fca950813d9047bc12fa730d Mon Sep 17 00:00:00 2001 From: Viktor Babanov Date: Sun, 24 Nov 2024 20:18:21 +0500 Subject: [PATCH] Allow to restore original pushed message class on consume (#214) * Allow to restore original pushed message class on consume * Apply fixes from StyleCI * Apply Rector changes (CI) * Move message class name to metadata and fix tests * Apply fixes from StyleCI * Make JsonMessageSerializer::unserialize() faster when unserializable message class is the default one * Unify the EnvelopeTrait::fromData() method body --------- Co-authored-by: StyleCI Bot Co-authored-by: viktorprogger --- src/Message/EnvelopeTrait.php | 10 +++++ src/Message/JsonMessageSerializer.php | 42 +++++++++++++------ src/Message/Message.php | 5 +++ src/Message/MessageInterface.php | 2 + .../Message/JsonMessageSerializerTest.php | 40 ++++++++++++++---- tests/Unit/Support/TestMessage.php | 30 +++++++++++++ 6 files changed, 109 insertions(+), 20 deletions(-) create mode 100644 tests/Unit/Support/TestMessage.php diff --git a/src/Message/EnvelopeTrait.php b/src/Message/EnvelopeTrait.php index cc52cf15..e8d19d65 100644 --- a/src/Message/EnvelopeTrait.php +++ b/src/Message/EnvelopeTrait.php @@ -8,6 +8,16 @@ trait EnvelopeTrait { private MessageInterface $message; + /** + * A mirror of {@see MessageInterface::fromData()} + */ + abstract public static function fromMessage(MessageInterface $message): self; + + public static function fromData(string $handlerName, mixed $data, array $metadata = []): MessageInterface + { + return self::fromMessage(Message::fromData($handlerName, $data, $metadata)); + } + public function getMessage(): MessageInterface { return $this->message; diff --git a/src/Message/JsonMessageSerializer.php b/src/Message/JsonMessageSerializer.php index 81a6220c..d9b3a92e 100644 --- a/src/Message/JsonMessageSerializer.php +++ b/src/Message/JsonMessageSerializer.php @@ -19,6 +19,11 @@ public function serialize(MessageInterface $message): string 'data' => $message->getData(), 'meta' => $message->getMetadata(), ]; + if (!isset($payload['meta']['message-class'])) { + $payload['meta']['message-class'] = $message instanceof EnvelopeInterface + ? $message->getMessage()::class + : $message::class; + } return json_encode($payload, JSON_THROW_ON_ERROR); } @@ -34,25 +39,38 @@ public function unserialize(string $value): MessageInterface throw new InvalidArgumentException('Payload must be array. Got ' . get_debug_type($payload) . '.'); } + $name = $payload['name'] ?? null; + if (!isset($name) || !is_string($name)) { + throw new InvalidArgumentException('Handler name must be a string. Got ' . get_debug_type($name) . '.'); + } + $meta = $payload['meta'] ?? []; if (!is_array($meta)) { - throw new InvalidArgumentException('Metadata must be array. Got ' . get_debug_type($meta) . '.'); + throw new InvalidArgumentException('Metadata must be an array. Got ' . get_debug_type($meta) . '.'); } - // TODO: will be removed later - $message = new Message($payload['name'] ?? '$name', $payload['data'] ?? null, $meta); - + $envelopes = []; if (isset($meta[EnvelopeInterface::ENVELOPE_STACK_KEY]) && is_array($meta[EnvelopeInterface::ENVELOPE_STACK_KEY])) { - $message = $message->withMetadata( - array_merge($message->getMetadata(), [EnvelopeInterface::ENVELOPE_STACK_KEY => []]), - ); - foreach ($meta[EnvelopeInterface::ENVELOPE_STACK_KEY] as $envelope) { - if (is_string($envelope) && class_exists($envelope) && is_subclass_of($envelope, EnvelopeInterface::class)) { - $message = $envelope::fromMessage($message); - } - } + $envelopes = $meta[EnvelopeInterface::ENVELOPE_STACK_KEY]; } + $meta[EnvelopeInterface::ENVELOPE_STACK_KEY] = []; + $class = $payload['meta']['message-class'] ?? Message::class; + // Don't check subclasses when it's a default class: that's faster + if ($class !== Message::class && !is_subclass_of($class, MessageInterface::class)) { + $class = Message::class; + } + + /** + * @var class-string $class + */ + $message = $class::fromData($name, $payload['data'] ?? null, $meta); + + foreach ($envelopes as $envelope) { + if (is_string($envelope) && class_exists($envelope) && is_subclass_of($envelope, EnvelopeInterface::class)) { + $message = $envelope::fromMessage($message); + } + } return $message; } diff --git a/src/Message/Message.php b/src/Message/Message.php index a414ffb0..ab85d069 100644 --- a/src/Message/Message.php +++ b/src/Message/Message.php @@ -18,6 +18,11 @@ public function __construct( ) { } + public static function fromData(string $handlerName, mixed $data, array $metadata = []): MessageInterface + { + return new self($handlerName, $data, $metadata); + } + public function getHandlerName(): string { return $this->handlerName; diff --git a/src/Message/MessageInterface.php b/src/Message/MessageInterface.php index c65f32fd..ea3b1882 100644 --- a/src/Message/MessageInterface.php +++ b/src/Message/MessageInterface.php @@ -6,6 +6,8 @@ interface MessageInterface { + public static function fromData(string $handlerName, mixed $data, array $metadata = []): self; + /** * Returns handler name. * diff --git a/tests/Unit/Message/JsonMessageSerializerTest.php b/tests/Unit/Message/JsonMessageSerializerTest.php index 776a9835..18bc5ed9 100644 --- a/tests/Unit/Message/JsonMessageSerializerTest.php +++ b/tests/Unit/Message/JsonMessageSerializerTest.php @@ -11,6 +11,7 @@ use Yiisoft\Queue\Message\JsonMessageSerializer; use Yiisoft\Queue\Message\Message; use Yiisoft\Queue\Message\MessageInterface; +use Yiisoft\Queue\Tests\Unit\Support\TestMessage; /** * Testing message serialization options @@ -42,10 +43,10 @@ public static function dataUnsupportedPayloadFormat(): iterable */ public function testMetadataFormat(mixed $meta): void { - $payload = ['data' => 'test', 'meta' => $meta]; + $payload = ['name' => 'handler', 'data' => 'test', 'meta' => $meta]; $serializer = $this->createSerializer(); - $this->expectExceptionMessage(sprintf('Metadata must be array. Got %s.', get_debug_type($meta))); + $this->expectExceptionMessage(sprintf('Metadata must be an array. Got %s.', get_debug_type($meta))); $this->expectException(InvalidArgumentException::class); $serializer->unserialize(json_encode($payload)); } @@ -59,31 +60,32 @@ public static function dataUnsupportedMetadataFormat(): iterable public function testUnserializeFromData(): void { - $payload = ['data' => 'test']; + $payload = ['name' => 'handler', 'data' => 'test']; $serializer = $this->createSerializer(); $message = $serializer->unserialize(json_encode($payload)); $this->assertInstanceOf(MessageInterface::class, $message); $this->assertEquals($payload['data'], $message->getData()); - $this->assertEquals([], $message->getMetadata()); + $this->assertEquals([EnvelopeInterface::ENVELOPE_STACK_KEY => []], $message->getMetadata()); } public function testUnserializeWithMetadata(): void { - $payload = ['data' => 'test', 'meta' => ['int' => 1, 'str' => 'string', 'bool' => true]]; + $payload = ['name' => 'handler', 'data' => 'test', 'meta' => ['int' => 1, 'str' => 'string', 'bool' => true]]; $serializer = $this->createSerializer(); $message = $serializer->unserialize(json_encode($payload)); $this->assertInstanceOf(MessageInterface::class, $message); $this->assertEquals($payload['data'], $message->getData()); - $this->assertEquals(['int' => 1, 'str' => 'string', 'bool' => true], $message->getMetadata()); + $this->assertEquals(['int' => 1, 'str' => 'string', 'bool' => true, EnvelopeInterface::ENVELOPE_STACK_KEY => []], $message->getMetadata()); } public function testUnserializeEnvelopeStack(): void { $payload = [ + 'name' => 'handler', 'data' => 'test', 'meta' => [ EnvelopeInterface::ENVELOPE_STACK_KEY => [ @@ -113,7 +115,7 @@ public function testSerialize(): void $json = $serializer->serialize($message); $this->assertEquals( - '{"name":"handler","data":"test","meta":[]}', + '{"name":"handler","data":"test","meta":{"message-class":"Yiisoft\\\\Queue\\\\Message\\\\Message"}}', $json, ); } @@ -129,9 +131,10 @@ public function testSerializeEnvelopeStack(): void $this->assertEquals( sprintf( - '{"name":"handler","data":"test","meta":{"envelopes":["%s"],"%s":"test-id"}}', + '{"name":"handler","data":"test","meta":{"envelopes":["%s"],"%s":"test-id","message-class":"%s"}}', str_replace('\\', '\\\\', IdEnvelope::class), IdEnvelope::MESSAGE_ID_KEY, + str_replace('\\', '\\\\', Message::class), ), $json, ); @@ -145,14 +148,35 @@ public function testSerializeEnvelopeStack(): void IdEnvelope::class, ], IdEnvelope::MESSAGE_ID_KEY => 'test-id', + 'message-class' => Message::class, ], $message->getMetadata()); $this->assertEquals([ EnvelopeInterface::ENVELOPE_STACK_KEY => [], IdEnvelope::MESSAGE_ID_KEY => 'test-id', + 'message-class' => Message::class, ], $message->getMessage()->getMetadata()); } + public function testRestoreOriginalMessageClass(): void + { + $message = new TestMessage(); + $serializer = $this->createSerializer(); + $serializer->unserialize($serializer->serialize($message)); + + $this->assertInstanceOf(TestMessage::class, $message); + } + + public function testRestoreOriginalMessageClassWithEnvelope(): void + { + $message = new IdEnvelope(new TestMessage()); + $serializer = $this->createSerializer(); + $serializer->unserialize($serializer->serialize($message)); + + $this->assertInstanceOf(IdEnvelope::class, $message); + $this->assertInstanceOf(TestMessage::class, $message->getMessage()); + } + private function createSerializer(): JsonMessageSerializer { return new JsonMessageSerializer(); diff --git a/tests/Unit/Support/TestMessage.php b/tests/Unit/Support/TestMessage.php new file mode 100644 index 00000000..a4c0ab8d --- /dev/null +++ b/tests/Unit/Support/TestMessage.php @@ -0,0 +1,30 @@ +