2017-03-10 272 views
0

我想在PHP中制作CQRS事件源应用程序。我想知道,把聚合根(AbstractItem在下面的例子中)放入在db中序列化的事件是否可行? (我想没有,但什么是另类?)比如我有命令处理程序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); 
    } 

CartAbstractItem是总根源。 我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 AR本身在ItemToCartAdded事件中。然后我会在applyItemToCartAdded方法中根据那些DTO数据实例化新的对象AbstractItem

但由于项目是抽象类,我不知道我需要实例化什么实现。当然,我可以在那个DTO中有班级名称,所以我知道然后以某种方式破解它。但似乎有点矫枉过正,因为我在AbstractItem上有私有构造函数,并在特定实现中使用工厂方法。

另一方面,序列化整个聚合根带我到序列化的问题:当我序列化AR时,我需要反序列化在某些时候,但我该怎么做?我知道有反射,但它是丑陋的黑客,绕过我的反序列化AR验证,因此我可能最终以某种方式无效的AR可能陷入噩梦。或不?

我应该如何序列化和反序列化我的事件以很好地解决此问题?也许我可以在ItemToCartAdded事件中找到这两个聚合的ID,但是我不能将这个事件应用到购物车AR,以便它拥有AbstractItem(最终保护我的一些不变量)列表。我最终会在购物车AR中列出物品ID列表,因为我当然没有在我的AR中访问存储库。

我的问题在哪里,或者我错了什么?

顺便说一句,我使用这个库是叉:https://github.com/beberlei/litecqrs-php

+1

你可以看看另一个CQRS + ES库PHP这里https://github.com/xprt64/todosample-cqrs-es在这个库事件从'Aggregate'被'产生',然后持续到'EventStore'。免责声明:我已经做到了:) –

+0

@ConstantinGALBENU我检查了你的框架,顺便把命令直接传递给聚合是一个有趣的方法,但是我需要质疑,是否真的需要某个AR负责每个特定的命令?我认为在绝大多数时候是的,但真的是每次? :) – Tom

+0

@ConstantinGALBENU聚合产生事件是很好的方法,但我不明白他们应该如何解决我的问题:(我的问题是,我的购物车AR由'AbstractItem' ARs(它们也可以不存在'Cart' ),并且我需要将它们放入购物车中。对于您的库,我会遇到类似的问题:我需要将'AbstractItem' AR添加到'AddItemToCartCommand'(这是无稽之谈),或者是所有数据都能够重新实例化购物车AR处理程序 - 这听起来更好,但恢复AR而不是从存储库(不是在AR处理程序中)听起来很奇怪 – Tom

回答

1

我想在PHP中制作CQRS事件源应用程序。我想知道,把聚合根(AbstractItem在下面的例子中)放入在db中序列化的事件可以吗? (我想不,但是有什么选择?)

不,你不能。我同意@Codescribler。 我认为你的设计有问题。

而Cart和AbstractItem是聚合根。

你为什么要做AbstractItemAggregate rootAbstractItem保护什么不变? Commands它处理什么和Events产生什么?这是你的主要设计问题。你应该根据你的Ubiquitous language来命名,比如Product,或者如果你需要更抽象的名字,那么CartItem

Product是一个聚合根,但在不同的有界上下文中,如Inventory。或者,如果不需要保护任何不变量,它可能是一个CRUD实体。在该BC中,它负责创建,修改和删除商店中的产品。

您现在处于Ordering bounded context中,您应该查看本BC中的域规则。那么,你的规则是什么?您是否允许将Inventory BC中的价格修改传播到Cart中的项目?我不这么认为。您的客户会非常生气,至少在没有任何通知说他们将商品添加到购物车后价格发生了变化。所以,假设一旦产品加入购物车,价格就会冻结。所以,这将是不可改变的。

现在关于Cart。为了保护您的不变式,您需要关于Products的哪些信息被添加到Cart?什么是Cart不变式?那么,你可以有一个最大值Order,比方说10000美元。所以你需要标识ProductProduct price。 因此,CartItem是不可变的Value Object,其包含ProductIdProductPrice。 所以,你的代码应该是这样的:

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; 
    } 
} 
+0

首先,客户创建一些'AbstractItem' - 只设置他想订购的产品或服务的参数(每个都是我们的域,并且应该有BC的实际处理)。这个'AbstractItem'具有在特定类中实现的自己的invarint,例如具有最小和最大长度不变的'CleaningServiceItem'。然后顾客将其中的一些添加到购物车并结账。它会创建包含这些项目所有信息的订单。然后,客户可以在支付订单前使用promocode,根据其实施情况对每个项目应用不同的项目。如何建模? – Tom

+0

更具体地说,例如,如何从购物车中删除物品,当它是没有ID的值对象时?我仍然有一些链接removeItemFromCart?ID = SomeUuID在Web交付机制的用户界面,对吧? – Tom

+1

即使它是值对象,您仍然可以引用相应的聚合根ID。请在我的回答中注意'CartItem'有'productId'。 –

1

答案很简单 - 你不把聚集成一个事件。

事件表示状态的变化。不是存储聚合的当前状态,而是捕获更改的内容。这是你的事件。在你的情况下,添加的项目的ID将是所需的最低信息。顺便提一下,为了方便起见,我想在我的活动中添加更多信息。

为什么在一个事件中存储AR是不好的?

事件是不可变的。 AR根据定义是可变的。一个AR也被封装,使得序列化最好,尴尬。

+0

是的,我对此感到怀疑。但是,当我仅用ID(这实际上是我理论上需要的唯一信息)存储事件时,我无法在购物车上的applyItemToCartAdded方法中创建整个AbstractItem聚合,我需要保护购物车上的一些不变量。大多数情况下会以某种方式在应用程序中通过ID从存储库中检索AR,但由于在AR中存储库显然是无稽之谈,我不知道它是否已经放置在任何地方,听起来很诡异。所以我坚持实际保存有关AR的所有信息,然后用apply方法来实例化AR? – Tom

+1

我不知道你的具体领域,但它听起来像你有一些建模工作要做。聚合根应该拥有它在聚合中维护不变量所需的全部内容。也许你在设计中缺少一个概念。 – Codescribler

+0

或者,也许,如何在事件中使用AR,但是在序列化中,只需将它们的ID保存在事件存储的反序列化中,并从存储库加载它们呢?解决这个问题是否方便和普遍? :) – Tom

相关问题