Links

Aggregate Command Handlers

Aggregate PHP
This chapter will cover the basics on how to implement an Aggregate. For more details on what an Aggregate is read the DDD and CQRS concepts page.

Aggregate Command Handlers

Working with Aggregate Command Handlers is the same as with External Command Handlers. We mark given method with Command Handler attribute and Ecotone will know that it should call it when this command is dispatched.
However Aggregates need to be fetched from repository in order to be executed. When we will take a look on most of the examples is that we fetch the aggregate on our own and using External Command Handlers to do it.
$product = $this->repository->getById($command->id());
$product->changePrice($command->getPriceAmount());
$this->repository->save($product);
This is a lot of boilerplate that needs to be handled with each of the Command we introduce into the system. This code is not business aligned, it's only needed due to technical limitations. However Ecotone resolves this, by doing the above code for you.
#[Aggregate]
class Product
{
#[AggregateIdentifier]
private string $productId;
By providing AggregateIdentifier attribute on top of property in your Aggregate, Ecotone knows what is your id, that should be used for fetching your aggregate.
Then when Command is dispatched, Ecotone looks for same property to resolve the id.
class ChangePriceCommand
{
private string $productId;
private Money $priceAmount;
You may use multiple aggregate identifiers or identifiers being objects, as long as they provide __toString method.
When identifier is resolved, Ecotone use repository to fetch the aggregate call the method and then save it.
You may use inbuilt repositories, so you fully focus on the domain model. Ecotone provides Event Store, Document Store, integration with Doctrine ORM or Eloquent, if that's not enough you can build your own.
For creation of the aggregate factory methods are used. You can read more about repositories and creating aggregates in following sections.

State-Stored Aggregate

An Aggregate is a regular object, which contains state and methods to alter that state. It can be described as Entity, which carry set of behaviours. When creating the Aggregate object, you are creating the Aggregate Root.
#[Aggregate] // 1
class Product
{
#[AggregateIdentifier] // 2
private string $productId;
private string $name;
private integer $priceAmount;
private function __construct(string $orderId, string $name, int $priceAmount)
{
$this->productId = $orderId;
$this->name = $name;
$this->priceAmount = $priceAmount;
}
#[CommandHandler] //3
public static function register(RegisterProductCommand $command) : self
{
return new self(
$command->getProductId(),
$command->getName(),
$command->getPriceAmount()
);
}
#[CommandHandler] // 4
public function changePrice(ChangePriceCommand $command) : void
{
$this->priceAmount = $command->getPriceAmount();
}
}
  1. 1.
    Aggregate tells Ecotone, that this class should be registered as Aggregate Root.
  2. 2.
    AggregateIdentifier is the external reference point Aggregate.
    This field tells Ecotone to which Aggregate a given Command is targeted. You may also you expose identifier over public method by annotating it with attribute
    #[AggregateIdentifierMethod("productId")]
  3. 3.
    CommandHandler defined on static method acts as factory method. Given command it should return new instance of specific aggregate, in that case new Product.
  4. 4.
    CommandHandler defined on non static class method is place where you would put business logic and state changes

Event Sourcing Aggregate

The difference between State-Stored and Event Sourced Aggregates are that State-Stored are holding only current state, where the second are keeping history of everything ever happened to given aggregate. This way we can keep audit of the changes just by design. Aggregate are returning events instead of changing state when CommandHandler is called and by using EventSourcingHandler we can rebuild the state internally.
#[EventSourcingAggregate]
final class Wallet
{
use WithAggregateVersioning;
#[AggregateIdentifier]
private string $walletId;
private int $balance = 0;
#[CommandHandler]
public static function setUp(SetUpWallet $command): array
{
/** Returning events instead of state */
return [new WalletWasSetUp($command->walletId)];
}
#[CommandHandler]
public function deposit(DepositMoney $command): array
{
return [new MoneyWasDeposited($command->walletId, $command->amount)];
}
#[EventSourcingHandler]
public function applyWalletWasSetUp(WalletWasSetUp $event): void
{
/** Apply the state from events */
$this->walletId = $event->walletId;
}
#[EventSourcingHandler]
public function applyMoneyWasDeposited(MoneyWasDeposited $event): void
{
$this->balance += $event->amount;
}
}
You can read more in Event Sourcing Section.