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
21 changes: 19 additions & 2 deletions docs/2-features/10-command-bus.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ final readonly class UserController
The asynchronous commands implementation of Tempest is currently experimental. Although you can use it, please note that it is not covered by our backwards compatibility promise.
:::

A common use case for Tempest's command bus is to dispatch asynchronous commands: commands that are executed by their handler in the background, outside the main PHP process. Making a command asynchronous is done by adding the `#[Async]` to your command object:
A common use case for Tempest's command bus is to dispatch asynchronous commands: commands that are executed by their handler in the background, outside the main PHP process. Making a command asynchronous is done by adding the `#[Async]` attribute to the command object:

```php
// app/SendMail.php
Expand All @@ -100,7 +100,24 @@ final readonly class SendMail
}
```

Besides adding the `#[Async]` attribute, the flow remains exactly the same as if you were dispatching synchronous commands:
The attribute may instead be placed on the command handler:

```php
use Tempest\CommandBus\Async;
use Tempest\CommandBus\CommandHandler;

final readonly class SendMailHandler
{
#[Async]
#[CommandHandler]
public function __invoke(SendMail $command): void
{
// Send mail…
}
}
```

Regardless of where the `#[Async]` attribute is placed, the flow remains exactly the same as if you were dispatching synchronous commands:

```php
use function Tempest\command;
Expand Down
3 changes: 2 additions & 1 deletion packages/command-bus/src/AsyncCommandMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@
{
public function __construct(
private CommandRepository $repository,
private CommandBusConfig $commandBusConfig,
) {}

public function __invoke(object $command, CommandBusMiddlewareCallable $next): void
{
$reflector = new ClassReflector($command);

if ($reflector->hasAttribute(Async::class)) {
if ($reflector->hasAttribute(Async::class) || ($this->commandBusConfig->handlers[$command::class] ?? null)?->handler->hasAttribute(Async::class)) {
$this->repository->store(Random\uuid(), $command);

return;
Expand Down
9 changes: 9 additions & 0 deletions tests/Fixtures/Handlers/MyAsyncCommandHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
namespace Tests\Tempest\Fixtures\Handlers;

use Exception;
use Tempest\CommandBus\Async;
use Tempest\CommandBus\CommandHandler;
use Tests\Tempest\Integration\CommandBus\Fixtures\MyAsyncCommand;
use Tests\Tempest\Integration\CommandBus\Fixtures\MyCommandWithAsyncHandler;
use Tests\Tempest\Integration\CommandBus\Fixtures\MyFailingAsyncCommand;

final class MyAsyncCommandHandler
Expand All @@ -19,6 +21,13 @@ public function onMyAsyncCommand(MyAsyncCommand $command): void
self::$isHandled = true;
}

#[Async]
#[CommandHandler]
public function onMyCommandWithAsyncHandler(MyCommandWithAsyncHandler $command): void
{
self::$isHandled = true;
}

#[CommandHandler]
public function onMyFailingAsyncCommand(MyFailingAsyncCommand $command): void
{
Expand Down
26 changes: 26 additions & 0 deletions tests/Integration/CommandBus/AsyncCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Tempest\Highlight\Themes\TerminalStyle;
use Tests\Tempest\Fixtures\Handlers\MyAsyncCommandHandler;
use Tests\Tempest\Integration\CommandBus\Fixtures\MyAsyncCommand;
use Tests\Tempest\Integration\CommandBus\Fixtures\MyCommandWithAsyncHandler;
use Tests\Tempest\Integration\FrameworkIntegrationTestCase;

use function Tempest\CommandBus\command;
Expand Down Expand Up @@ -45,6 +46,31 @@ public function test_async_commands_are_stored_and_handled_afterwards(): void
$this->assertTrue(MyAsyncCommandHandler::$isHandled);
}

public function test_commands_with_async_handlers_are_stored_and_handled_afterwards(): void
{
$repository = new MemoryRepository();

$this->container->singleton(
CommandRepository::class,
fn () => $repository,
);

MyAsyncCommandHandler::$isHandled = false;

command(new MyCommandWithAsyncHandler('Brent'));

$pendingCommands = arr($repository->getPendingCommands());

$this->assertCount(1, $pendingCommands);
$command = $pendingCommands->first();
$this->assertSame('Brent', $command->name);
$this->assertFalse(MyAsyncCommandHandler::$isHandled);

$this->console->call('command:handle ' . $pendingCommands->keys()->first());

$this->assertTrue(MyAsyncCommandHandler::$isHandled);
}

public function test_async_command_monitor(): void
{
$process = new Process(['php', 'tempest', 'command:monitor']);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace Tests\Tempest\Integration\CommandBus\Fixtures;

final readonly class MyCommandWithAsyncHandler
{
public function __construct(
public string $name,
) {}
}
Loading