diff --git a/docs/2-features/10-command-bus.md b/docs/2-features/10-command-bus.md index d3e4ed6ce1..e327d3b115 100644 --- a/docs/2-features/10-command-bus.md +++ b/docs/2-features/10-command-bus.md @@ -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 @@ -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; diff --git a/packages/command-bus/src/AsyncCommandMiddleware.php b/packages/command-bus/src/AsyncCommandMiddleware.php index 18366a0eaf..aa2c152281 100644 --- a/packages/command-bus/src/AsyncCommandMiddleware.php +++ b/packages/command-bus/src/AsyncCommandMiddleware.php @@ -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; diff --git a/tests/Fixtures/Handlers/MyAsyncCommandHandler.php b/tests/Fixtures/Handlers/MyAsyncCommandHandler.php index b9466e14d9..e5967e8907 100644 --- a/tests/Fixtures/Handlers/MyAsyncCommandHandler.php +++ b/tests/Fixtures/Handlers/MyAsyncCommandHandler.php @@ -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 @@ -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 { diff --git a/tests/Integration/CommandBus/AsyncCommandTest.php b/tests/Integration/CommandBus/AsyncCommandTest.php index 2899542c34..2b5513537d 100644 --- a/tests/Integration/CommandBus/AsyncCommandTest.php +++ b/tests/Integration/CommandBus/AsyncCommandTest.php @@ -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; @@ -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']); diff --git a/tests/Integration/CommandBus/Fixtures/MyCommandWithAsyncHandler.php b/tests/Integration/CommandBus/Fixtures/MyCommandWithAsyncHandler.php new file mode 100644 index 0000000000..81901a8e5d --- /dev/null +++ b/tests/Integration/CommandBus/Fixtures/MyCommandWithAsyncHandler.php @@ -0,0 +1,12 @@ +