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.
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.
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.
Aggregate
tells Ecotone, that this class should be registered as Aggregate Root. - 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.
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.
CommandHandler
defined on non static class method is place where you would put business logic and state changes
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;
}
}
Last modified 1mo ago