Lead Software Engineer at Mumsnet .

Published on

Laravel Command Bus

In Laravel 5.1 the Command Bus was replaced with Dispatchable Jobs, we can still use them but let us also look at how to add a Command Bus.

To start using a command bus in Laravel, first we need an interface to bind to, because let's be honest this is just good practice. For now we are going to stick to adding all our custom code inside of our App namespace, although in reality I would usually have this sort of code inside a src directory.

Create a directory called app/CommandBus/Contracts and we will start here by creating a CommandContract which should look like the following:

3namespace App\CommandBus\Contracts;
5interface CommandContract
7 // nothing is needed in here as most of what we need is constructor properties

This will be the interface/contract for our Commands within our application, next we need to think about how we want to handle these. Commands are handled by CommandHandlers which is a little like the handle method on a Dispatchable Job in Laravel. However the key difference is that the only purpose of this class is to handle a command event. Next we will create app/CommandBus/Contracts/CommandHandlerContract which should look like the below:

3namespace App\CommandBus\Contracts;
5interface CommandHandlerContract
7 public function handle(CommandContract $command): mixed;

What we are doing here is accepting a command that implements the CommandContract and we return mixed. THe reason we are returning mixed over anything else is because not all commands need to return anything, and we can't guarantee the return response of the command. We could take it a step further and create a custom CommandResponse class - but that for now would be overkill.

Our final interface/contract that we need to create is for the Command Bus itself, so let us create app/CommandBus/CommandBusContract.php and make it look like the following example:

3namespace App\CommandBus\Contracts;
5interface CommandBusContract
7 public function dispatch(CommandContract $command): mixed;
9 /**
10 * @param array<class-string<CommandContract>,class-string<CommandHandlerContract>> $map
11 * @return void
12 */
13 public function map(array $map): void;

From the above you can see that we have a dispatch method and a map method, the map is to map new command and handlers into our bus and dispatch handles dispatching the command itself.

So now we are at a point where we have an interface/contract for the major components for our application, our next step is to design the implementation for how we want to build out command bus for Laravel.

Our command bus will need to accept the Laravels bus Dispatcher to dispatch these commands nicely, so let us create app/CommandBus/Adapters/Laravel/LaravelCommandBus.php and make it look like the following:

3namespace App\CommandBus\Adapters\Laravel;
5use App\CommandBus\Contracts\CommandBusContract;
6use App\CommandBus\Contracts\CommandBusContract;
7use Illuminate\Bus\Dispatcher;
9final class LaravelCommandBus implements CommandBusContract
11 public function __construct() {
12 private Dispatcher $bus,
13 }
15 public function dispatch(CommandContract $command): mixed
16 {
17 return $this->bus->dispatch($command);
18 }
20 public function map(array $map): void
21 {
22 $this->bus->map($map);
23 }

Now that we have a command bus implementation, we need to bind this into our container so that we can inject the correct implementation inside our code when we need it. Let us create a new service provider called CommandBusServiceProvider using artisan:

1php artisan make:provider CommandBusServiceProvider

This will generate the scaffold of the class, and now we need to create the actual service provider:

3namespace App\Providers;
5use App\CommandBus\Adapters\Laravel\LaravelCommandBus;
6use App\CommandBus\Contracts\CommandBus;
7use Illuminate\Support\ServiceProvider;
9final class CommandBusServiceProvider extends ServiceProvider
11 public function register(): void
12 {
13 $this->app->singleton(
14 abstract: CommandBus::class,
15 concrete: LaravelCommandBus::class,
16 );
17 }

We now have a way to tell Laravel which command bus we want to use within our application, our next step is to register this inside config/app.php under the providers array. Once we have done this we need to find ways to register the commands we want to use. This is where things can take a turn, as there are many ways in which you might want to be able to do this. This will mostly depend on your application architecture, so for this example I will use a modular architecture, where we have a src/Modules directory where we register our modules.

Let us imagine this is a Customer Relationship Management system, and we will register a few command into the command bus. Create a new service provider called src/Modules/Clients/Providers/ClientCommandBusServiceProvider.php and add the following code:

3namespace Modules\Clients\Providers;
5use App\CommandBus\Contracts\CommandBusContract;
6use Illuminate\Support\ServiceProvider;
8final class ClientCommandBusServiceProvider extends ServiceProvider
10 public function register(): void
11 {
12 /**
13 * @var CommandBusContract $commandBus
14 */
15 $commandBus = resolve(CommandBusContract::class);
17 $commandBus->map([
18 CreateClientCommand::class => CreateClientCommandHandler::class,
19 ]);
20 }

