Skip to content

Handler Discovery

Automatic handler discovery uses PHP attributes to map commands and queries to their handlers without manual registration.

The discovery system:

  1. Scans the Composer classmap for classes in the Monolith\ namespace
  2. Filters to handler directories (/Application/Command/Handlers/ or /Application/CommandHandler/)
  3. Reads #[AsCommandHandler] and #[AsQueryHandler] attributes
  4. Builds a map of message classes to handler classes

Mark a class or method as a command handler:

use Cline\MessageBus\Commands\Attributes\AsCommandHandler;
#[AsCommandHandler(CreateUserCommand::class)]
final readonly class CreateUserHandler
{
public function handle(CreateUserCommand $command): User
{
// ...
}
}

Mark a class or method as a query handler:

use Cline\MessageBus\Queries\Attributes\AsQueryHandler;
#[AsQueryHandler(GetUserByIdQuery::class)]
final readonly class GetUserByIdHandler
{
public function handle(GetUserByIdQuery $query): ?User
{
// ...
}
}

Attributes work at both class and method level:

#[AsCommandHandler(CreateUserCommand::class)]
final readonly class CreateUserHandler
{
public function handle(CreateUserCommand $command): User
{
// Handler maps to: CreateUserHandler::class
}
}
final readonly class UserCommandHandler
{
#[AsCommandHandler(CreateUserCommand::class)]
public function handleCreate(CreateUserCommand $command): User
{
// Handler maps to: UserCommandHandler::class . '@handleCreate'
}
#[AsCommandHandler(UpdateUserCommand::class)]
public function handleUpdate(UpdateUserCommand $command): User
{
// Handler maps to: UserCommandHandler::class . '@handleUpdate'
}
}

A single handler can handle multiple messages:

#[AsCommandHandler(CreateUserCommand::class)]
#[AsCommandHandler(ImportUserCommand::class)]
final readonly class CreateUserHandler
{
public function handle(object $command): User
{
// Handle both command types
}
}

Discovery scans these patterns within the Monolith\ namespace:

src/
└── Application/
├── Command/
│ └── Handlers/
│ ├── CreateUserHandler.php
│ └── UpdateUserHandler.php
└── Query/
└── Handlers/
├── GetUserByIdHandler.php
└── ListUsersHandler.php
src/
└── Application/
├── CommandHandler/
│ ├── CreateUserCommandHandler.php
│ └── UpdateUserCommandHandler.php
└── QueryHandler/
├── GetUserByIdQueryHandler.php
└── ListUsersQueryHandler.php

Both conventions are supported simultaneously for migration purposes.

In local development (APP_ENV=local), discovery runs on every request. This provides:

  • Automatic registration of new handlers
  • No cache management required
  • Instant feedback during development

For production, cache the handler map to avoid runtime reflection:

Terminal window
php artisan handlers:cache

This generates:

  • bootstrap/cache/command-handlers.php
  • bootstrap/cache/query-handlers.php

The service provider loads these cached maps at boot.

Customize cache paths in config/message-bus.php:

return [
'paths' => [
'command_handlers' => base_path('bootstrap/cache/command-handlers.php'),
'query_handlers' => base_path('bootstrap/cache/query-handlers.php'),
],
];

To clear the handler cache:

Terminal window
php artisan handlers:clear

Or delete the cache files manually.

The discovery system automatically skips:

  • Abstract classes
  • Interfaces
  • Classes outside /Application/ directories
  • Classes outside the Monolith\ namespace