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.
In Ecotone, the class itself is not a Command Handler — only the specific method is. This means you can place multiple Command Handlers inside the same class, to make correlated actions available under same API class.
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
){}
}All Messages (Commands, Queries, and Events), as well as Message Handlers, are just plain PHP objects. They don’t need to extend or implement any Ecotone-specific classes. This keeps your business code clean, simple, and easy to understand.
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();
}
}$messagingSystem->getCommandBus()->send(
new CreateTicketCommand(
$priority,
$description,
)
);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()]
);
}
}$messagingSystem->getCommandBus()->send(
new CloseTicketCommand($ticketId),
["executorId" => $executorId]
);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.
If we use Asynchronous Command Handler, Ecotone will ensure our metadata will be serialized and deserialized correctly.
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 $authorizationServiceSending 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();
}
}$messagingSystem->getCommandBus()->sendWithRouting(
"createTicket",
$data,
"application/json"
);class TicketService
{
// Ecotone will do deserialization for the Command
#[CommandHandler("createTicket")]
public function createTicket(CreateTicketCommand $command): void
{
// handle creating ticket
}
}Ecotone is using message routing for cross application communication. This way applications can stay decoupled from each other, as there is no need to share the classes between them.
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();
}
}$messagingSystem->getCommandBus()->sendWithRouting(
"closeTicket",
Uuid::fromString($ticketId)
);class TicketService
{
#[CommandHandler("closeTicket")]
public function closeTicket(UuidInterface $ticketId): void
{
// handle closing ticket
}
}Ecotone provides flexibility which allows to create Command classes when there are actually needed. In other cases we may use routing functionality together with simple types in order to fulfill our business logic.
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);Keep in mind that return values only work with synchronous Command Handlers. For asynchronous handlers, we can't return values directly because the command is processed in the background—instead, we'd use events or callbacks to communicate results back to the user when processing completes.
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?