Home > Article > Backend Development > Let's talk about event sourcing in php
Event sourcing is one of the architectural patterns in domain-driven design thinking. Domain-driven design is a business-oriented modeling approach. It helps developers build models that are closer to the business. Today we will talk about event sourcing in php.
Event Sourcing is one of the architectural patterns in the Domain Driven Design design philosophy. Domain-driven design is a business-oriented modeling approach. It helps developers build models that are closer to the business.
In traditional applications, we store the state in the database. When the state changes, we immediately update the corresponding state value in the database. Event sourcing adopts a completely different model. Its core is events, and all states are derived from events. We obtain the status of the application by playing events, so it is called event sourcing.
In this article, we will use the event sourcing model to write a simplified shopping cart to break down several important concepts of event sourcing. We will also use Spatie's event sourcing library to avoid reinventing the wheel.
In our case, users can add, delete and view the contents of the shopping cart, and it has two business logic:
The shopping cart cannot add more than 3 products. When the user adds the 4th product, the system will automatically send an alert email.
Requirements and declarations
This article uses the Laravel framework. This article uses the specific version spatie/laravel-event-sourcing:4.9.0 to avoid syntax issues between different versions. This article is not a step-by-step tutorial. You must have a certain basic knowledge of Laravel to understand this article. Please avoid wordy words and focus on the structure of the architectural pattern. The focus of this article is to elaborate on the core idea of event sourcing. The implementation of event sourcing in this library is not the only solution.
Domain Event
Events in event tracing are called domain events. Different from traditional transaction events, they have the following characteristics:
It is closely related to business, so its naming often contains business nouns and should not be linked to the database. For example, when adding products to the shopping cart, the corresponding domain event should be ProductAddedToCart, not CartUpdated. It refers to something that happened, so it must be in the past tense, such as ProductAddedToCart instead of ProductAddToCart. Domain events can only be appended and cannot be deleted or changed. If they need to be deleted, we need to use domain events with deletion effects, such as ProductRemovedFromCart.
Based on the above information, we construct three domain events:
ProductAddedToCart:
<?php use Spatie\EventSourcing\StoredEvents\ShouldBeStored; class ProductAddedToCart extends ShouldBeStored { public int $productId; public int $amount; public function __construct(int $productId, int $amount) { $this->productId = $productId; $this->amount = $amount; } }
ProductRemovedFromCart:
<?php use Spatie\EventSourcing\StoredEvents\ShouldBeStored; class ProductRemovedFromCart extends ShouldBeStored { public int $productId; public function __construct(int $productId) { $this->productId = $productId; } }
CartCapacityExceeded:
<?php use Spatie\EventSourcing\StoredEvents\ShouldBeStored; class CartCapacityExceeded extends ShouldBeStored { public array $currentProducts; public function __construct(array $currentProducts) { $this->currentProducts = $currentProducts; } }
The events ProductAddedToCart and ProductRemovedFromCart represent items added to the shopping cart and removed from the shopping cart respectively. The event CartCapacityExceeded represents the items in the shopping cart exceeding the limit. This is one of the business logics we mentioned earlier.
Aggregate
In domain-driven design, aggregation refers to a group of closely related classes that form a bounded Organization, objects outside the boundary can only interact with this aggregate through the aggregate root (Aggregate Root), which is a special class in the aggregate. We can imagine aggregation as a family household registration book. Any operation on this household registration book must go through the head of the household (aggregation root).
Aggregation has the following characteristics:
It ensures the immutability of the core business. In other words, we perform verification in aggregation and throw exceptions for operations that violate business logic. It is where domain events occur. Domain events are generated in the aggregate root. That is to say, we can complete the business requirements in the domain event. It is self-contained and has clear boundaries, that is, methods in the aggregate can only be called through the aggregate root.
Aggregation is the main and most direct part that serves business logic. We use it to intuitively model our business.
To sum up, let us build a CartAggregateRoot aggregate root:
<?php use Spatie\EventSourcing\AggregateRoots\AggregateRoot; class CartAggregateRoot extends AggregateRoot { public function addItem(int $productId, int $amount) { } public function removeItem(int $productId) { } }
CartAggregateRoot has two methods addItem and removeItem, which represent adding and removing products respectively.
In addition, we need to add some attributes to record the contents of the shopping cart:
<?php use Spatie\EventSourcing\AggregateRoots\AggregateRoot; class CartAggregateRoot extends AggregateRoot { private array $products; public function addItem(int $productId, int $amount) { } public function removeItem(int $productId) { } }
private array $products; will record the products in the shopping cart, so when can we assign a value to it? In event sourcing, this is after the event occurs, so we first need to publish the domain event:
<?php use Spatie\EventSourcing\AggregateRoots\AggregateRoot; class CartAggregateRoot extends AggregateRoot { private array $products; public function addItem(int $productId, int $amount) { $this->recordThat( new ProductAddedToCart($productId, $amount) ); } public function removeItem(int $productId) { $this->recordThat( new ProductRemovedFromCart($productId) ); } }
When calling the addItem and removeItem events, we publish the ProductAddedToCart and ProductRemovedFromCart events respectively. At the same time, we pass apply The magic method assigns value to $products:
<?php use Spatie\EventSourcing\AggregateRoots\AggregateRoot; class CartAggregateRoot extends AggregateRoot { private array $products; public function addItem(int $productId, int $amount) { $this->recordThat( new ProductAddedToCart($productId, $amount) ); } public function removeItem(int $productId) { $this->recordThat( new ProductRemovedFromCart($productId) ); } public function applyProductAddedToCart(ProductAddedToCart $event) { $this->products[] = $event->productId; } public function applyProductRemovedFromCart(ProductRemovedFromCart $event) { $this->products[] = array_filter($this->products, function ($productId) use ($event) { return $productId !== $event->productId; }); } }
apply* is the magic method that comes with Spatie's event sourcing library. When we use recordThat to publish an event, apply* will be automatically called. It ensures that the status change is in After the event is released.
现在 CartAggregateRoot 已通过事件获取了需要的状态,现在我们可以加入第一条业务逻辑:购物车不可添加超过 3 种产品。
修改 CartAggregateRoot::addItem,当用户添加第 4 种产品时,发布相关领域事件 CartCapacityExceeded:
public function addItem(int $productId, int $amount) { if (count($this->products) >= 3) { $this->recordThat( new CartCapacityExceeded($this->products) ); return; } $this->recordThat( new ProductAddedToCart($productId, $amount) ); }
现在我们已经完成了聚合根工作,虽然代码很简单,但是根据模拟业务而建立的模型非常直观。
加入商品时,我们调用:
CartAggregateRoot::retrieve(Uuid::uuid4())->addItem(1, 100);
加入商品时,我们调用:
CartAggregateRoot::retrieve($uuid)->removeItem(1);
放映机(Projector)
UI 界面是应用中不可缺少的部分,比如向用户展示购物车中的内容,通过重播聚合根或许会有性能问题。此时我们可以使用放映机(Projector)。
放映机实时监控领域事件,我们通过它可以建立服务于 UI 的数据库表。放映机的特点是它可以重塑,当我们发现代码中的 bug 影响到 UI 数据时,我们可以重塑此放映机建立的表单。
让我们写一个服务于用户的放映机 CartProjector:
<?php use Spatie\EventSourcing\EventHandlers\Projectors\Projector; class CartProjector extends Projector { public function onProductAddedToCart(ProductAddedToCart $event) { $projection = new ProjectionCart(); $projection->product_id = $event->productId; $projection->saveOrFail(); } public function onProductRemovedFromCart(ProductRemovedFromCart $event) { ProjectionCart::where('product_id', $event->productId)->delete(); } }
放映机 CartProjector
会根据监听的事件来增加或者删除表单 projection_carts,ProjectionCart 是一个普通的 Laravel 模型,我们仅使用它来操作数据库。
当我们的 UI 需要展示购物车中的内容时,我们从 projection_carts 读取数据,这和读写分离有异曲同工之妙。
反应机(Reactor)
反应机(Reactor)和放映机一样,实时监控领域事件。不同的是反应机不可以重塑,它的用途是用来执行带有副作用的操作,所以它不可以重塑。
我们使用它来实现我们的第二个业务逻辑:当用户添加第 4 个产品时,系统将自动发出一个预警邮件。
<?php use Spatie\EventSourcing\EventHandlers\Reactors\Reactor; class WarningReactor extends Reactor { public function onCartCapacityExceeded(CartCapacityExceeded $event) { Mail::to('admin@corporation.com')->send(new CartWarning()); } }
反应机 WarningReactor
会监听到事件 CartCapacityExceeded, 我们就会使用 Laravel Mailable 发送一封警报邮件。
总结
至此我们简单的介绍了事件溯源的几个组成部分。软件的初衷是运用我们熟悉的编程语言来解决复杂的业务问题。为了解决现实中的业务问题,大神们发明了面向对象编程(OOP),于是我们可以避免写出面条代码,可以建立最贴近现实的模型。但是由于某种原因, ORM 的出现让大多数开发者的模型停留在了数据库层面,模型不应该是对数据库表的封装,而是对业务的封装。面向对象编程赋予我们的是对业务对象更精确的建模能力。数据库的设计,数据的操作并不是软件关注的核心,业务才是。
在软件设计之初,我们应该忘记数据库设计,将注意力放到业务上面。
推荐学习:php视频教程
The above is the detailed content of Let's talk about event sourcing in php. For more information, please follow other related articles on the PHP Chinese website!