diff --git a/composer.json b/composer.json index 7846b83f..fd3e594f 100644 --- a/composer.json +++ b/composer.json @@ -40,6 +40,7 @@ "psr/container": "^1.0 || ^2.0", "psr/log": "^2.0 || ^3.0", "symfony/console": "^5.4 || ^6.0 || ^7.0", + "yiisoft/arrays": "^3.1", "yiisoft/definitions": "^3.3.1", "yiisoft/factory": "^1.3", "yiisoft/friendly-exception": "^1.0", diff --git a/src/Message/Envelope.php b/src/Message/Envelope.php new file mode 100644 index 00000000..d171e3e7 --- /dev/null +++ b/src/Message/Envelope.php @@ -0,0 +1,56 @@ +message; + } + + public function getHandlerName(): string + { + return $this->message->getHandlerName(); + } + + public function getData(): mixed + { + return $this->message->getData(); + } + + public function getMetadata(): array + { + if ($this->metadata === null) { + $this->metadata = array_merge( + $this->message->getMetadata(), + [ + EnvelopeInterface::ENVELOPE_STACK_KEY => array_merge( + $this->message->getMetadata()[EnvelopeInterface::ENVELOPE_STACK_KEY] ?? [], + [static::class], + ), + ], + $this->getEnvelopeMetadata(), + ); + } + + return $this->metadata; + } + + abstract protected function getEnvelopeMetadata(): array; +} diff --git a/src/Message/EnvelopeInterface.php b/src/Message/EnvelopeInterface.php index 7c58daa1..612fe368 100644 --- a/src/Message/EnvelopeInterface.php +++ b/src/Message/EnvelopeInterface.php @@ -12,9 +12,7 @@ interface EnvelopeInterface extends MessageInterface /** @psalm-suppress MissingClassConstType */ public const ENVELOPE_STACK_KEY = 'envelopes'; - public static function fromMessage(MessageInterface $message): self; + public static function fromMessage(MessageInterface $message): static; public function getMessage(): MessageInterface; - - public function withMessage(MessageInterface $message): self; } diff --git a/src/Message/EnvelopeTrait.php b/src/Message/EnvelopeTrait.php deleted file mode 100644 index 68ae0d88..00000000 --- a/src/Message/EnvelopeTrait.php +++ /dev/null @@ -1,62 +0,0 @@ -message; - } - - public function withMessage(MessageInterface $message): self - { - $instance = clone $this; - $instance->message = $message; - - return $instance; - } - - public function getHandlerName(): string - { - return $this->message->getHandlerName(); - } - - public function getData(): mixed - { - return $this->message->getData(); - } - - public function getMetadata(): array - { - return array_merge( - $this->message->getMetadata(), - [ - EnvelopeInterface::ENVELOPE_STACK_KEY => array_merge( - $this->message->getMetadata()[EnvelopeInterface::ENVELOPE_STACK_KEY] ?? [], - [self::class], - ), - ], - $this->getEnvelopeMetadata(), - ); - } - - public function getEnvelopeMetadata(): array - { - return []; - } -} diff --git a/src/Message/IdEnvelope.php b/src/Message/IdEnvelope.php index bbf0d67f..3907e7ee 100644 --- a/src/Message/IdEnvelope.php +++ b/src/Message/IdEnvelope.php @@ -7,34 +7,40 @@ /** * ID envelope allows to identify a message. */ -final class IdEnvelope implements EnvelopeInterface +final class IdEnvelope extends Envelope { - use EnvelopeTrait; - public const MESSAGE_ID_KEY = 'yii-message-id'; public function __construct( - private MessageInterface $message, - private string|int|null $id = null, + MessageInterface $message, + private readonly string|int|null $id, ) { + parent::__construct($message); } - public static function fromMessage(MessageInterface $message): self - { - return new self($message, $message->getMetadata()[self::MESSAGE_ID_KEY] ?? null); - } - - public function setId(string|int|null $id): void + public static function fromMessage(MessageInterface $message): static { - $this->id = $id; + /** @var mixed $rawId */ + $rawId = $message->getMetadata()[self::MESSAGE_ID_KEY] ?? null; + + /** @var int|string|null $id */ + $id = match (true) { + $rawId === null => null, + is_string($rawId) => $rawId, + is_int($rawId) => $rawId, + is_object($rawId) && method_exists($rawId, '__toString') => (string)$rawId, + default => throw new \InvalidArgumentException(sprintf('Message ID must be string|int|null, %s given.', get_debug_type($rawId))), + }; + + return new self($message, $id); } public function getId(): string|int|null { - return $this->id ?? $this->message->getMetadata()[self::MESSAGE_ID_KEY] ?? null; + return $this->id; } - private function getEnvelopeMetadata(): array + protected function getEnvelopeMetadata(): array { return [self::MESSAGE_ID_KEY => $this->getId()]; } diff --git a/src/Message/Message.php b/src/Message/Message.php index ab85d069..4f153767 100644 --- a/src/Message/Message.php +++ b/src/Message/Message.php @@ -7,9 +7,9 @@ final class Message implements MessageInterface { /** + * @param string $handlerName A name of a handler which should handle this message. * @param mixed $data Message data, encodable by a queue adapter * @param array $metadata Message metadata, encodable by a queue adapter - * @param string|null $id Message id */ public function __construct( private string $handlerName, @@ -37,12 +37,4 @@ public function getMetadata(): array { return $this->metadata; } - - public function withMetadata(array $metadata): self - { - $instance = clone $this; - $instance->metadata = $metadata; - - return $instance; - } } diff --git a/src/Message/MessageInterface.php b/src/Message/MessageInterface.php index ea3b1882..a7367248 100644 --- a/src/Message/MessageInterface.php +++ b/src/Message/MessageInterface.php @@ -10,22 +10,16 @@ public static function fromData(string $handlerName, mixed $data, array $metadat /** * Returns handler name. - * - * @return string */ public function getHandlerName(): string; /** * Returns payload data. - * - * @return mixed */ public function getData(): mixed; /** * Returns message metadata: timings, attempts count, metrics, etc. - * - * @return array */ public function getMetadata(): array; } diff --git a/src/Middleware/FailureHandling/FailureEnvelope.php b/src/Middleware/FailureHandling/FailureEnvelope.php index 0f6a62e9..9b9f8515 100644 --- a/src/Middleware/FailureHandling/FailureEnvelope.php +++ b/src/Middleware/FailureHandling/FailureEnvelope.php @@ -4,32 +4,34 @@ namespace Yiisoft\Queue\Middleware\FailureHandling; -use Yiisoft\Queue\Message\EnvelopeInterface; -use Yiisoft\Queue\Message\EnvelopeTrait; +use Yiisoft\Arrays\ArrayHelper; +use Yiisoft\Queue\Message\Envelope; use Yiisoft\Queue\Message\MessageInterface; -final class FailureEnvelope implements EnvelopeInterface +final class FailureEnvelope extends Envelope { - use EnvelopeTrait; - public const FAILURE_META_KEY = 'failure-meta'; public function __construct( - private MessageInterface $message, - private array $meta = [], + MessageInterface $message, + private readonly array $metadata = [], ) { + parent::__construct($message); } - public static function fromMessage(MessageInterface $message): self + public static function fromMessage(MessageInterface $message): static { - return new self($message, $message->getMetadata()[self::FAILURE_META_KEY] ?? []); + /** @var array $metadata */ + $metadata = $message->getMetadata()[self::FAILURE_META_KEY] ?? []; + + return new self($message, $metadata); } - public function getMetadata(): array + protected function getEnvelopeMetadata(): array { - $meta = $this->message->getMetadata(); - $meta[self::FAILURE_META_KEY] = array_merge($meta[self::FAILURE_META_KEY] ?? [], $this->meta); + /** @var array $metadata */ + $metadata = $this->message->getMetadata()[self::FAILURE_META_KEY] ?? []; - return $meta; + return [self::FAILURE_META_KEY => ArrayHelper::merge($metadata, $this->metadata)]; } } diff --git a/tests/App/DummyEnvelope.php b/tests/App/DummyEnvelope.php index 65c6162f..dc1d1f7d 100644 --- a/tests/App/DummyEnvelope.php +++ b/tests/App/DummyEnvelope.php @@ -4,19 +4,18 @@ namespace Yiisoft\Queue\Tests\App; -use Yiisoft\Queue\Message\EnvelopeInterface; -use Yiisoft\Queue\Message\EnvelopeTrait; +use Yiisoft\Queue\Message\Envelope; use Yiisoft\Queue\Message\MessageInterface; -final class DummyEnvelope implements EnvelopeInterface +final class DummyEnvelope extends Envelope { - use EnvelopeTrait; - - public static function fromMessage(MessageInterface $message): self + public static function fromMessage(MessageInterface $message): static { - $instance = new self(); - $instance->message = $message; + return new self($message); + } - return $instance; + protected function getEnvelopeMetadata(): array + { + return []; } } diff --git a/tests/Unit/Message/EnvelopeTraitTest.php b/tests/Unit/Message/EnvelopeTraitTest.php index 5dd5c4a6..6ae7f085 100644 --- a/tests/Unit/Message/EnvelopeTraitTest.php +++ b/tests/Unit/Message/EnvelopeTraitTest.php @@ -5,16 +5,10 @@ namespace Yiisoft\Queue\Tests\Unit\Message; use PHPUnit\Framework\TestCase; -use Yiisoft\Queue\Message\Message; use Yiisoft\Queue\Tests\App\DummyEnvelope; final class EnvelopeTraitTest extends TestCase { - private function createTestEnvelope(): DummyEnvelope - { - return new DummyEnvelope(); - } - public function testFromData(): void { $handlerName = 'test-handler'; @@ -29,21 +23,4 @@ public function testFromData(): void $this->assertArrayHasKey('meta', $envelope->getMetadata()); $this->assertSame('data', $envelope->getMetadata()['meta']); } - - public function testWithMessage(): void - { - $originalMessage = new Message('original-handler', 'original-data'); - $newMessage = new Message('new-handler', 'new-data'); - - $envelope = $this->createTestEnvelope(); - $envelope = $envelope->withMessage($originalMessage); - - $this->assertSame($originalMessage, $envelope->getMessage()); - - $newEnvelope = $envelope->withMessage($newMessage); - - $this->assertNotSame($envelope, $newEnvelope); - $this->assertSame($newMessage, $newEnvelope->getMessage()); - $this->assertSame($originalMessage, $envelope->getMessage()); - } } diff --git a/tests/Unit/Message/IdEnvelopeTest.php b/tests/Unit/Message/IdEnvelopeTest.php new file mode 100644 index 00000000..a02f9e5b --- /dev/null +++ b/tests/Unit/Message/IdEnvelopeTest.php @@ -0,0 +1,112 @@ +createMessage(); + $id = 'test-id'; + + $envelope = new IdEnvelope($message, $id); + + $this->assertSame($message, $envelope->getMessage()); + $this->assertSame($id, $envelope->getId()); + } + + public function testFromMessageWithStringId(): void + { + $id = 'test-id'; + $message = $this->createMessage([IdEnvelope::MESSAGE_ID_KEY => $id]); + + $envelope = IdEnvelope::fromMessage($message); + + $this->assertSame($id, $envelope->getId()); + } + + public function testFromMessageWithIntId(): void + { + $id = 123; + $message = $this->createMessage([IdEnvelope::MESSAGE_ID_KEY => $id]); + + $envelope = IdEnvelope::fromMessage($message); + + $this->assertSame($id, $envelope->getId()); + } + + public function testFromMessageWithNullId(): void + { + $message = $this->createMessage(); + + $envelope = IdEnvelope::fromMessage($message); + + $this->assertNull($envelope->getId()); + } + + public function testFromMessageWithObjectHavingToString(): void + { + $stringableObject = new class () { + public function __toString(): string + { + return 'object-id'; + } + }; + $message = $this->createMessage([IdEnvelope::MESSAGE_ID_KEY => $stringableObject]); + $envelope = IdEnvelope::fromMessage($message); + + $this->assertSame('object-id', $envelope->getId()); + } + + public function testFromMessageWithInvalidIdType(): void + { + $invalidId = ['array-cannot-be-id']; + $message = $this->createMessage([IdEnvelope::MESSAGE_ID_KEY => $invalidId]); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Message ID must be string|int|null, array given.'); + + IdEnvelope::fromMessage($message); + } + + public function testGetEnvelopeMetadata(): void + { + $id = 'test-id'; + $message = $this->createMessage(); + $envelope = new IdEnvelope($message, $id); + + $metadata = $envelope->getMetadata(); + + $this->assertArrayHasKey(IdEnvelope::MESSAGE_ID_KEY, $metadata); + $this->assertSame($id, $metadata[IdEnvelope::MESSAGE_ID_KEY]); + } + + public function testFromData(): void + { + $handlerName = 'test-handler'; + $data = ['key' => 'value']; + $metadata = ['meta' => 'data', IdEnvelope::MESSAGE_ID_KEY => 'test-id']; + + $envelope = IdEnvelope::fromData($handlerName, $data, $metadata); + + $this->assertInstanceOf(IdEnvelope::class, $envelope); + $this->assertSame($handlerName, $envelope->getHandlerName()); + $this->assertSame($data, $envelope->getData()); + $this->assertArrayHasKey('meta', $envelope->getMetadata()); + $this->assertSame('data', $envelope->getMetadata()['meta']); + $this->assertSame('test-id', $envelope->getId()); + } + + private function createMessage(array $metadata = []): MessageInterface + { + return new Message('test-handler', ['test-data'], $metadata); + } +} diff --git a/tests/Unit/Message/JsonMessageSerializerTest.php b/tests/Unit/Message/JsonMessageSerializerTest.php index 93889d3b..38820cb3 100644 --- a/tests/Unit/Message/JsonMessageSerializerTest.php +++ b/tests/Unit/Message/JsonMessageSerializerTest.php @@ -219,7 +219,7 @@ public function testRestoreOriginalMessageClass(): void public function testRestoreOriginalMessageClassWithEnvelope(): void { - $message = new IdEnvelope(new TestMessage()); + $message = new IdEnvelope(new TestMessage(), 1); $serializer = $this->createSerializer(); $serializer->unserialize($serializer->serialize($message)); diff --git a/tests/Unit/Middleware/FailureHandling/FailureEnvelopeTest.php b/tests/Unit/Middleware/FailureHandling/FailureEnvelopeTest.php new file mode 100644 index 00000000..e3991cc7 --- /dev/null +++ b/tests/Unit/Middleware/FailureHandling/FailureEnvelopeTest.php @@ -0,0 +1,83 @@ +createMessage(); + $metadata = ['attempt' => 1, 'error' => 'Test error']; + + $envelope = new FailureEnvelope($message, $metadata); + + $this->assertSame($message, $envelope->getMessage()); + $this->assertArrayHasKey(FailureEnvelope::FAILURE_META_KEY, $envelope->getMetadata()); + $this->assertSame($metadata, $envelope->getMetadata()[FailureEnvelope::FAILURE_META_KEY]); + } + + public function testFromMessageWithExistingMetadata(): void + { + $existingMetadata = ['attempt' => 1]; + $message = $this->createMessage([FailureEnvelope::FAILURE_META_KEY => $existingMetadata]); + + $envelope = FailureEnvelope::fromMessage($message); + + $this->assertSame($existingMetadata, $envelope->getMetadata()[FailureEnvelope::FAILURE_META_KEY]); + } + + public function testFromMessageWithoutMetadata(): void + { + $message = $this->createMessage(); + + $envelope = FailureEnvelope::fromMessage($message); + + $this->assertArrayHasKey(FailureEnvelope::FAILURE_META_KEY, $envelope->getMetadata()); + $this->assertSame([], $envelope->getMetadata()[FailureEnvelope::FAILURE_META_KEY]); + } + + public function testMetadataMerging(): void + { + $existingMetadata = ['attempt' => 1, 'firstError' => 'First error']; + $message = $this->createMessage([FailureEnvelope::FAILURE_META_KEY => $existingMetadata]); + $newMetadata = ['attempt' => 2, 'lastError' => 'Last error']; + + $envelope = new FailureEnvelope($message, $newMetadata); + + $mergedMetadata = $envelope->getMetadata()[FailureEnvelope::FAILURE_META_KEY]; + $this->assertSame(2, $mergedMetadata['attempt']); + $this->assertSame('First error', $mergedMetadata['firstError']); + $this->assertSame('Last error', $mergedMetadata['lastError']); + } + + public function testFromData(): void + { + $handlerName = 'test-handler'; + $data = ['key' => 'value']; + $metadata = [ + 'meta' => 'data', + FailureEnvelope::FAILURE_META_KEY => ['attempt' => 1], + ]; + + $envelope = FailureEnvelope::fromData($handlerName, $data, $metadata); + + $this->assertInstanceOf(FailureEnvelope::class, $envelope); + $this->assertSame($handlerName, $envelope->getHandlerName()); + $this->assertSame($data, $envelope->getData()); + $this->assertArrayHasKey('meta', $envelope->getMetadata()); + $this->assertSame('data', $envelope->getMetadata()['meta']); + $this->assertSame(['attempt' => 1], $envelope->getMetadata()[FailureEnvelope::FAILURE_META_KEY]); + } + + private function createMessage(array $metadata = []): MessageInterface + { + return new Message('test-handler', ['test-data'], $metadata); + } +} diff --git a/tests/Unit/Middleware/FailureHandling/Implementation/SendAgainMiddlewareTest.php b/tests/Unit/Middleware/FailureHandling/Implementation/SendAgainMiddlewareTest.php index 665c6bea..92351a8d 100644 --- a/tests/Unit/Middleware/FailureHandling/Implementation/SendAgainMiddlewareTest.php +++ b/tests/Unit/Middleware/FailureHandling/Implementation/SendAgainMiddlewareTest.php @@ -151,9 +151,6 @@ public function testQueueSendingStrategies( $this->expectExceptionMessage('testException'); } - $metaInitial = [FailureEnvelope::FAILURE_META_KEY => $metaInitial]; - $metaResult = [FailureEnvelope::FAILURE_META_KEY => $metaResult]; - $handler = $this->getHandler($metaResult, $suites); $queue = $this->getPreparedQueue($metaResult, $suites); @@ -162,7 +159,7 @@ public function testQueueSendingStrategies( new Message( 'test', null, - $metaInitial + [FailureEnvelope::FAILURE_META_KEY => $metaInitial] ), new Exception('testException'), $queue @@ -194,7 +191,7 @@ private function getHandler(array $metaResult, bool $suites): MessageFailureHand $pipelineAssertion = static function (FailureHandlingRequest $request) use ( $metaResult ): FailureHandlingRequest { - Assert::assertEquals($metaResult, $request->getMessage()->getMetadata()); + Assert::assertEquals($metaResult, $request->getMessage()->getMetadata()[FailureEnvelope::FAILURE_META_KEY] ?? []); throw $request->getException(); }; @@ -209,7 +206,7 @@ private function getHandler(array $metaResult, bool $suites): MessageFailureHand private function getPreparedQueue(array $metaResult, bool $suites): QueueInterface { $queueAssertion = static function (MessageInterface $message) use ($metaResult): MessageInterface { - Assert::assertEquals($metaResult, $message->getMetadata()); + Assert::assertEquals($metaResult, $message->getMetadata()[FailureEnvelope::FAILURE_META_KEY] ?? []); return $message; };