Как откладывать выполнение доменных событий агрегата в агрегате уровнем выше?
Ниже приведен код домена "Покупка фильма" в сервисе продажи фильмов онлайн.
Опишу изначальный код.
У меня есть сервис FilmPurchaseService. Который списывает деньги со счета и дает доступ к фильму. При этом клиенту шлется смс.
<?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();
}
}
?>
На этом этапе все отлично. Пользователь покупает фильм. Ему уходит смс по событию UserPurchaseFilm.
Но бизнес логика расширяется фичей: пользователь может подписаться на услугу "Дайте мне доступ к лучшему фильму недели.". Каждый 5 фильм бесплатно. Т.е. обязательно вести историю автоматических покупок.
Я предполагаю, что это отдельный бизнес процесс. Таким образом у меня появится домен "Лучшие фильмы" со всей логикой поиска. И еще появится домен "Покупка лучшего фильма" как надстройка над двумя другими "Лучшие фильмы" и "Покупка фильма". Так как домен "Покупка фильма" работает хорошо и совсем не хочется добавлять знания о домене "Лучшие фильмы".
<?
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);
});
}
}
?>
Вот как здесь быть? "$this->statisticsAutomaticFilmPurchaseService->saveAutomaticFilmPurchaseEvent($user, $film);" может сфейлится.
Тогда деньги возвращаем. Но смс уже ушла! т.к. UserPurchaseFilmEvent уже передан на выполнение!
Как быть с событиями из агрегата, который используется в агрегате уровнем выше?
Я понимаю что этот пример несколько надуман, и я могу обойтись одним сервисом.
Но в реальной жизни домены сложнее.