This article reprinted from decoupling 】 【 how: https://codedecoupled.com/php…

Event Sourcing is one of the architectural patterns in Domain Driven Design. Domain Driven Design is a business-oriented approach to modeling. It helps developers model more closely to the business.

In a traditional application, we store the state in a database, and when the state changes, we update the corresponding state value in the database in real time. Event traceability takes a completely different approach. The core of event traceability is events. All states are derived from events.

In this article, we’ll use the event traceability pattern to write a simplified shopping cart that breaks down several of the key components of event traceability. We will also use Spatie’s event traceability database to avoid duplicate ships.

In our case, the user can add, remove, and view the contents of the shopping cart, and it has two business logic:

  • You cannot add more than 3 products to your shopping cart.
  • When the user adds a fourth product, the system will automatically send an alert email.

Requirements and declarations

  • This article uses the Laravel framework.
  • This article uses a specific versionSpatie/laravel - event - sourcing: 4.9.0To avoid syntax problems between versions.
  • This is not a step-by-step tutorial, you must have some Laravel background to understand this article, so avoid the rhetoric and focus on the composition of the architectural patterns.
  • The key point of this paper is to expound the core idea of event traceability. The realization of event traceability in this library is not the only scheme.

Domain Events

The events in event traceability are called domain events. Unlike traditional transaction events, they have the following characteristics:

  • It is business-related, so its name tends to include business terms and should not be tied to the database. For example, if you add items to your shopping cart, the corresponding domain event should beProductAddedToCartRather thanCartUpdated.
  • It refers to something that happened, so it must be in the past tense, for exampleProductAddedToCartRather thanProductAddToCart.
  • Domain events can only be appended and cannot be deleted or changed. If deletion is required, we need to use a domain event that has the effect of deletion, such asProductRemovedFromCart.

Based on the above information, we construct three kinds of 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;
    }

}

Events productAddedToCart and productMovedFromCart represent items added to and removed from the cart, respectively. Event cartCapacityExceeded represents items exceeding the limit in the cart. This is one of the business logic we mentioned above.

Polymerization (Aggregate)

In domain-driven design, an Aggregate is a set of closely related classes that stand on their own to form a bounded organization, and objects outside the boundary can interact with the Aggregate only through the Aggregate Root, a special class within the Aggregate. We can aggregate an imaginary household book, and any operation on it must go through the head of the household (aggregation root).

Aggregation has the following characteristics:

  • It ensures immutability of the core business. That is, we are doing validation in the aggregate, throwing exceptions for operations that violate business logic.
  • It is where domain events occur. Domain events occur in the aggregation root. That is, we can accomplish business requirements in domain events.
  • It is self-contained and has a distinct boundary, meaning that methods in the aggregate can only be called through the aggregate root.

Aggregation is the primary and most immediate part that serves the business logic that we use to model our business intuitively.

To sum up, let’s build a CartaggregateRoot aggregation 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 items, respectively.

We also need to add some properties to record the contents of the 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; The items in the shopping cart will be recorded, so when can we assign them a value? In event sourcing, this is after the event has occurred, 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 we call the AddItem and RemoveItem events, we publish the ProductAddedToCart and ProductMovedFromCart events, respectively. At the same time, we assign a value to $products via the apply magic method:

<? 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 a magic method in Spatie’s event traceability library. When we publish an event using RecordThat, apply* is called automatically. It ensures that the state is changed after the event has been published.

Now that CarTagGregateRoot has got the required state from the event, we can add our first business logic: No more than three products can be added to the shopping cart.

Modify CartAggregateRoot: : addItem, when a user to add 4 kinds of products, publish CartCapacityExceeded related events:

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)
    );
}

We have now completed the aggregation root, and while the code is simple, the model built from the simulated business is straightforward.

When we add an item, we call:

CartAggregateRoot::retrieve(Uuid::uuid4())->addItem(1, 100);

When we add an item, we call:

CartAggregateRoot::retrieve($uuid)->removeItem(1);

Projector

The UI is an essential part of the application, such as showing the contents of the shopping cart to the user, and replaying the aggregation root may cause performance problems. We can use a Projector at this time.

The projector monitors domain events in real time and allows us to create database tables that serve the UI. The great thing about the projector is that it can be remade, and if we find a bug in the code that affects the UI data, we can remade the form that the projector created.

Let’s write a projector CartProjector that works for the user:

<?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();
    }

}

The projector cartProjector adds or deletes the form projection_carts based on the events being listened on. ProjectionCart is a common Laravel model that we use only to manipulate the database.

When our UI needs to display the contents of the shopping cart, we read the data from projection_carts, which is similar to read-write separation.

Reactor

The Reactor, like a projector, monitors domain events in real time. The difference is that the reactor can’t be remolded, it’s used to perform operations with side effects, so it can’t be remolded.

We use it to implement our second business logic: when the user adds a fourth product, the system automatically sends out an alert email.

<?php


use Spatie\EventSourcing\EventHandlers\Reactors\Reactor;

class WarningReactor extends Reactor
{
    public function onCartCapacityExceeded(CartCapacityExceeded $event)
    {
        Mail::to('[email protected]')->send(new CartWarning());
    }
}

The WarningReactor will listen for the event cartCapacityExceed, and we will use Laravel Mailable to send an alert email.

conclusion

So far we have briefly introduced several components of event sourcing. Software was originally designed to solve complex business problems using familiar programming languages. In order to solve real business problems, the gurus invented object-oriented programming (OOP) so that we can avoid writing noodle code and build models that are closest to reality. But for some reason, ORM has left most developers’ models at the database level. Models should not encapsulate database tables, but rather business. What object-oriented programming gives us is the ability to model business objects more accurately. The design of the database, the manipulation of the data is not the core of the software, the business is.

At the beginning of software design, we should forget about database design and focus on the business.

This article reprinted from decoupling 】 【 how: https://codedecoupled.com/php… If you are also interested in TDD, DDD and clean code, please follow the public account “How to Decouple” to explore the way of software development.