Я пытаюсь сделать приложение CQRS источником событий в PHP. И мне интересно, нормально ли ставить агрегатный рут (AbstractItem
в следующем примере) в событие, которое сериализовано в БД? (Я полагаю, нет, но что является альтернативой?) Например, у меня есть обработчик команды для AddItemToCartCommand
с помощью этого метода дескриптора:
public function handle(Command $command)
{
Assertion::isInstanceOf($command, AddItemToCartCommand::class);
$cart = $this->loadCart($command->getCartId());
$item = $this->loadItem($command->getItemId());
$cart->addItem($item);
$this->eventRepository->save($cart);
}
В то время как Cart
а также AbstractItem
являются совокупными корнями.
мой Cart
AR реализован так:
class Cart extends AggregateRoot
{
/** @var UuidInterface */
private $customerId;
/** @var AbstractItem[] */
private $items;
public function __construct(UuidInterface $cartId, UuidInterface $customerId)
{
$this->apply(new EmptyCartCreated($cartId, $customerId));
}
public function addItem(AbstractCartItem $item)
{
$this->apply(new ItemToCartAdded($this->getId(), $item));
}
protected function applyEmptyCartCreated(EmptyCartCreated $event)
{
$this->setId($event->getCartId());
$this->customerId = $event->getCustomerId();
}
protected function applyItemToCartAdded(ItemToCartAdded $event)
{
$item = $event->getItem();
$this->items[(string) $item->getId()] = $item;
}
}
Теперь проблема с ItemToCartAdded
Событие, которое имеет такую структуру:
class ItemToCartAdded extends AbstractDomainEvent
{
/** @var UuidInterface */
protected $cartId;
/** @var AbstractItem */
protected $item;
public function __construct(UuidInterface $cartId, AbstractItem $item)
{
$this->cartId = $cartId;
$this->item = $item;
}
public function getCartId(): UuidInterface
{
return $this->cartId;
}
public function getItem(): AbstractItem
{
return $this->item;
}
}
Может быть, я должен иметь некоторый объект DTO для хранения AbstractItem
данные вместо AbstractItem
Сама АР в ItemToCartAdded
событие. Тогда я бы в applyItemToCartAdded
метод просто создать новый AbstractItem
объект на основе этих данных DTO.
Но так как item является абстрактным классом, я не знаю, какую реализацию мне нужно создать. Конечно, я мог бы иметь имя класса в этом DTO, так что я знаю, а затем каким-то образом взломать его. Но это выглядит немного излишним, так как у меня есть личный конструктор AbstractItem
и использовать фабричные методы в конкретных реализациях.
С другой стороны, сериализация целого агрегатного корня приводит меня к проблеме с сериализацией: когда я сериализую AR, мне нужно в какой-то момент десериализоваться, но как мне это сделать? Я знаю, что есть рефлексия, но это отвратительный хак, который обходит мою проверку AR при десериализации, и, таким образом, я могу в итоге получить как-то недопустимый AR, который может превратиться в кошмар. Или нет?
Как мне сериализовать и десериализовать мои события, чтобы хорошо решить эту проблему? Может быть, я мог бы иметь в ItemToCartAdded
события только идентификаторы этих двух агрегатов, но тогда я не смог бы применить это событие к корзине AR, чтобы оно имело список AbstractItem
s (чтобы в конечном итоге защитить некоторые из моих инвариантов). Я бы закончил с просто списком идентификаторов элементов в корзине AR, так как, конечно, у меня нет доступа к хранилищу в моем AR.
Где моя проблема здесь или в чем я ошибаюсь?
Кстати, я использую для этой развилки этой библиотеки: https://github.com/beberlei/litecqrs-php
Я пытаюсь сделать приложение CQRS источником событий в PHP. И мне интересно, нормально ли помещать агрегатный корень (AbstractItem в следующем примере) в событие, которое сериализуется в БД? (Полагаю, нет, но какая альтернатива?)
Нет, ты не можешь. Я согласен с @Codescribler.
Я думаю, что у вас есть проблемы с вашим дизайном.
Тогда как Cart и AbstractItem являются совокупными корнями.
Почему ты сделал AbstractItem
Aggregate root
? Что делает инвариант AbstractItem
защищает? Какие Commands
это обрабатывает и что Events
доходность? Это ваша главная проблема дизайна. И вы должны назвать его в соответствии с вашим Ubiquitous language
, лайк Product
или если вам нужно более абстрактное имя, то CartItem
,
Product
Агрегированный корень, но в другом ограниченном контексте, например Inventory
, Или это может быть объект CRUD, если не нужно защищать какие-либо инварианты. В этом BC он занимается созданием, изменением и удалением товара из вашего магазина.
Вы сейчас в Ordering bounded context
и вы должны пересмотреть правила домена в этом BC. Итак, каковы ваши правила? Вы разрешаете изменение цены в Inventory
До н.э., чтобы распространяться на элементы в Cart
? Я так не думаю. Ваши клиенты будут очень злы, по крайней мере, без уведомлений об изменении цен с момента добавления товара в корзину. Итак, предположим, что как только товар был добавлен в корзину, цена зависает. Так что это будет неизменным.
Теперь о Cart
, Какая информация вам нужна о Products
которые добавляются к Cart
чтобы защитить ваш инварианты? Каковы Cart
инварианты? Ну, вы могли бы иметь максимум Order
стоимость, скажем, 10000 грн. Так что вам нужно идентифицировать Product
и Product price
,
Итак, CartItem
является неизменным Value Object
, содержащий ProductId
а также ProductPrice
,
Итак, ваш код должен выглядеть так:
namespace Domain\Ordering;
class Cart
{
/** @var CartItem[] */
private $items = [];
const MAXIMUM_CART_VALUE = 10000;
public function handleAddItemToCart(AddItemToCart $command)
{
if ($this->getTotalCartValue() + $command->getItem()->getTotalItemPrice() > self::MAXIMUM_CART_VALUE) {
throw new \Exception(sprintf("A cart value cannot be more than %d USD", self::MAXIMUM_CART_VALUE));
}
yield new AnItemWasAddedToCart($command->getCartId(), $command->getItem());
}
public function applyAnItemWasAddedToCart(AnItemWasAddedToCart $event)
{
$this->items[] = $event->getItem();
}
private function getTotalCartValue()
{
return array_reduce($this->items, function (float $acc, CartItem $item) {
return $acc + $item->getTotalItemPrice();
}, 0.0);
}
}
class AddItemToCart implements Command
{
/**
* @var CartId
*/
private $cartId;
/**
* @var CartItem
*/
private $item;
public function __construct(
CartId $cartId,
CartItem $item
)
{
$this->cartId = $cartId;
$this->item = $item;
}
public function getCartId(): CartId
{
return $this->cartId;
}
public function getItem(): CartItem
{
return $this->item;
}
}
class AnItemWasAddedToCart implements Event
{
/**
* @var CartId
*/
private $cartId;
/**
* @var CartItem
*/
private $item;
public function __construct(
CartId $cartId,
CartItem $item
)
{
$this->cartId = $cartId;
$this->item = $item;
}
public function getCartId(): CartId
{
return $this->cartId;
}
public function getItem(): CartItem
{
return $this->item;
}
}
class CartId
{
//...
}
class ProductId
{
//...
}
class CartItem
{
/**
* @var ProductId
*/
private $productId;
/**
* @var float
*/
private $pricePerUnit;
/**
* @var int
*/
private $quantity;
public function __construct(
ProductId $productId,
float $pricePerUnit,
int $quantity
)
{
$this->productId = $productId;
$this->pricePerUnit = $pricePerUnit;
$this->quantity = $quantity;
}
public function getProductId(): ProductId
{
return $this->productId;
}
public function getTotalItemPrice()
{
return $this->pricePerUnit * $this->quantity;
}
}
Простой ответ — вы не помещаете агрегат в событие.
Событие представляет собой изменение состояния. Вместо сохранения текущего состояния агрегата вы фиксируете то, что изменилось. Это ваше событие. В вашем случае идентификатор добавленного элемента будет минимально необходимой информацией. Кстати, мне нравится добавлять больше информации в мои события для удобства.
Почему плохое хранение AR в событии?
Событие является неизменным. AR по определению изменчив. AR также инкапсулируется, что делает сериализацию в лучшем случае неудобной.