Links

Business Interface

Be sure to read CQRS Introduction before diving in this chapter.

Execute your Business Actions via Interface

There may be cases when we will need to call specific Query Handler or Command Handler from inside of your business code. Introducing Command/Query Bus in our business code will result in blurring the logic as those are Messaging concepts. Besides Command or Query Buses may be intercepted by middlewares (to verify authorization for example), which may require us to pass some extra details which normally would not be needed.
In order to keep our business code clean and to allow for calling Query and Command Handlers from the business code, Ecotone provides Business Interface.

Command Interface

Let's take as an example creating new Ticket
class TicketService
{
#[CommandHandler("ticket.create")]
public function createTicket(CreateTicketCommand $command) : void
{
// handle create ticket command
}
}
We may define interface, that will call this Command Handler whenever will be executed.
interface TicketApi
{
#[BusinessMethod('ticket.create')]
public function create(CreateTicketCommand $command): void;
}
This way we don't need to use Command Bus and can bypass all Bus related interceptors.
The attribute #[BusinessMethod] tells Ecotone that given interface is meant to be used as entrypoint to Messaging and which Message Handler it should send the Message to. Ecotone will provide implementation of this interface directly in your Dependency Container.
We already used Business Interfaces without being aware, Command, Query and Event Buses are Business Interfaces. From lower level API Business Method is actually a Message Gateway.

Aggregate Command Interface

We may also execute given Aggregate directly using Business Interface.
#[Aggregate]
class Ticket
{
#[Identifier]
private Uuid $ticketId;
private bool $isClosed;
#[CommandHandler("ticket.close")]
public function close(): void
{
$this->isClosed = true;
}
}
Then we define interface:
interface TicketApi
{
#[BusinessMethod('ticket.close')]
public function create(#[Identifier] Uuid $ticketId): void;
}
We may of course pass Command class, if we need to pass data to our Aggregate's Command Handler.

Query Interface

Defining Query Interface works exactly the same as Command Interface and we may also use it with Aggregates.
class TicketService
{
#[QueryHandler("ticket.get_by_id")]
public function getTicket(GetTicketById $query) : array
{
//return ticket
}
}
Then we may call this Query Handler using Interface
interface TicketApi
{
#[BusinessMethod("ticket.get_by_id")]
public function getTicket(GetTicketById $query): array;
}

Query Interface Conversion

If we have registered Converter then we letEcotone convert the result of your Query Handler to specific format.
class TicketService
{
#[QueryHandler("ticket.get_by_id")]
public function getTicket(GetTicketById $query) : array
{
//return ticket
}
}
Then we may call this Query Handler using Interface
interface TicketApi
{
#[BusinessMethod("ticket.get_by_id")]
public function getTicket(GetTicketById $query): TicketDTO;
}
Ecotone will use defined Converter to conver array to TicketDTO.
Such conversion are useful in order to work with objects and to avoid writing transformation code in our business code.

Business Repository Interface

Special type of Business Interface is Repository. If you want to fetch or store Aggregate register under Ecotone's Repository you may use #[Repository] attribute.

For State-Stored Aggregate

interface OrderRepository
{
#[Repository]
public function getOrder(string $twitId): Order;
#[Repository]
public function findOrder(string $twitId): ?Order;
#[Repository]
public function save(Twitter $twitter): void;
}
Ecotone will read type hint to understand, which Aggregate you would like to fetch or save.
Implementation will be delivered by Framework. All you need to do is to define the interface and it will available in your Dependency Container

For Event Sourced Aggregate

interface OrderRepository
{
#[Repository]
public function getOrder(string $twitId): Order;
#[Repository]
public function findOrder(string $twitId): ?Order;
#[Repository]
#[RelatedAggregate(Order::class)]
public function save(string $aggregateId, int $currentVersion, array $events): void;
}
The difference is in save method, you need to provide aggregate id, current aggregate's version and array of events you would like to store.