Skip to content

Commit

Permalink
Allow to restore original pushed message class on consume (#214)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
Co-authored-by: viktorprogger <[email protected]>
  • Loading branch information
3 people authored Nov 24, 2024
1 parent b337fdd commit af0fb00
Show file tree
Hide file tree
Showing 6 changed files with 109 additions and 20 deletions.
10 changes: 10 additions & 0 deletions src/Message/EnvelopeTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
42 changes: 30 additions & 12 deletions src/Message/JsonMessageSerializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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<MessageInterface> $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;
}
Expand Down
5 changes: 5 additions & 0 deletions src/Message/Message.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions src/Message/MessageInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

interface MessageInterface
{
public static function fromData(string $handlerName, mixed $data, array $metadata = []): self;

/**
* Returns handler name.
*
Expand Down
40 changes: 32 additions & 8 deletions tests/Unit/Message/JsonMessageSerializerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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));
}
Expand All @@ -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 => [
Expand Down Expand Up @@ -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,
);
}
Expand All @@ -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,
);
Expand All @@ -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();
Expand Down
30 changes: 30 additions & 0 deletions tests/Unit/Support/TestMessage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Queue\Tests\Unit\Support;

use Yiisoft\Queue\Message\MessageInterface;

final class TestMessage implements MessageInterface
{
public static function fromData(string $handlerName, mixed $data, array $metadata = []): MessageInterface
{
return new self();
}

public function getHandlerName(): string
{
return 'test';
}

public function getData(): mixed
{
return null;
}

public function getMetadata(): array
{
return [];
}
}

0 comments on commit af0fb00

Please sign in to comment.