Scout APM helps PHP developers pinpoint N+1 queries, memory leaks & more so you can troubleshoot fast & get back to coding faster. Start your free 14-day trial today.

A new major version of Laravel Event Sourcing

Today, we released a new version of spatie/laravel-event-sourcing, version 5 is probably one of the largest releases since the beginning of the package, we've worked several months on it and have been testing it extensively already in our own projects.

Credit where credit is due, many new features were inspired by Axon,
a popular event sourcing framework in the Java world, and several people pitched in during the development process.

In this post, I'll walk you through all significant changes, but first I want to mention the course that we've built at Spatie over the last months about event sourcing. If you're working on an event sourced project or thinking about starting one, this course will be of great help. Check it out on https://event-sourcing-laravel.com/!

# Consistent event handling

If you've used previous versions of our package, you might have struggled with how event handlers were registered across classes. Aggregate roots required you to write applyEventName functions, while projectors and reactors had an explicit event mapping.

Whatever class you're writing will now register event handlers the same way: by looking at the type of the event. You don't need any more configuration or naming conventions anymore.

class CartAggregateRoot extends AggregateRoot
{
    // …
    
    public function onCartAdded(CartAdded $event): void
    {
        // Any `CartAdded` event will automatically be matched to this handler
    }
}
class CartProjector extends Projector
{
    public function onCartAdded(CartAdded $event): void
    {
        // The same goes for projectors and reactors.
    }
}

# Event Queries

Event queries are a new feature that allow you to easily query an event stream without building database projections. You can think of them as in-memory projections that are rebuilt every time you call them.

Here's an example of it in action:

class EarningsForProductAndPeriod extends EventQuery
{
    private int $totalPrice = 0;
    
    public function __construct(
        private Period $period,
        private Collection $products
    ) {
        EloquentStoredEvent::query()
            ->whereEvent(OrderCreated::class)
            ->whereDate('created_at', '>=', $this->period->getStart())
            ->whereDate('created_at', '<=', $this->period->getEnd())
            ->cursor()
            ->each(
                fn (EloquentStoredEvent $event) => $this->apply($event)
            );
    }

    protected function applyOrderCreated(OrderCreated $orderCreated): void 
    {
        $orderLines = collect($orderCreated->orderData->orderLineData);

        $totalPriceForOrder = $orderLines
            ->filter(function (OrderLineData $orderLineData) {
                return $this->products->first(
                    fn(Product $product) => $orderLineData->productEquals($product)
                ) !== null;
            })
            ->sum(
                fn(OrderLineData $orderLineData) => $orderLineData->totalPriceIncludingVat
            );

        $this->totalPrice += $totalPriceForOrder;
    }
}

Note that these examples come from the Event Sourcing in Laravel book.

# Aggregate Partials

Aggregate partials allow you to split large aggregate roots into separate classes, while still keeping everything contained within the same aggregate. Partials can record and apply events just like an aggregate root, and can share state between them and their associated aggregate root.

Here's an example of an aggregate partial that handles everything related to item management within a shopping cart:

class CartItems extends AggregatePartial
{
    // …
    
    public function addItem(
        string $cartItemUuid, 
        Product $product, 
        int $amount
    ): self {
        $this->recordThat(new CartItemAdded(
            cartItemUuid: $cartItemUuid,
            productUuid: $product->uuid,
            amount: $amount,
        ));

        return $this;
    }

    protected function applyCartItemAdded(
        CartItemAdded $cartItemAdded
    ): void {
        $this->cartItems[$cartItemAdded->cartItemUuid] = null;
    }
}

And this is how the cart aggregate root would use it:

class CartAggregateRoot extends AggregateRoot
{
    protected CartItems $cartItems;

    public function __construct()
    {
        $this->cartItems = new CartItems($this);
    }

    public function addItem(
        string $cartItemUuid,
        Product $product,
        int $amount
    ): self {
        if (! $this->state->changesAllowed()) {
            throw new CartCannotBeChanged();
        }

        $this->cartItems->addItem($cartItemUuid, $product, $amount);

        return $this;
    }

Aggregate partials come with the same testing capabilities as aggregate roots, and are a useful way of keeping aggregate-related code maintainable.

# Command bus

We've added a command bus that can automatically map commands to handlers on aggregate roots:

namespace Spatie\Shop\Cart\Commands;

use Spatie\Shop\Support\EventSourcing\Attributes\AggregateUuid;
use Spatie\Shop\Support\EventSourcing\Attributes\HandledBy;
use Spatie\Shop\Support\EventSourcing\Commands\Command;

#[HandledBy(CartAggregateRoot::class)]
class AddCartItem implements Command
{
    public function __construct(
        #[AggregateUuid] public string $cartUuid,
        public string $cartItemUuid,
        public Product $product,
        public int $amount,
    ) {
    }
}

Whenever this command is dispatched, it will automatically be captured and handled by the associated aggregate root. It even works with aggregate partials:

class CartItems extends AggregatePartial
{
    // …
    
    public function addItem(AddCartItem $addCartItem): self
    {
        // …
    }
    
    public function removeItem(RemoveCartItem $removeCartItem): self
    {
        // …
    }
}    

Besides these new features, there are also some quality-of-life changes across the board:


All in all, I'm very exited for this new release. All the new features are also used in our real-life projects, so we know from experience how useful they are in complex applications. Of course, a blog post can't discuss all the details and the thought process behind this new version, so make sure to read the book if you want in-depth knowledge about all of these features, and more.

Noticed a tpyo? You can submit a PR to fix it. If you want to stay up to date about what's happening on this blog, you can follow me on Twitter or subscribe to my newsletter: