Skip to content

Command Bus

The Command Bus dispatches commands for write operations through a middleware pipeline to registered handlers.

Commands are simple DTOs representing write operations:

final readonly class CreateUserCommand
{
public function __construct(
public string $email,
public string $name,
public ?string $password = null,
) {}
}

Use readonly classes to ensure immutability. Commands should be pure data containers.

Handlers process commands and perform the actual work:

use Cline\MessageBus\Commands\Attributes\AsCommandHandler;
#[AsCommandHandler(CreateUserCommand::class)]
final readonly class CreateUserHandler
{
public function __construct(
private UserRepository $repository,
private PasswordHasher $hasher,
) {}
public function handle(CreateUserCommand $command): User
{
return $this->repository->create([
'email' => $command->email,
'name' => $command->name,
'password' => $this->hasher->hash($command->password),
]);
}
}

The #[AsCommandHandler] attribute enables automatic discovery.

use Cline\MessageBus\Facades\CommandBus;
$user = CommandBus::dispatch(new CreateUserCommand(
email: 'user@example.com',
name: 'John Doe',
password: 'secret123',
));
use Cline\MessageBus\Commands\Contracts\CommandBusInterface;
class UserController
{
public function __construct(
private CommandBusInterface $commandBus,
) {}
public function store(Request $request): Response
{
$user = $this->commandBus->dispatch(
new CreateUserCommand(
email: $request->email,
name: $request->name,
)
);
return response()->json($user);
}
}

Some commands don’t return anything:

final readonly class SendWelcomeEmailCommand
{
public function __construct(
public string $userId,
) {}
}
#[AsCommandHandler(SendWelcomeEmailCommand::class)]
final readonly class SendWelcomeEmailHandler
{
public function __construct(
private MailerInterface $mailer,
) {}
public function handle(SendWelcomeEmailCommand $command): void
{
$this->mailer->send(new WelcomeEmail($command->userId));
}
}

Validate inputs in the command constructor:

final readonly class CreateUserCommand
{
public function __construct(
public string $email,
public string $name,
) {
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('Invalid email address');
}
}
}

This ensures commands are always valid before reaching the handler.

Optionally extend the base class for type hints:

use Cline\MessageBus\Commands\Support\AbstractCommand;
final readonly class CreateUserCommand extends AbstractCommand
{
public function __construct(
public string $email,
public string $name,
) {}
}

For handlers that process multiple commands:

final readonly class UserHandler
{
#[AsCommandHandler(CreateUserCommand::class)]
public function handleCreate(CreateUserCommand $command): User
{
// Create user
}
#[AsCommandHandler(UpdateUserCommand::class)]
public function handleUpdate(UpdateUserCommand $command): User
{
// Update user
}
}

The discovery system detects both class-level and method-level attributes.