Handler Discovery
Automatic handler discovery uses PHP attributes to map commands and queries to their handlers without manual registration.
How Discovery Works
Section titled “How Discovery Works”The discovery system:
- Scans the Composer classmap for classes in the
Monolith\namespace - Filters to handler directories (
/Application/Command/Handlers/or/Application/CommandHandler/) - Reads
#[AsCommandHandler]and#[AsQueryHandler]attributes - Builds a map of message classes to handler classes
Attributes
Section titled “Attributes”AsCommandHandler
Section titled “AsCommandHandler”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 { // ... }}AsQueryHandler
Section titled “AsQueryHandler”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 { // ... }}Attribute Targets
Section titled “Attribute Targets”Attributes work at both class and method level:
Class-Level (Recommended)
Section titled “Class-Level (Recommended)”#[AsCommandHandler(CreateUserCommand::class)]final readonly class CreateUserHandler{ public function handle(CreateUserCommand $command): User { // Handler maps to: CreateUserHandler::class }}Method-Level
Section titled “Method-Level”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' }}Multiple Attributes
Section titled “Multiple Attributes”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 }}Directory Conventions
Section titled “Directory Conventions”Discovery scans these patterns within the Monolith\ namespace:
Modern Convention
Section titled “Modern Convention”src/└── Application/ ├── Command/ │ └── Handlers/ │ ├── CreateUserHandler.php │ └── UpdateUserHandler.php └── Query/ └── Handlers/ ├── GetUserByIdHandler.php └── ListUsersHandler.phpLegacy Convention
Section titled “Legacy Convention”src/└── Application/ ├── CommandHandler/ │ ├── CreateUserCommandHandler.php │ └── UpdateUserCommandHandler.php └── QueryHandler/ ├── GetUserByIdQueryHandler.php └── ListUsersQueryHandler.phpBoth conventions are supported simultaneously for migration purposes.
Local Development
Section titled “Local Development”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
Production Caching
Section titled “Production Caching”For production, cache the handler map to avoid runtime reflection:
php artisan handlers:cacheThis generates:
bootstrap/cache/command-handlers.phpbootstrap/cache/query-handlers.php
The service provider loads these cached maps at boot.
Cache Configuration
Section titled “Cache Configuration”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'), ],];Clearing Cache
Section titled “Clearing Cache”To clear the handler cache:
php artisan handlers:clearOr delete the cache files manually.
Excluded Classes
Section titled “Excluded Classes”The discovery system automatically skips:
- Abstract classes
- Interfaces
- Classes outside
/Application/directories - Classes outside the
Monolith\namespace