We can register this service provider in whichever way we need to, my personal way would be to create a service provider per module and have that register additional providers. Either way, you will need to make sure that this service provider is registered with our application in one way or another.

We can now look at creating these two classes, firstly let us look at our command itself. It is called CreateClientCommand which is pretty self explainitory, it needs all the data required to create a new client. Typically I would create a Data Transfer Object here so that I can standardise the input and have type validation handle part of the work for me. Create a new class src/Modules/Clients/Commands/CreateClientCommand.php with the following code:

3namespace Modules\Clients\Commands;
5use App\CommandBus\Contracts\CommandContract;
6use Modules\Clients\DataObjects\NewClientDataObject;
8final class CreateClientCommand implements CommandContract
10 public function __construct(
11 public readonly NewClientDataObject $client,
12 ) {}

What we have is a command that implements the correct interface/contract and the constructor accepts a data transfer object for us to get data from. This data object is set as readonly using PHP 8.1 to ensure that the object is immutable, this is something we want to ensure in the command.

Next we should look at how we want to handle this command itself, before we worry about implementing this command bus. So let us create a new class src/Modules/Clients/CommandHandlers/CreateClientCommandHandler.php and add the following code to it:

3namespace Modules\Clients\CommandHandlers;
5use App\Models\Client;
6use App\CommandBus\Contracts\CommandContract;
7use App\CommandBus\Contracts\CommandHandlerContract;
9final class CreateClientCommandHandler implements CommandHandlerContract
11 public function handle(CommandContract $command): mixed
12 {
13 return Client::query()->create(
14 attributes: $command->client->toArray(),
15 );
16 }

As you can see the only thing we are doing within this handler is interacting with Eloquent to create a new Client, using to data transfer object to cast the properties to an array so eloquent will accept it. This could be using something like the repository pattern or a service class if that is how you would like to do it, but the end result should be the same.

Finally we get to the point where we want to implement running our command. So create a new controller/handler to handle an incoming request, for this example I will use it as if it were an API to make things simpler. Create a new class app/Http/Controllers/API/V1/Clients/StoreController.php and add the following code:

3namespace App\Http\Controllers\API\V1\Clients;
5use App\CommandBus\Contracts\CommandBusContract;
6use App\CommandBus\Contracts\CreateClientCommand;
7use App\Http\Resources\API\V1\ClientResource;
8use App\Http\Requests\API\V1\Clients\StoreRequest;
9use Illuminate\Http\JsonResponse;
10use Modules\Clients\DataObjects\ClientDataObject;
12final class StoreController
14 public function __construct(
15 private readonly CommandBusContract $bus,
16 ) {}
18 public function __invoke(StoreRequest $request)
19 {
20 $client = $this->bus->dispatch(
21 command: new CreateClientCommand(
22 client: new ClientDataObject(
23 attributes: array_merge(
24 $request->validated(),
25 ['user_id' => auth()->id()]
26 ),
27 )
28 ),
29 );
31 return new JsonResponse(
32 data: new ClientResource(
33 resource: $client,
34 ),
35 status: 201
36 );
37 }

As you can see above, we accept the CommandBusContract into the constructor which will allow the Laravel container to resolve the correctly bound implementation. The when our request is being handled we pass in a StoreRequest which handled the validation of the request, allowing us to trust the data being passed to our request handler. We then tell the command bus to dispatch the correct command, passing in a data object which we build up by passing in the validated data and the currently authenticated users ID. Once we have our eloquent model back we pass this over to a JsonResponse using a ClientResource to trasform the models data into what our API should return. Finally we pass back a Http status code 201 which signals that the resource was infact created.

As you can see from this relatively simple implementation and usage, using a command bus is quite simple in Laravel. It isn't the only option we have available in our application, but it is a sounds and proven approach to application architecture. In future blog posts I will investigate other ways in which we can architect our application. The benefits are fantastic to these approaches as each class has a single responsibility and as we are leaning heavily on our DI container, we can create mock instances of these classes in our tests very simply.

Did you like this article? Let me know on twitter! I love feedback, and if you think I could achieve the same result in a different way let me know!