Laravel Advanced CQRS

Sep 17, 2022 10 minutes read
A toy car on table - Alena Darmel (pexels.com)

In the previous post, we built the CQRS from scratch. In this article, I will try to show you a few ways to extend it to achieve things like: transactions, logging, failover and a few more. Let's see how we can tweak our CQRS! Again, the Laravel does not provide any support here (for CQRS), because it's more about our logic than framework way. But still, it will be very simple to extend what we've made before and will provide a lot of benefits for our application.

BTW If you would like to check the previous article, please follow this link Laravel CQRS from scratch.

#1 Handle multiple commands at once

Problem

The CommandBus is very simple it takes just one argument $command which is a Plain Old PHP Object (POPO). But what if we'd like to pass more commands to be processed at once? For example you want to execute some EditCommand and SubmitCommand.

public function handle($command)
{
    // resolve handler
    $reflection = new ReflectionClass($command);
    $handlerName = str_replace("Command", "Handler", $reflection->getShortName());
    $handlerName = str_replace($reflection->getShortName(), $handlerName, $reflection->getName());
    $handler = App::make($handlerName);
    // invoke handler
    $handler($command);
}

Solution

We need to change a bit the logic here. We could create another method like handleMultiple and it would take an array as a argument OR we could keep just one method handle and put some logic to detect if the argument is an array or object. Do not know why, but for now I'd prefer the second solution.

public function handle($commands)
{
    if (!is_array($commands)) {
        $commands = [$commands];
    }
    foreach ($commands as $command) {
        // resolve handler
        $reflection = new ReflectionClass($command);
        $handlerName = str_replace("Command", "Handler", $reflection->getShortName());
        $handlerName = str_replace($reflection->getShortName(), $handlerName, $reflection->getName());
        $handler = App::make($handlerName);
        // invoke handler
        $handler($command);  
    }
}

 

#2 Transactions

Problem

Now we can run multiple commands, but what happen if there was an exception during the execution? Literally it will cause a partial update or data lose in our application. That's why we should use transactions to protect the write moment. Thanks to most RDBMS we have the ACID.

Solution

The solution is simple, we need to start the transaction before we call any handler in the CommandBus. In Laravel it's very simple, we can call beginTransaction() or use DB::transaction before the $handler($command) in the CommandBus. There is one thing you should pay attention to. The transaction will be opened in the moment you call handle($commands) method. It depends on what kind of transaction isolation level you use, but it means some data may be locked during the processing of some long running commands. Maybe there are better solutions here, but for me it works just fine. Here is the extended version of previous handle method.

public function handle($commands)
{
    if (!is_array($commands)) {
        $commands = [$commands];
    }

    $exception = null;
    try {
        DB::transaction(function () use ($commands) {
            $exception = null;
            foreach ($commands as $command) {
                try {
                    if (is_null($exception)) {
                        // resolve handler
                        $reflection = new ReflectionClass($command);
                        $handlerName = str_replace("Command", "Handler", $reflection->getShortName());
                        $handlerName = str_replace($reflection->getShortName(), $handlerName, $reflection->getName());
                        $handler = App::make($handlerName);
                        // invoke handler
                        $handler($command);
                    }
                } catch (Exception $e) {
                    $exception = $e;
                }
            }
            if ($exception) {
                throw $exception;
            }
        });
    } catch (\Exception $commandsException) {
        $exception = $commandsException;
    }

    // finally if there was an exception
    // let's throw it to previous layer
    if ($exception) {
        throw $exception;
    }
}

What it does? It is still the same foreach, but we wrapped the $handler($command) in transaction. Then if there was any exception threw, we catch it and loop through all other commands, but without calling them. After the loop we throw it above to try because we want to throw it again to be caught by higher try. Yeah it looks a little complex, but we must do that, because we relay on rollback logic here 🏄. And finally we check if there was an exception, then it throws it to the previous layer where the method handle was called. By doing that, we are sure, that no command was committed in the application - it happens because any exception in the transaction clojure will cause an immediate rollback.

#3 Logs

Problem

