CQRS Introduction - Commands

Commands CQRS PHP

In this section, we will look at how to use Commands, Events, and Queries. This will help you understand the basics of Ecotone’s CQRS support and how to build a message-driven application.

Command Handlers are methods where we typically place our business logic, so we’ll start by exploring how to use them.

Handling Commands

Any service available in your Dependency Container can become a Command Handler. Command Handlers are responsible for performing business actions in your system. In Ecotone-based applications, you register a Command Handler by adding the CommandHandler attribute to the specific method that should handle the command:

class TicketService
{
    #[CommandHandler] 
    public function createTicket(CreateTicketCommand $command) : void
    {
        // handle create ticket command
    }
}

In the example above, the #[CommandHandler] attribute tells Ecotone that the "createTicket" method should handle the CreateTicketCommand.

The first parameter of a Command Handler method determines which command type it handles — in this case, it is CreateTicketCommand.

If you are using autowiring, all your classes are registered in the container under their class names. This means Ecotone can automatically resolve them without any extra configuration.

If your service is registered under a different name in the Dependency Container, you can use ClassReference to point Ecotone to the correct service:

#[ClassReference("ticketService")]
class TicketService

Sending Commands

We send a Command using the Command Bus. After installing Ecotone, all Buses are automatically available in the Dependency Container, so we can start using them right away. Before we can send a Command, we first need to define it:

class readonly CreateTicketCommand
{
    public function __construct(
        public string $priority,
        public string $description
    ){}
}

To send a command, we use the send method on the CommandBus. The command gets automatically routed to its corresponding Command Handler

class TicketController
{
   // Command Bus will be auto registered in Depedency Container.
   public function __construct(private CommandBus $commandBus) {}
   
   public function createTicketAction(Request $request) : Response
   {
      $this->commandBus->send(
         new CreateTicketCommand(
            $request->get("priority"),
            $request->get("description"),            
         )
      );
      
      return new Response();
   }
}

Sending Commands with Metadata

We can send commands with metadata (also called Message Headers) through the Command Bus. This lets us include additional context that doesn't belong in the command itself, or share information across multiple Command Handlers without duplicating it in each command class.

class TicketController
{
   public function __construct(private CommandBus $commandBus) {}
   
   public function closeTicketAction(Request $request, Security $security) : Response
   {
      $this->commandBus->send(
         new CloseTicketCommand($request->get("ticketId")),
         ["executorId" => $security->getUser()->getId()]
      );
   }
}

And then to access given metadata, we will be using Header attribute:

class TicketService
{   
    #[CommandHandler]
    public function closeTicket(
        CloseTicketCommand $command, 
        // by adding Header attribute we state what metadata we want to fetch
        #[Header("executorId")] string $executorId
    ): void
    {          
//        handle closing ticket with executor from metadata
    }   
}

The #[Header] attribute tells Ecotone to fetch a specific piece of metadata using the key executorId. This way, Ecotone knows exactly which metadata value to pass into our Command Handler.

Injecting Services into Command Handler

If we need additional services from the Dependency Container to handle our business logic, we can inject them into our Command Handler using the #[Reference] attribute:

class TicketService
{   
    #[CommandHandler]
    public function closeTicket(
        CloseTicketCommand $command, 
        #[Reference] AuthorizationService $authorizationService
    ): void
    {          
//        handle closing ticket with executor from metadata
    }   
}

In case Service is defined under custom id in DI, we may pass the reference name to the attribute:

#[Reference("authorizationService")] AuthorizationService $authorizationService

Sending Commands via Routing

In Ecotone we may register Command Handlers under routing instead of a class name. This is especially useful if we will register Converters to tell Ecotone how to deserialize given Command. This way we may simplify higher level code like Controllers or Console Line Commands by avoid transformation logic.

class TicketController
{
   public function __construct(private CommandBus $commandBus) {}
   
   public function createTicketAction(Request $request) : Response
   {
      $commandBus->sendWithRouting(
         "createTicket", 
         $request->getContent(),
         "application/json" // we tell what format is used in the request content
      );
      
      return new Response();
   }
}
class TicketService
{   
    // Ecotone will do deserialization for the Command
    #[CommandHandler("createTicket")]
    public function createTicket(CreateTicketCommand $command): void
    {
//        handle creating ticket
    }   
}

Routing without Command Classes

There may be cases where creating Command classes is unnecessary boilerplate, in those situations, we may simplify the code and make use scalars, arrays or non-command classes directly.

class TicketController
{
   private CommandBus $commandBus;

   public function __construct(CommandBus $commandBus)
   {
       $this->commandBus = $commandBus;   
   }
   
   public function closeTicketAction(Request $request) : Response
   {
      $commandBus->sendWithRouting(
         "closeTicket", 
         Uuid::fromString($request->get("ticketId"))
      );
      
      return new Response();
   }
}
class TicketService
{   
    #[CommandHandler("closeTicket")]
    public function closeTicket(UuidInterface $ticketId): void
    {
//        handle closing ticket
    }   
}

Returning Data from Command Handler

Sometimes we need to return a value immediately after handling a command. This is useful for scenarios that require instant feedback—for example, when processing a payment, we might need to return a redirect URL to guide the user to the payment gateway. Ecotone's allows for returning data from Command Handler, that will be available as a result from your CommandBus:

class PaymentService
{   
    #[CommandHandler]
    public function closeTicket(MakePayment $command): Url
    {
//        handle making payment

        return $paymentUrl;
    }   
}

The returned data will be available as result of the Command Bus.

$redirectUrl = $this->commandBus->send($command);

Sending Commands with deserialization

When any Serialization mechanism is configured (For example JMS), we can let Ecotone do the deserialization in-fly, so we don't need to both with doing custom transformations in the Controller:

   public function createTicketAction(Request $request) : Response
   {
      $ticketId = $this->commandBus->send(
            routingKey: 'createTicket',
            command: $request->getContent(),  // Ecotone will deserialize Command in-fly
            commandMediaType: 'application/json',
      );
      
      return new Response([
            'ticketId' => $ticketId
      ]);
   }

Last updated

Was this helpful?