Skip to content

Commit ef2dd9e

Browse files
vjiksamdark
andauthored
Introduce abstract Message class (#291)
Co-authored-by: Alexander Makarov <sam@rmcreative.ru>
1 parent 1a8ba57 commit ef2dd9e

55 files changed

Lines changed: 512 additions & 313 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 71 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -42,76 +42,111 @@ See the [adapter list](docs/guide/en/adapter-list.md) and follow the adapter-spe
4242
> In this mode messages are processed immediately in the same process, so it won't provide true
4343
> async execution, but the code stays the same when you switch to a real adapter.
4444
45-
### 2. Configure the queue
45+
### 2. Prepare a message and handler
4646

47-
#### Configuration with [yiisoft/config](https://github.com/yiisoft/config)
48-
49-
**If you use [yiisoft/app](https://github.com/yiisoft/app) or [yiisoft/app-api](https://github.com/yiisoft/app-api)**
50-
51-
Add queue configuration to your application `$params` config. In [yiisoft/app](https://github.com/yiisoft/app)/[yiisoft/app-api](https://github.com/yiisoft/app-api) templates it's typically the `config/params.php` file.
52-
_If your project structure differs, put it into any params config file that is loaded by [yiisoft/config](https://github.com/yiisoft/config)._
53-
54-
Minimal configuration example:
47+
Define a message class for the work to be done — a simple value object with typed properties:
5548

5649
```php
57-
return [
58-
'yiisoft/queue' => [
59-
'handlers' => [
60-
'message-type' => [FooHandler::class, 'handle'],
61-
],
62-
],
63-
];
64-
```
50+
use Yiisoft\Queue\Message\Message;
6551

66-
[Advanced configuration with `yiisoft/config`](docs/guide/en/configuration-with-config.md)
52+
final class DownloadFileMessage extends Message
53+
{
54+
public const TYPE = 'download-file';
6755

68-
#### Manual configuration
56+
public function __construct(
57+
public readonly string $url,
58+
public readonly string $destinationPath,
59+
) {}
6960

70-
For setting up all classes manually, see the [Manual configuration](docs/guide/en/configuration-manual.md) guide.
61+
public static function fromData(string $type, mixed $data): static
62+
{
63+
if ($type !== self::TYPE) {
64+
throw new \InvalidArgumentException("Expected type \"" . self::TYPE . "\", got \"$type\".");
65+
}
66+
if (!is_array($data)
67+
|| !is_string($data['url'] ?? null)
68+
|| !is_string($data['destinationPath'] ?? null)
69+
) {
70+
throw new \InvalidArgumentException('Invalid data for ' . self::class . '.');
71+
}
72+
return new self($data['url'], $data['destinationPath']);
73+
}
7174

72-
### 3. Prepare a handler
75+
public function getType(): string
76+
{
77+
return self::TYPE;
78+
}
7379

74-
You need to create a handler class that will process the queue messages. The most simple way is to implement the `MessageHandlerInterface`. Let's create an example for remote file processing:
80+
public function getData(): array
81+
{
82+
return ['url' => $this->url, 'destinationPath' => $this->destinationPath];
83+
}
84+
}
85+
```
86+
87+
Then create a handler that processes it:
7588

7689
```php
7790
use Yiisoft\Queue\Message\MessageInterface;
7891
use Yiisoft\Queue\Message\MessageHandlerInterface;
7992

8093
final readonly class RemoteFileHandler implements MessageHandlerInterface
8194
{
82-
// These dependencies will be resolved on handler creation by the DI container
8395
public function __construct(
8496
private FileDownloader $downloader,
8597
private FileProcessor $processor,
8698
) {}
8799

88-
// Every received message will be processed by this method
89-
public function handle(MessageInterface $downloadMessage): void
100+
public function handle(MessageInterface $message): void
90101
{
91-
$url = $downloadMessage->getData()['url'];
92-
$localPath = $this->downloader->download($url);
102+
assert($message instanceof DownloadFileMessage);
103+
$localPath = $this->downloader->download($message->url, $message->destinationPath);
93104
$this->processor->process($localPath);
94105
}
95106
}
96107
```
97108

98-
### 4. Send (produce/push) a message to a queue
109+
### 3. Configure the queue
110+
111+
#### Configuration with [yiisoft/config](https://github.com/yiisoft/config)
112+
113+
**If you use [yiisoft/app](https://github.com/yiisoft/app) or [yiisoft/app-api](https://github.com/yiisoft/app-api)**
114+
115+
Add queue configuration to your application `$params` config. In [yiisoft/app](https://github.com/yiisoft/app)/[yiisoft/app-api](https://github.com/yiisoft/app-api) templates it's typically the `config/params.php` file.
116+
_If your project structure differs, put it into any params config file that is loaded by [yiisoft/config](https://github.com/yiisoft/config)._
99117

100-
To send a message to the queue, you need to get the queue instance and call the `push()` method. Typically, with Yii Framework you'll get a `Queue` instance as a dependency of a service.
118+
Minimal configuration example:
101119

102120
```php
121+
return [
122+
'yiisoft/queue' => [
123+
'handlers' => [
124+
DownloadFileMessage::TYPE => RemoteFileHandler::class,
125+
],
126+
],
127+
];
128+
```
129+
130+
[Advanced configuration with `yiisoft/config`](docs/guide/en/configuration-with-config.md)
131+
132+
#### Manual configuration
103133

104-
final readonly class Foo {
134+
For setting up all classes manually, see the [Manual configuration](docs/guide/en/configuration-manual.md) guide.
135+
136+
### 4. Send (produce/push) a message to a queue
137+
138+
To send a message to the queue, get the queue instance and call `push()`. Typically the queue is injected as a dependency:
139+
140+
```php
141+
final readonly class Foo
142+
{
105143
public function __construct(private QueueInterface $queue) {}
106144

107145
public function bar(): void
108146
{
109-
$this->queue->push(new Message(
110-
// The first parameter is the message type used to resolve the handler which will process the message
111-
RemoteFileHandler::class,
112-
// The second parameter is the data that will be passed to the handler.
113-
// It should be serializable to JSON format
114-
['url' => 'https://example.com/file-path.csv'],
147+
$this->queue->push(new DownloadFileMessage(
148+
url: 'https://example.com/file-path.csv',
149+
destinationPath: '/tmp/file-path.csv',
115150
));
116151
}
117152
}

docs/guide/en/configuration-manual.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,7 @@ $logger = new NullLogger(); // replace with your PSR-3 logger in production
3535

3636
// Define message handlers
3737
$handlers = [
38-
'file-download' => [FileDownloader::class, 'handle'],
39-
FileDownloader::class => [FileDownloader::class, 'handle'],
38+
DownloadFileMessage::TYPE => [FileDownloader::class, 'handle'],
4039
];
4140

4241
$callableFactory = new CallableFactory($container);
@@ -79,7 +78,7 @@ $queue = new Queue(
7978
);
8079

8180
// Now you can push messages
82-
$message = new \Yiisoft\Queue\Message\Message('file-download', ['url' => 'https://example.com/file.pdf']);
81+
$message = new DownloadFileMessage(url: 'https://example.com/file.pdf', destinationPath: '/tmp/file.pdf');
8382
$queue->push($message);
8483
```
8584

docs/guide/en/message-handler-advanced.md

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,52 @@ Handler definitions are configured in:
1414

1515
### Handlers mapped by short message type
1616

17-
Use a short stable message type when pushing a `Message` instead of a PHP class name:
17+
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.
18+
Define a dedicated message class where `getType()` returns that type:
1819

1920
```php
2021
use Yiisoft\Queue\Message\Message;
2122

22-
new Message('send-email', ['data' => '...']); // "send-email" is the message type here
23+
final class SendEmailMessage extends Message
24+
{
25+
public const TYPE = 'send-email';
26+
27+
public function __construct(
28+
public readonly string $to,
29+
public readonly string $subject,
30+
public readonly string $body,
31+
) {}
32+
33+
public static function fromData(string $type, mixed $data): static
34+
{
35+
if ($type !== self::TYPE) {
36+
throw new \InvalidArgumentException("Expected type \"" . self::TYPE . "\", got \"$type\".");
37+
}
38+
if (!is_array($data)
39+
|| !is_string($data['to'] ?? null)
40+
|| !is_string($data['subject'] ?? null)
41+
|| !is_string($data['body'] ?? null)
42+
) {
43+
throw new \InvalidArgumentException('Invalid data for ' . self::class . '.');
44+
}
45+
return new self($data['to'], $data['subject'], $data['body']);
46+
}
47+
48+
public function getType(): string
49+
{
50+
return self::TYPE;
51+
}
52+
53+
public function getData(): array
54+
{
55+
return ['to' => $this->to, 'subject' => $this->subject, 'body' => $this->body];
56+
}
57+
}
58+
```
59+
60+
```php
61+
new SendEmailMessage('user@example.com', 'Welcome', 'Thank you for registering.');
62+
// getType() returns "send-email" — used by the worker to look up the handler
2363
```
2464

2565
**Config**:

docs/guide/en/message-handler.md

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,34 @@ If your handler implements `Yiisoft\Queue\Message\MessageHandlerInterface`, you
1313
**Message**:
1414

1515
```php
16-
new \Yiisoft\Queue\Message\Message(\App\Queue\RemoteFileHandler::class, ['url' => '...']);
16+
use Yiisoft\Queue\Message\Message;
17+
18+
final class RemoteFileMessage extends Message
19+
{
20+
public function __construct(public readonly string $url) {}
21+
22+
public static function fromData(string $type, mixed $data): static
23+
{
24+
if (!is_array($data) || !is_string($data['url'] ?? null)) {
25+
throw new \InvalidArgumentException('Invalid data for ' . self::class . '.');
26+
}
27+
return new self($data['url']);
28+
}
29+
30+
public function getType(): string
31+
{
32+
return \App\Queue\RemoteFileHandler::class;
33+
}
34+
35+
public function getData(): array
36+
{
37+
return ['url' => $this->url];
38+
}
39+
}
40+
```
41+
42+
```php
43+
new RemoteFileMessage('https://...');
1744
```
1845

1946
**Handler**:

docs/guide/en/messages-and-handlers.md

Lines changed: 62 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,18 @@ This separation is intentional and important. Understanding it will save you fro
1111

1212
A *producer* creates messages and pushes them onto the queue. A *consumer* (worker) pulls messages from the queue and invokes the matching handler.
1313

14-
```
15-
Producer side Consumer side
16-
───────────────────────────── ──────────────────────────────────
17-
new Message('send-email', …) →→→ Worker resolves handler → handles
18-
(payload only) (logic only)
14+
```mermaid
15+
flowchart LR
16+
subgraph Producer["Producer side"]
17+
Message["new SendEmailMessage(...)\n(payload only)"]
18+
end
19+
20+
subgraph Consumer["Consumer side"]
21+
Worker["Worker resolves handler"]
22+
Handler["Handler handles\n(logic only)"]
23+
end
24+
25+
Message --> Worker --> Handler
1926
```
2027

2128
The producer only needs to know the message type and its data. It does not need to know anything about how the message will be processed, or even in which application.
@@ -30,21 +37,62 @@ This means the producer and consumer can be:
3037

3138
## Message: payload only
3239

33-
A message carries just enough data to perform the work:
40+
A message carries just enough data to perform the work. Usually data has some parameters but not the full context to process. Getting full context is better to be moved to the handler unless processing is done in another application that doesn't have access to data storage.
41+
Defining a dedicated class for each message type makes your code
42+
self-documenting and type-safe:
43+
44+
```php
45+
use Yiisoft\Queue\Message\Message;
46+
47+
final class SendEmailMessage extends Message
48+
{
49+
public const TYPE = 'send-email';
50+
51+
public function __construct(
52+
public readonly string $to,
53+
public readonly string $subject,
54+
public readonly string $body,
55+
) {}
56+
57+
public static function fromData(string $type, mixed $data): static
58+
{
59+
if ($type !== self::TYPE) {
60+
throw new \InvalidArgumentException("Expected type \"" . self::TYPE . "\", got \"$type\".");
61+
}
62+
if (!is_array($data)
63+
|| !is_string($data['to'] ?? null)
64+
|| !is_string($data['subject'] ?? null)
65+
|| !is_string($data['body'] ?? null)
66+
) {
67+
throw new \InvalidArgumentException('Invalid data for ' . self::class . '.');
68+
}
69+
return new self($data['to'], $data['subject'], $data['body']);
70+
}
71+
72+
public function getType(): string
73+
{
74+
return self::TYPE;
75+
}
76+
77+
public function getData(): array
78+
{
79+
return ['to' => $this->to, 'subject' => $this->subject, 'body' => $this->body];
80+
}
81+
}
82+
```
83+
84+
Usage:
3485

3586
```php
36-
new \Yiisoft\Queue\Message\Message('send-email', [
37-
'to' => 'user@example.com',
38-
'subject' => 'Welcome',
39-
]);
87+
new SendEmailMessage('user@example.com', 'Welcome', 'Thank you for registering.');
4088
```
4189

4290
The message has:
4391

4492
- A **message type** — a string used by the worker to look up the correct handler.
45-
- A **data payload**arbitrary data the handler needs. Must be serializable.
93+
- A **data payload**typed properties serialized to JSON via `getData()`. Must be JSON-encodable.
4694

47-
The message has no methods, no business logic, no dependencies. It is a value object — a data wrapper.
95+
The message has no business logic, no dependencies. It is a value object — a typed data wrapper.
4896

4997
## Handler: logic only
5098

@@ -57,8 +105,8 @@ final class SendEmailHandler implements \Yiisoft\Queue\Message\MessageHandlerInt
57105

58106
public function handle(\Yiisoft\Queue\Message\MessageInterface $message): void
59107
{
60-
$data = $message->getData();
61-
$this->mailer->send($data['to'], $data['subject']);
108+
assert($message instanceof SendEmailMessage);
109+
$this->mailer->send($message->to, $message->subject, $message->body);
62110
}
63111
}
64112
```

docs/guide/en/queue-names.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@ Pushing a message via DI:
6868

6969
```php
7070
use Yiisoft\Queue\QueueInterface;
71-
use Yiisoft\Queue\Message\Message;
7271

7372
final readonly class SendWelcomeEmail
7473
{
@@ -78,7 +77,7 @@ final readonly class SendWelcomeEmail
7877

7978
public function run(string $email): void
8079
{
81-
$this->queue->push(new Message('send-email', ['to' => $email]));
80+
$this->queue->push(new SendEmailMessage(to: $email, subject: 'Welcome!', body: 'Thank you for registering.'));
8281
}
8382
}
8483
```
@@ -105,7 +104,6 @@ If you have multiple queue names, inject `QueueProviderInterface` and call `get(
105104

106105
```php
107106
use Yiisoft\Queue\Provider\QueueProviderInterface;
108-
use Yiisoft\Queue\Message\Message;
109107

110108
final readonly class SendTransactionalEmail
111109
{
@@ -117,7 +115,7 @@ final readonly class SendTransactionalEmail
117115
{
118116
$this->queueProvider
119117
->get('emails')
120-
->push(new Message('send-email', ['to' => $email]));
118+
->push(new SendEmailMessage(to: $email, subject: 'Welcome!', body: 'Thank you for registering.'));
121119
}
122120
}
123121
```

0 commit comments

Comments
 (0)