Links
Comment on page

Lesson 2: Tactical DDD

DDD PHP
Not having code for Lesson 2?
git checkout lesson-2

Aggregate

An Aggregate is an entity or group of entities that is always kept in a consistent state. Aggregates are very explicitly present in the Command Model, as that is where change is initiated and business behaviour is placed.
Let's create our first Aggregate Product.
namespace App\Domain\Product;
use Ecotone\Modelling\Attribute\Aggregate;
use Ecotone\Modelling\Attribute\AggregateIdentifier;
use Ecotone\Modelling\Attribute\CommandHandler;
use Ecotone\Modelling\Attribute\QueryHandler;
#[Aggregate]
class Product
{
#[AggregateIdentifier]
private int $productId;
private int $cost;
private function __construct(int $productId, int $cost)
{
$this->productId = $productId;
$this->cost = $cost;
}
#[CommandHandler]
public static function register(RegisterProductCommand $command) : self
{
return new self($command->getProductId(), $command->getCost());
}
#[QueryHandler]
public function getCost(GetProductPriceQuery $query) : int
{
return $this->cost;
}
}
  1. 1.
    Aggregate annotation marks class to be known as Aggregate
  2. 2.
    AggregateIdentififer marks properties as identifiers of specific Aggregate instance. Each Aggregate must contains at least one identifier.
  3. 3.
    CommandHandler enables command handling on specific method just as we did in Lesson 1. If method is static, it's treated as factory method and must return new aggregate instance. Rule applies as long as we use State-Stored Aggregate instead of Event Sourcing Aggregate.
  4. 4.
    QueryHandler enables query handling on specific method just as we did in Lesson 1.
If you want to known more details about Aggregate start with chapter State-Stored Aggregate
Now remove App\Domain\Product\ProductService as it contains handlers for same command and query classes. Before we will run our test scenario, we need to register Repository.
Usually you will mark services as Query Handlers not aggregates. Ecotonedoes not block possibility to place Query Handler on Aggregate. It's up to you, where do you want to place Query Handler.

Repository

Repositories are used for retrieving and saving the aggregate to persistent storage. We will build in memory implementation for now.
namespace App\Domain\Product;
use Ecotone\Modelling\Attribute\Repository;
use Ecotone\Modelling\StandardRepository;
#[Repository] // 1
class InMemoryProductRepository implements StandardRepository // 2
{
/**
* @var Product[]
*/
private $products = [];
// 3
public function canHandle(string $aggregateClassName): bool
{
return $aggregateClassName === Product::class;
}
// 4
public function findBy(string $aggregateClassName, array $identifiers): ?object
{
if (!array_key_exists($identifiers["productId"], $this->products)) {
return null;
}
return $this->products[$identifiers["productId"]];
}
// 5
public function save(array $identifiers, object $aggregate, array $metadata, ?int $expectedVersion): void
{
$this->products[$identifiers["productId"]] = $aggregate;
}
}
  1. 1.
    Repository annotation marks class to be known to Ecotone as Repository.
  2. 2.
    We need to implement some methods in order to allow Ecotone, retrieve and save Aggregate. Based on implemented interface, Ecotone knowns, if Aggregate is state-stored or event sourced.
  3. 3.
    canHandle tells which classes can be handled by this specific repository
  4. 4.
    findBy return found aggregate instance or null. As there may be more, than single indentifier per aggregate, identifiers are array.
  5. 5.
    save saves passed aggregate instance. You do not need to bother right what is $metadata and $expectedVersion
If you want to known more details about Repository start with chapter Repository
Laravel
Symfony
Lite
# As default auto wire of Laravel creates new service instance each time
# service is requested from Depedency Container, we need to register
# ProductService as singleton.
# Go to bootstrap/QuickStartProvider.php and register our ProductService
namespace Bootstrap;
use App\Domain\Product\InMemoryProductRepository;
use Illuminate\Support\ServiceProvider;
class QuickStartProvider extends ServiceProvider
{
public function register()
{
$this->app->singleton(InMemoryProductRepository::class, function(){
return new InMemoryProductRepository();
});
}
(...)
Everything is set up by the framework, please continue...
Everything is set up, please continue...
Let's run our testing command:
bin/console ecotone:quickstart
Running example...
100
Good job, scenario ran with success!
Have you noticed, what are we missing here? Our Event Handler was not called, as we do not publish ProductWasRegistered event at this moment.

Event Publishing

In order to automatically publish events recorded within Aggregate, we need to add method annotated with AggregateEvents. This will tell Ecotone where to get the events from. Ecotone comes with default implementation, that can be used as trait WithAggregateEvents.
use Ecotone\Modelling\WithAggregateEvents;
#[Aggregate]
class Product
{
use WithAggregateEvents;
#[AggregateIdentifier]
private int $productId;
private int $cost;
private function __construct(int $productId, int $cost)
{
$this->productId = $productId;
$this->cost = $cost;
$this->recordThat(new ProductWasRegisteredEvent($productId));
}
(...)
You may implement your own method for returning events, if you do not want to couple with framework.
Let's run our testing command:
bin/console ecotone:quickstart
Running example...
Product with id 1 was registered!
100
Good job, scenario ran with success!
Congratulations, we have just finished Lesson 2. In this lesson we learnt how to make use of Aggregates and Repositories. Now we will learn about Converters and Metadata