Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/guide/en/message-handler-advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Handler definitions are configured in:
### Handlers mapped by short message type

Use a short stable message type instead of a PHP class name. That decoupling would allow you to refactor the code and handle the message with external handler.
Define a dedicated message class where `getType()` returns that type:
Define a dedicated message class where `getType()` returns that type. The type must be a non-empty string:

```php
use Yiisoft\Queue\Message\Message;
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/en/messages-and-handlers.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ new SendEmailMessage('user@example.com', 'Welcome', 'Thank you for registering.'

The message has:

- A **message type** — a string used by the worker to look up the correct handler.
- A **message type** — a non-empty string used by the worker to look up the correct handler.
- A **data payload** — typed properties serialized via `getPayload()`. Must contain only `null`, scalars (`bool`, `int`, `float`, `string`), or arrays composed of the same types recursively.

The message has no business logic, no dependencies. It is a value object — a typed data wrapper.
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/en/migrating-from-yii2-queue.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ There was a concept in [yiisoft/yii2-queue] called `Job`: you had to push it to
being consumed. In the new package, it is divided into two different concepts: a message and a handler.

- A `Message` is a class implementing `MessageInterface`. It contains two types of data:
- Type. The worker uses it to find the right handler for a message.
- Type. A non-empty string. The worker uses it to find the right handler for a message.
- Payload. Any serializable data that should be used by the message handler.

All the message payload is fully serializable (that means message `payload` must be serializable too). It allows you to
Expand Down
3 changes: 2 additions & 1 deletion src/Message/ClassResolver/MessageClassResolverInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ interface MessageClassResolverInterface
/**
* Returns the message class for the given type, or `null` if the type is not registered.
*
* @param string $type Message type.
* @param string $type Message type. Must be a non-empty string.
*
* @return string|null Message class, or `null` if the type is not registered.
*
* @psalm-param non-empty-string $type
* @psalm-return class-string<MessageInterface>|null
*/
public function resolve(string $type): ?string;
Expand Down
14 changes: 12 additions & 2 deletions src/Message/GenericMessage.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

namespace Yiisoft\Queue\Message;

use InvalidArgumentException;

/**
* A general-purpose immutable {@see MessageInterface} implementation that holds a message type and its payload data.
*
Expand All @@ -14,16 +16,24 @@
final class GenericMessage extends Message
{
/**
* @param string $type A message type used to resolve the handler.
* @param string $type A message type used to resolve the handler. Must be a non-empty string.
* @param bool|int|float|string|array|null $payload Message payload data. Must contain only `null`, scalars (`bool`,
* `int`, `float`, `string`), or arrays composed of the same types recursively.
*
* @psalm-param non-empty-string $type
* @psalm-param MessagePayload $payload
*/
public function __construct(
private readonly string $type,
private readonly bool|int|float|string|array|null $payload,
) {}
) {
/**
* @psalm-suppress TypeDoesNotContainType Guard against an empty type passed without static analysis.
*/
if ($this->type === '') {
throw new InvalidArgumentException('Message type must be a non-empty string.');
}
}

public static function fromPayload(string $type, bool|int|float|string|array|null $payload): static
{
Expand Down
9 changes: 3 additions & 6 deletions src/Message/Handler/HandlerResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

namespace Yiisoft\Queue\Message\Handler;

use LogicException;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\ContainerInterface;
use Yiisoft\Injector\Injector;
Expand Down Expand Up @@ -51,18 +50,16 @@ public function __construct(
/**
* Get a handler for the given message type.
*
* @param string $messageType Message type.
* @param string $messageType Message type. Must be a non-empty string.
*
* @psalm-param non-empty-string $messageType
*
* @throws HandlerNotFoundException If no handler exists for the message type.
* @throws InvalidHandlerConfigurationException If the handler definition is configured incorrectly.
* @throws ContainerExceptionInterface Error while retrieving the entry from container.
*/
public function resolve(string $messageType): HandlerInterface
{
if ($messageType === '') {
throw new LogicException('Message type cannot be empty.');
}

if (array_key_exists($messageType, $this->cache)) {
return $this->cache[$messageType];
}
Expand Down
7 changes: 5 additions & 2 deletions src/Message/MessageInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@ interface MessageInterface
/**
* Creates a new message instance from the given type and payload data.
*
* @param string $type Message type.
* @param string $type Message type. Must be a non-empty string.
* @param bool|int|float|string|array|null $payload Message payload data. Must contain only `null`, scalars (`bool`,
* `int`, `float`, `string`), or arrays composed of the same types recursively.
*
* @psalm-param non-empty-string $type
* @psalm-param MessagePayload $payload
*
* @return static Instance of the called class with the given type and payload.
Expand All @@ -28,7 +29,9 @@ public static function fromPayload(string $type, bool|int|float|string|array|nul
/**
* Returns message type.
*
* @return string Message type.
* @return string Message type. Always a non-empty string.
*
* @psalm-return non-empty-string
*/
public function getType(): string;

Expand Down
9 changes: 6 additions & 3 deletions src/Message/Serializer/MessageSerializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,15 @@ public function unserialize(string $value): MessageInterface
$data = $this->encoder->decode($value);

if (!is_array($data)) {
throw new MessageSerializerException('Decoded data must be array. Got ' . get_debug_type($data) . '.');
throw new MessageSerializerException('Decoded data must be an array. Got ' . get_debug_type($data) . '.');
}

$type = $data['type'] ?? null;
if (!isset($type) || !is_string($type)) {
throw new MessageSerializerException('Message type must be a string. Got ' . get_debug_type($type) . '.');
if (!is_string($type)) {
throw new MessageSerializerException('Message type must be a non-empty string. Got ' . get_debug_type($type) . '.');
}
if ($type === '') {
throw new MessageSerializerException('Message type must be a non-empty string. Got empty string.');
}

$meta = $data['meta'] ?? [];
Expand Down
28 changes: 28 additions & 0 deletions tests/Unit/Message/GenericMessageTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Queue\Tests\Unit\Message;

use InvalidArgumentException;
use PHPUnit\Framework\TestCase;
use Yiisoft\Queue\Message\GenericMessage;

final class GenericMessageTest extends TestCase
{
public function testConstructorThrowsWhenTypeIsEmpty(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Message type must be a non-empty string.');

new GenericMessage('', null);
}

public function testFromPayloadThrowsWhenTypeIsEmpty(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Message type must be a non-empty string.');

GenericMessage::fromPayload('', null);
}
}
12 changes: 0 additions & 12 deletions tests/Unit/Message/Handler/Resolver/HandlerResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

namespace Yiisoft\Queue\Tests\Unit\Message\Handler\Resolver;

use LogicException;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Yiisoft\Test\Support\Container\SimpleContainer;
Expand Down Expand Up @@ -182,17 +181,6 @@ public function handle(): void {}

$resolver->resolve('invalid');
}

public function testResolveThrowsWhenMessageTypeIsEmpty(): void
{
$this->expectException(LogicException::class);
$this->expectExceptionMessage('Message type cannot be empty.');

$container = new SimpleContainer();
$resolver = new HandlerResolver([], $container);

$resolver->resolve('');
}
}

function namedFunctionHandler(MessageInterface $message): void
Expand Down
18 changes: 16 additions & 2 deletions tests/Unit/Message/Serializer/MessageSerializerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ final class MessageSerializerTest extends TestCase
public function testNonArrayPayload(string $json, string $type): void
{
$this->expectException(MessageSerializerException::class);
$this->expectExceptionMessage(sprintf('Decoded data must be array. Got %s.', $type));
$this->expectExceptionMessage(sprintf('Decoded data must be an array. Got %s.', $type));
$this->createSerializer()->unserialize($json);
}

Expand All @@ -42,7 +42,21 @@ public function testUnsupportedType(mixed $type): void
);

$this->expectException(MessageSerializerException::class);
$this->expectExceptionMessage(sprintf('Message type must be a string. Got %s.', get_debug_type($type)));
$this->expectExceptionMessage(
sprintf('Message type must be a non-empty string. Got %s.', get_debug_type($type)),
);
$this->createSerializer()->unserialize($value);
}

public function testEmptyType(): void
{
$value = json_encode(
['type' => '', 'payload' => 'test', 'meta' => []],
JSON_THROW_ON_ERROR,
);

$this->expectException(MessageSerializerException::class);
$this->expectExceptionMessage('Message type must be a non-empty string. Got empty string.');
$this->createSerializer()->unserialize($value);
}

Expand Down
Loading