As we seen above, there is no default way to guarantee that each command was successfully executed. The execution errors might be related to missing argument, some business rules, validations etc. For example in our app we could have multiple layers, and multiple different exception types (like ApplicationException, DomainException, ServiceException, ValidationException and so on). The problem with current solution is we are missing valuable information about our system. We want to track all information of all our commands in the system, to get know what's going on in our application.

Solution

First we need to create a database table were we will store all the logs. Create a new migration file with this code:

Schema::create('command_bus_log', function (Blueprint $table) {
    $table->bigIncrements('id');
    $table->string('command_name', 700)->nullable();
    $table->json('command_params')->nullable();
    $table->string('user_id')->nullable()->index();
    $table->boolean('has_started');
    $table->boolean('is_committed');
    $table->text('error_message')->nullable();
    $table->dateTime('created_at')->nullable();
});

I think that all the columns are pretty-descriptive. If you need to store additional information, you can extend this migration. In this table we will store:

  • id - the auto-increment ID
  • group_uuid - the uuid will represent the unique identifier for command's group
  • command_name - the FQN of the command
  • command_params - all attributes from the command class
  • user_id - it's optional, but it's good to have an information what user triggered the action - I suggest to keep it in the code
  • has_started - it will inform if the command was executed or not
  • is_committed - this flag will be used to get know if the transaction was successfully committed in our app
  • error_message - here we will store the output from $exception->getMessage()
  • created_at - the date time when the command happened

Okay, let's run the migration with Laravel artisan command

php artisan migrate

Now let's create a method which will extract all attributes from the class. We will use ReflectionClass utilities for that. Let's add a new method to our CommandBus.

private function getCommandAttributes($command)
{
    $reflection = new ReflectionClass($command);

    $reflectionProperties = $reflection->getProperties();
    $params = [];
    foreach ($reflectionProperties as $reflectionProperty) {
        $reflectionProperty->setAccessible(true);
        $params[$reflectionProperty->getName()] = $reflectionProperty->getValue($command);
    }

    return json_encode($params);
}

Because we will gather information about command executing in multiple places, we need to create a dedicated method to store stats about command. Here we will use the method which created above getCommandAttributes. Let's add new method to our CommandBus.

private function generateCommandStats(
    string $uuid,
    $command,
    bool $hasStarted,
    bool $isCommitted,
    ?string $errorMessage,
    ?User $user
): array
{
    return [
        'group_uuid' => $uuid,
        'command_name' => get_class($command),
        'command_params' => $this->getCommandAttributes($command),
        'user_id' => $user?->getAuthIdentifier(),
        'has_started' => $hasStarted,
        'is_committed' => $isCommitted,
        'error_message' => $errorMessage,
        'created_at' => now(),
    ];
}

The final step here is to update our handle method in CommandBus. It will become a bit longer, so please spend some time to check the code carefully. Here is the final version:

public function handle($commands)
{
    if (!is_array($commands)) {
        $commands = [$commands];
    }

    $user = Auth::user();
    $executedCommands = [];
    $uuid = Str::uuid();
    $exception = null;
    try {
        DB::transaction(function () use ($commands, $user, &$executedCommands, $uuid) {
            $exception = null;
            foreach ($commands as $command) {
                try {
                    if (is_null($exception)) {
                        // resolve handler
                        $reflection = new ReflectionClass($command);
                        $handlerName = str_replace("Command", "Handler", $reflection->getShortName());
                        $handlerName = str_replace($reflection->getShortName(), $handlerName, $reflection->getName());
                        $handler = App::make($handlerName);
                        // invoke handler
                        $handler($command);

                        $executedCommands[] =
                            $this->generateCommandStats(
                                $uuid,
                                $command,
                                true,
                                true,
                                null,
                                $user
                            );
                    }
                    else {
                        $executedCommands[] = $this->generateCommandStats(
                            $uuid,
                            $command,
                            false,
                            false,
                            null,
                            $user
                        );
                    }
                } catch (Exception $e) {
                    $exception = $e;
                    $executedCommands[] = $this->generateCommandStats(
                        $uuid,
                        $command,
                        true,
                        false,
                        $exception->getMessage(),
                        $user
                    );
                }
            }
            if ($exception) {
                throw $exception;
            }
        });
    } catch (\Exception $commandsException) {
        $exception = $commandsException;
    }

    DB::table('command_bus_log')->insert($executedCommands);

    // finally if there was an exception
    // let's throw it to previous layer
    if ($exception) {
        throw $exception;
    }
}

