Skip to content
Open
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
107 changes: 71 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,76 +42,111 @@ See the [adapter list](docs/guide/en/adapter-list.md) and follow the adapter-spe
> In this mode messages are processed immediately in the same process, so it won't provide true
> async execution, but the code stays the same when you switch to a real adapter.

### 2. Configure the queue
### 2. Prepare a message and handler

#### Configuration with [yiisoft/config](https://github.com/yiisoft/config)

**If you use [yiisoft/app](https://github.com/yiisoft/app) or [yiisoft/app-api](https://github.com/yiisoft/app-api)**

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.
_If your project structure differs, put it into any params config file that is loaded by [yiisoft/config](https://github.com/yiisoft/config)._

Minimal configuration example:
Define a message class for the work to be done — a simple value object with typed properties:

```php
return [
'yiisoft/queue' => [
'handlers' => [
'message-type' => [FooHandler::class, 'handle'],
],
],
];
```
use Yiisoft\Queue\Message\Message;

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

#### Manual configuration
public function __construct(
public readonly string $url,
public readonly string $destinationPath,
) {}

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

### 3. Prepare a handler
public function getType(): string
{
return self::TYPE;
}

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:
public function getData(): array
{
return ['url' => $this->url, 'destinationPath' => $this->destinationPath];
}
}
```

Then create a handler that processes it:

```php
use Yiisoft\Queue\Message\MessageInterface;
use Yiisoft\Queue\Message\MessageHandlerInterface;

final readonly class RemoteFileHandler implements MessageHandlerInterface
{
// These dependencies will be resolved on handler creation by the DI container
public function __construct(
private FileDownloader $downloader,
private FileProcessor $processor,
) {}

// Every received message will be processed by this method
public function handle(MessageInterface $downloadMessage): void
public function handle(MessageInterface $message): void
{
$url = $downloadMessage->getData()['url'];
$localPath = $this->downloader->download($url);
assert($message instanceof DownloadFileMessage);
$localPath = $this->downloader->download($message->url, $message->destinationPath);
$this->processor->process($localPath);
}
}
```

### 4. Send (produce/push) a message to a queue
### 3. Configure the queue

#### Configuration with [yiisoft/config](https://github.com/yiisoft/config)

**If you use [yiisoft/app](https://github.com/yiisoft/app) or [yiisoft/app-api](https://github.com/yiisoft/app-api)**

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.
_If your project structure differs, put it into any params config file that is loaded by [yiisoft/config](https://github.com/yiisoft/config)._

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.
Minimal configuration example:

```php
return [
'yiisoft/queue' => [
'handlers' => [
DownloadFileMessage::TYPE => RemoteFileHandler::class,
],
],
];
```

[Advanced configuration with `yiisoft/config`](docs/guide/en/configuration-with-config.md)

#### Manual configuration

final readonly class Foo {
For setting up all classes manually, see the [Manual configuration](docs/guide/en/configuration-manual.md) guide.

### 4. Send (produce/push) a message to a queue

To send a message to the queue, get the queue instance and call `push()`. Typically the queue is injected as a dependency:

```php
final readonly class Foo
{
public function __construct(private QueueInterface $queue) {}

public function bar(): void
{
$this->queue->push(new Message(
// The first parameter is the message type used to resolve the handler which will process the message
RemoteFileHandler::class,
// The second parameter is the data that will be passed to the handler.
// It should be serializable to JSON format
['url' => 'https://example.com/file-path.csv'],
$this->queue->push(new DownloadFileMessage(
url: 'https://example.com/file-path.csv',
destinationPath: '/tmp/file-path.csv',
));
}
}
Expand Down
5 changes: 2 additions & 3 deletions docs/guide/en/configuration-manual.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,7 @@ $logger = new NullLogger(); // replace with your PSR-3 logger in production

// Define message handlers
$handlers = [
'file-download' => [FileDownloader::class, 'handle'],
FileDownloader::class => [FileDownloader::class, 'handle'],
DownloadFileMessage::TYPE => [FileDownloader::class, 'handle'],
];

$callableFactory = new CallableFactory($container);
Expand Down Expand Up @@ -79,7 +78,7 @@ $queue = new Queue(
);

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

Expand Down
44 changes: 42 additions & 2 deletions docs/guide/en/message-handler-advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,52 @@ Handler definitions are configured in:

### Handlers mapped by short message type

Use a short stable message type when pushing a `Message` instead of a PHP class name:
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:

```php
use Yiisoft\Queue\Message\Message;

new Message('send-email', ['data' => '...']); // "send-email" is the message type here
final class SendEmailMessage extends Message
{
public const TYPE = 'send-email';

public function __construct(
public readonly string $to,
public readonly string $subject,
public readonly string $body,
) {}

public static function fromData(string $type, mixed $data): static
{
if ($type !== self::TYPE) {
throw new \InvalidArgumentException("Expected type \"" . self::TYPE . "\", got \"$type\".");
}
if (!is_array($data)
|| !is_string($data['to'] ?? null)
|| !is_string($data['subject'] ?? null)
|| !is_string($data['body'] ?? null)
) {
throw new \InvalidArgumentException('Invalid data for ' . self::class . '.');
}
return new self($data['to'], $data['subject'], $data['body']);
}

public function getType(): string
{
return self::TYPE;
}

public function getData(): array
{
return ['to' => $this->to, 'subject' => $this->subject, 'body' => $this->body];
}
}
```

```php
new SendEmailMessage('user@example.com', 'Welcome', 'Thank you for registering.');
// getType() returns "send-email" — used by the worker to look up the handler
```

**Config**:
Expand Down
29 changes: 28 additions & 1 deletion docs/guide/en/message-handler.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,34 @@ If your handler implements `Yiisoft\Queue\Message\MessageHandlerInterface`, you
**Message**:

```php
new \Yiisoft\Queue\Message\Message(\App\Queue\RemoteFileHandler::class, ['url' => '...']);
use Yiisoft\Queue\Message\Message;

final class RemoteFileMessage extends Message
{
public function __construct(public readonly string $url) {}

public static function fromData(string $type, mixed $data): static
{
if (!is_array($data) || !is_string($data['url'] ?? null)) {
throw new \InvalidArgumentException('Invalid data for ' . self::class . '.');
}
return new self($data['url']);
}

public function getType(): string
{
return \App\Queue\RemoteFileHandler::class;
}

public function getData(): array
{
return ['url' => $this->url];
}
}
```

```php
new RemoteFileMessage('https://...');
```

**Handler**:
Expand Down
76 changes: 62 additions & 14 deletions docs/guide/en/messages-and-handlers.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,18 @@ This separation is intentional and important. Understanding it will save you fro

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

```
Producer side Consumer side
───────────────────────────── ──────────────────────────────────
new Message('send-email', …) →→→ Worker resolves handler → handles
(payload only) (logic only)
```mermaid
flowchart LR
subgraph Producer["Producer side"]
Message["new SendEmailMessage(...)\n(payload only)"]
end

subgraph Consumer["Consumer side"]
Worker["Worker resolves handler"]
Handler["Handler handles\n(logic only)"]
end

Message --> Worker --> Handler
```

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.
Expand All @@ -30,21 +37,62 @@ This means the producer and consumer can be:

## Message: payload only

A message carries just enough data to perform the work:
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.
Defining a dedicated class for each message type makes your code
self-documenting and type-safe:

```php
use Yiisoft\Queue\Message\Message;

final class SendEmailMessage extends Message
{
public const TYPE = 'send-email';

public function __construct(
public readonly string $to,
public readonly string $subject,
public readonly string $body,
) {}

public static function fromData(string $type, mixed $data): static
{
if ($type !== self::TYPE) {
throw new \InvalidArgumentException("Expected type \"" . self::TYPE . "\", got \"$type\".");
}
if (!is_array($data)
|| !is_string($data['to'] ?? null)
|| !is_string($data['subject'] ?? null)
|| !is_string($data['body'] ?? null)
) {
throw new \InvalidArgumentException('Invalid data for ' . self::class . '.');
}
return new self($data['to'], $data['subject'], $data['body']);
}

public function getType(): string
{
return self::TYPE;
}

public function getData(): array
{
return ['to' => $this->to, 'subject' => $this->subject, 'body' => $this->body];
}
}
```

Usage:

```php
new \Yiisoft\Queue\Message\Message('send-email', [
'to' => 'user@example.com',
'subject' => 'Welcome',
]);
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 **data payload** — arbitrary data the handler needs. Must be serializable.
- A **data payload** — typed properties serialized to JSON via `getData()`. Must be JSON-encodable.

The message has no methods, no business logic, no dependencies. It is a value object — a data wrapper.
The message has no business logic, no dependencies. It is a value object — a typed data wrapper.

## Handler: logic only

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

public function handle(\Yiisoft\Queue\Message\MessageInterface $message): void
{
$data = $message->getData();
$this->mailer->send($data['to'], $data['subject']);
assert($message instanceof SendEmailMessage);
$this->mailer->send($message->to, $message->subject, $message->body);
}
}
```
Expand Down
6 changes: 2 additions & 4 deletions docs/guide/en/queue-names.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ Pushing a message via DI:

```php
use Yiisoft\Queue\QueueInterface;
use Yiisoft\Queue\Message\Message;

final readonly class SendWelcomeEmail
{
Expand All @@ -78,7 +77,7 @@ final readonly class SendWelcomeEmail

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

```php
use Yiisoft\Queue\Provider\QueueProviderInterface;
use Yiisoft\Queue\Message\Message;

final readonly class SendTransactionalEmail
{
Expand All @@ -117,7 +115,7 @@ final readonly class SendTransactionalEmail
{
$this->queueProvider
->get('emails')
->push(new Message('send-email', ['to' => $email]));
->push(new SendEmailMessage(to: $email, subject: 'Welcome!', body: 'Thank you for registering.'));
}
}
```
Expand Down
Loading
Loading