DEV Community

loading...
Cover image for CQRS code example "Requested"

CQRS code example "Requested"

soulwife profile image Anastasiia Lysenko ・3 min read

This post is linked with the detailed article Real-life DDD in an "onionshell" and explains with code examples specific version of a standard CQRS command requests. This customization allows us to handle different types of command requests integrating with bounded contexts.

Our service has two types of API requests: external "usecase" type and "command request" type, that can be external and internal. The second type can be managed differently, depending on it’s goal. It can be a simple external request to handle a command. Another time it can be a complex task with transaction in it. So we should use a different approach for every version of the task according to the configuration pre-set. Either way we create a task request and manage it using a message broker.

We have "usecase" and "request-commands" concept. Last one should be used for any server-to-server or context-to-context interaction.
Request command should be created via CommandRunner (RequestCommandHandlerService), that operates with command handlers and have the following interface:

/**
 * Interface CommandRunner
 * @package Utility\RequestCommand
 */
interface CommandRunner
{
    /**
     * @param TaskRequestInterface $taskRequest
     * @throws JsonException
     */
    public function send(TaskRequestInterface $taskRequest): void;

    /**
     * @param TaskRequestInterface $taskRequest
     * @throws CommandRequestException
     */
    public function run(TaskRequestInterface $taskRequest): void;
}
Enter fullscreen mode Exit fullscreen mode

All of handle pairs should be determined in configuration config.php/requestCommand.php. For example,

Alt Text

If request command is going to be called externally you are going to need alias for this call:

/*
|------------------------------------------------------------
| Handlers Aliases
|------------------------------------------------------------
*/
'requestAliases' => [
    'makeMessage' => MakeMessageTaskRequest::class,
    'createTeam'  => CreateTeamTaskRequest::class,
],
Enter fullscreen mode Exit fullscreen mode

That's way we can add multi-purpose Handler-Request pair to 'requestCommand'. It can be simple peer-to-peer:

AddScenarioStatisticTaskRequest::class => AddScenarioStatisticHandler::class

Or transaction mode (all handlers are going to be committed in one transaction):

MakeMessageTaskRequest::class => [
     MakeMessageTaskRequest::class => MakeMessageHandler::class,
     UpdateSessionLastSeenTaskRequest::class => UpdateSessionLastSeenHandler::class
],
Enter fullscreen mode Exit fullscreen mode

Pay attention here: you should create only one request via command handler, for example 'MakeMessageTaskRequest' and all other requests are going to be created automatically according with your chain of requests/handlers.

After that you can create specific TaskRequest with entity-specific DTO in Infrastructure/TaskRequest and Handler for it in Application/CommandHandler.
TaskRequest example:

Alt Text

Yep, it's going to be DeactivateUserTariffPlanDTO instead of RequestDTO immediately after we update PHP version to 8.0 :)

CommandHandler example (it can be in any context):

Alt Text

As you can see, handler 'process' method should call Application Service accordingly with procedure, that you need. Add Middleware Validators for each of them accordingly in Handler.

Handler without Transactions should implement RequestCommandHandler interface and Transaction Handlers should implement RequestCommandHandler,TransactionRequestCommandHandler interfaces.

There are two types of transaction handlers: "insiders" and "closing". Insiders (implements InsiderTransactionRequestCommandHandler interface as well) should create DTO for the next transaction request in the process() method via creating

$this->nextTransactionRequestDTO and return it in the following way:

/**
 * @return bool
 */
public function isInTransaction(): bool
{
    return true;
}

/**
 * @return RequestDTO|null
*/
public function getNextTransactionRequestDTO(): RequestDTO
{
    return $this->nextTransactionRequestDTO;
} 
Enter fullscreen mode Exit fullscreen mode

Closing one closes transaction (the last request on the list), so it should only implement isInTransaction method:

/**
 * @return bool
*/
public function isInTransaction(): bool
{
    return false;
}
Enter fullscreen mode Exit fullscreen mode

I am planning to create two different strategies for "insiders" and "closing" request, but it haven't done yet.
TaskHandler is going to handle all inner processes, such as setup transactions decorator service with UnitOfWork, obtain and handle request-commands pairs and request data from the configuration service, handle low-level exceptions and so on.

Alt Text

All we have left to do is to start listen "request-command" queue via message broker (we use RabbitMQ, but it's going to work with any of brokers, anyway).

Example for DeactivateUserTariffPlan:

From any of bounded contexts services inside API:

$this->commandRunner->send(new DeactivateUserTariffPlanTaskRequest(new DeactivateUserTariffPlanDTO(['userId' => 1])));
Enter fullscreen mode Exit fullscreen mode

From external services (for example, API Gateway):

Send json data to the "request-command" queue:

[
    'userId' => 1
]
Enter fullscreen mode Exit fullscreen mode

Discussion (0)

pic
Editor guide