With that simple logging mechanism you created a really good way of detecting what your users are doing in the system. For some systems that kind of log is mandatory because of data retention and audit related things.

#4 Returning data in CQRS

Problem

As you probably know one of the principle in CQS and CQRS is that we shouldn't return anything from the Command Handler. Command handlers should be void type and the return value should not be taken by previous layers as this violates its rules. Well I like that idea, but there are moments when we need to get something back from the handler. For example we need to get back the ID of a new created object or added comment or whatever we need.

Your probably know that the initial term CQS comes from '80, later on it has been improved into CQRS. I don't mean the logic get expired. But as you can imagine, the systems of the '80s certainly had different UIs that were not as the ones we have today.

Also there might be another issue if your commands are executed in asynchronous way (you may have to return the id of queued command).

Solution

As I mentioned, I like the idea about old-school CQRS, but my projects have some other needs. I have to find a way how to get back the identifier of a new created object. Here is an example how we can do that.

First, let's create a new class called Envelope. In the constructor there will be just one argument  data and the class will offer only one public method getData to get the data passed in the constructor.

<?php

namespace App;


class Envelope
{
    private $data;
    /**
     * Envelope constructor.
     * @param $data
     */
    public function __construct($data)
    {
        $this->data = $data;
    }

    /**
     * @return mixed
     */
    public function getData()
    {
        return $this->data;
    }
}

Now we need to update the handle method in the CommandBus. We will create a new variable $envelopeFromHandler and will assign the result from the handler. The change is simple. Just find all occurrences in the code and add that variable in proper places.

public function handle($commands)
{
    if (!is_array($commands)) {
        $commands = [$commands];
    }

    $user = Auth::user();
    $executedCommands = [];
    $uuid = Str::uuid();
    $exception = null;
    $envelopeFromHandler = null;
    try {
        DB::transaction(function () use ($commands, $user, &$executedCommands, $uuid, &$envelopeFromHandler) {
            // ...
            $envelopeFromHandler = $handler($command);
            // ...
        });
    } catch (\Exception $commandsException) {
        $exception = $commandsException;
    }

    // ...

    if (count($commands) === 1 && $envelopeFromHandler) {
        return $envelopeFromHandler;
    }
}

As you can see at the end of handle method there is a check count($commands). It means that envelope is only available when there is only one command to be processed. For most cases/scenarios that's totally enough. But if you want to support multiple envelops from multiple commands at once you need probably to generate some command id before calling handle method in CommandBus.

Let's see how to return the envelope from the CommandHandler.

<?php

namespace App\Commands;

use App\Models\Product;
use App\Envelope;

class CreateProductHandler
{
    public function __invoke(CreateProductCommand $command)
    {
    	$product = new Product();
    	$product->name = $command->getName();
    	$product->price = $command->getPrice();
    	$product->save();
    	// other logic, eg queue slack notification, dispatch event etc.
    	return new Envelope($product->id);
    }
}

And in the Controller it's even easier:

<?php

namespace App\Http\Controllers;

use App\CommandBus;
use App\Commands\CreateProductCommand;
use App\Queries\ProductSimpleQuery;

class ProductController extends Controller
{
    private $commandBus;

    public function __construct(CommandBus $commandBus)
    {
        $this->commandBus = $commandBus;
    }

    public function create()
    {
        // $this->authorize('...')
        $name = request()->query('name');
        $price = request()->query('price');
        // validation goes here ...
        $command = new CreateProductCommand($name, $price);
        $envelope = $this->commandBus->handle($command);
		
		return response()->json([
            'message' => 'success',
            'product_id' => $envelope?->getData(),
        ]);
    }
}

 

Closing notes

If you are reading this paragraph, thank you first for taking the time to read this post.  And I hope that my explanations were approachable and the CQRS approach itself will make your projects easier to manage and more pleasant to work on.

As a reminder, if you are looking for the previous article on the basics of CQRS, you can find it at this link Laravel CQRS From Scratch.

Enjoy!

Disclaimer: The opinions expressed here are my own and do not necessarily represent those of current or past employers.
Comments (0)