Answer the question
In order to leave comments, you need to log in
What about events from an aggregate that is used in a higher aggregate?
How to delay the execution of domain events of the aggregate in the aggregate level above?
Below is the domain code "Purchase movie" in the service of selling movies online.
I will describe the original code.
I have a FilmPurchaseService. Which writes off money from the account and gives access to the film. At the same time, an SMS is sent to the client.
<?php
trait HasEventsTrait
{
private $domainEvents = [];
public function registerEvent($event)
{
$this->domainEvents[] = $event;
}
public function releaseEvents()
{
$events = $this->domainEvents;
$this->domainEvents = [];
return $events;
}
}
class FilmUserService
{
public function __construct($entityManager)
{
$this->entityManager = $entityManager;
}
public function giveUserAccessToFilm(Film $film, User $user)
{
$filmUserLink = new FilmUserLink($film, $user);
$this->entityManager->persist($filmUserLink);
}
}
class FilmPurchaseService
{
use HasEventsTrait;
public function __construct(EventDispatcher $dispatcher, FilmUserAccessService $filmUserService, FilmCostCalculator $filmCostCalculator, UserBalanceCharger $userBalanceCharger)
{
$this->dispatcher = $dispatcher;
$this->filmUserAccessService = $filmUserService;
$this->filmCostCalculator = $filmCostCalculator;
$this->userBalanceCharger = $userBalanceCharger;
}
public function purchaseFilm(Film $film, User $user)
{
$cost = $this->filmCostCalculator->getCostFilm($film, $user);
DB::transaction(function () use($film, $user, $cost) {
$this->userBalanceCharger->chargeUser($cost);
$this->filmUserAccessService->giveUserAccessToFilm($film, $user);
$this->registerEvent(UserPurchaseFilmEvent::class);
});
foreach ($this->releaseEvents() as $event) {
$this->dispather->dispath($event);
}
}
}
class PurchaseFilmController
{
public function __construct(UserAuthService $userAuthService, FilmPurchaseService $filmPurchaseService)
{
$this->userAuthService = $userAuthService;
$this->filmPurchaseService = $filmPurchaseService;
}
public function __invoke(Film $film)
{
$user = $this->userAuthService->getUser();
$this->filmPurchaseService->purchaseFilm($film, $user);
return accept();
}
}
?>
<?
class AutomaticFilmPurchaseService
{
use HasEventsTrait;
public function __construct(FilmPurchaseService $filmPurchaseService, BestFilmFinder $bestFilmFinder, StatisticsAutomaticFilmPurchaseService $statisticsService)
{
$this->bestFilmFinder = $bestFilmFinder;
$this->filmPurchaseService = $filmPurchaseService;
$this->statisticsAutomaticFilmPurchaseService = $statisticsService;
}
public function purchaseBestFillm(User $user)
{
$film = $this->bestFilmFinder->find();
DB::transaction(function () use($film, $user) {
$this->filmPurchaseService->purchaseFilm($film, $user);
//вот здесь все может свалиться, а СМС уже отправлена!
$this->statisticsAutomaticFilmPurchaseService->saveAutomaticFilmPurchaseEvent($user, $film);
});
}
}
?>
Answer the question
In order to leave comments, you need to log in
To begin with, I recommend looking at the library for implementing domain events
https://github.com/gpslab/domain-event I
also recommend that you decide on the terminology. "Best Movie Buy", "Best Movies" and "Movie Buy" are not domains, but Bounded Contexts.
Cannot be an aggregate level higher. You have a misunderstanding of the term Aggregate. And with domain events too most. Domain events should be thrown in entities, not application layer services.
Further. You write off the balance from the user's account, then give him access to the movie and then throw the event and do all this sequentially, and even wrap the transaction in the database, although this is a business transaction.
Business logic should be all the same at the level of the subject area.
Anyway, back to your problem.
> the user can subscribe to the service "Give me access to the best movie of the week"
You have clearly described the concept of the Service. And you have two, let's say, services:
- Best films of the week
- Purchase of one film
From here you get:
interface Service
{
public function price(): Money;
// ...
}
class OneFilmService implements Service
{
public function __construct(Film $film)
{
// ...
}
// ...
}
class BestFilmsService implements Service
{
// ...
}
class User
{
// ...
public function buyService(
AccountRepository $repository,
Service $service
) {
// получаем текущий счёт пользователя
// и выполняем покупку
$repository
->get($this->id)
->buy($service->price())
;
// добавляем пользователю
// преобретенную услугу
$this->services[] = $service;
// бросаем доменное событие
$this->raise(new ServicePurchased(
$this->id,
$service->id()
);
}
}
Pass each domain its own event queue instance
In this case, the top-level aggregate will pass the queue to which no one subscribes to the filmPurchaseService instance. And it will already decide whether to forward the events collected by it in this queue, higher or not.
PS Well, it is absolutely incomprehensible why it is necessary to cancel the purchase of the film only because statisticsService has fallen off for you?
While the solution is (taking into account tons of legacy): During the transaction in the database, I start to accumulate messages, I do not send them to the queue. When committing to the database, I send all the accumulated messages to the queue (if I fail, I clean the queue).
Didn't find what you were looking for?
Ask your questionAsk a Question
731 491 924 answers to any question