My team at Spatie is currently building Mailcoach, a solution to self host your e-mail newsletter. Mailcoach can be used as stand alone software or as a Laravel package. Subscribe now at Mailcoach to get a notification as soon as we release it.

Implementing event sourcing: aggregates

Original – by Freek Van der Herten – 4 minute read

Recently we've released v2 of laravel-event-projector. The package is probably the easiest way to get started with event sourcing in Laravel.

One of the prominent new features is support for aggregates. While creating v2, I found it surprising that such a powerful concept could be implemented in so little code. In this short blog post, I'd like to explain how aggregates are coded up.

If you don't know anything about event sourcing or don't know what aggregates are, head over to the docs of the package. It contains an entire introduction targeted at newcomers. The rest of this post assumes that you know what aggregates are and that you know how to work with them.

Reconstituting an aggregate from previous events

Before you can work with an aggregate, it must be reconstituted from all previous events for a given uuid. As a package user, you can do this with MyAggreggate::retrieve($uuid). Here is the implementation of that function.

public static function retrieve(string $uuid): AggregateRoot
{
    $aggregateRoot = (new static());
    
    $aggregateRoot->aggregateUuid = $uuid;
    
    return $aggregateRoot->reconstituteFromEvents();
}

The $uuid will be kept as an instance variable aggregateUuid. The actual reconstituting happens in reconstituteFromEvents. Let's take a look at that code.

private function reconstituteFromEvents(): AggregateRoot
{
    StoredEvent::uuid($this->aggregateUuid)->each(function (StoredEvent $storedEvent) {
        $this->apply($storedEvent->event);
    });
    
    return $this;
}

That ::uuid is just a scope that defined on the StoredEventModel. That each call is being called on a Builder instance, not a Collection. Under the hood that each call will fetch all events in a chunked way.

Let's take a look at that apply method. It will convert the class name of an event to a method name. App\Event\MoneyAdded will be converted to applyMoneyAdded. If such a method with that name exists on the aggregate, it will get called with the event as the argument.

private function apply(ShouldBeStored $event): void
{
    $classBaseName = class_basename($event);
    
    $camelCasedBaseName = ucfirst(Str::camel($classBaseName));
    
    $applyingMethodName = "apply{$camelCasedBaseName}";
    
    if (method_exists($this, $applyingMethodName)) {
        $this->$applyingMethodName($event);
    }
}

And that's basically all the code needed to reconstitute an aggregate from past events.

Recording new events

When an aggregate is persisted all new events it recorded should be stored. This is very easy to accomplish. An aggregate offers a recordThat function to record new events.

public function recordThat(ShouldBeStored $event): AggregateRoot
{
    $this->recordedEvents[] = $event;
    
    $this->apply($domainEvent);
    
    return $this;
}

That function keeps the events in array on the instance. It also immediately applies it onto the aggregate (using that apply method we already reviewed above).

When calling persist on the aggregate, all the events will be passed to the storeMany method of the StoredEvent model. The $recordedEvents array will be emptied so we don't record the same events should persist be called again on this instance of the aggregate.

public function persist(): AggregateRoot
{
    StoredEvent::storeMany(
       $this->getAndClearRecoredEvents(),
       $this->aggregateUuid,
    );
           
    return $this;
}

private function getAndClearRecoredEvents(): array
{
    $recordedEvents = $this->recordedEvents;
    
    $this->recordedEvents = [];
    
    return $recordedEvents;
}

Because the actual storing of the events in storeMany isn't a responsibility of the aggregate, we're not going to go over it in detail. In short, storeMany will store the events, pass them to all registered projectors and will dispatch a job to pass them to all queued projectors and reactors.

And with that, we've implemented aggregates.

Stay up to date with all things Laravel, PHP, and JavaScript.

Follow me on Twitter. I regularly tweet out programming tips, and what I myself have learned in ongoing projects.

Every two weeks I send out a newsletter containing lots of interesting stuff for the modern PHP developer.

Expect quick tips & tricks, interesting tutorials, opinions and packages. Because I work with Laravel every day there is an emphasis on that framework.

Rest assured that I will only use your email address to send you the newsletter and will not use it for any other purposes.

Cyril de Wit liked on 9th July 2019
Sokkary liked on 9th July 2019
eCreeth liked on 9th July 2019
Arash liked on 9th July 2019
Sander Versluys liked on 9th July 2019
Albert liked on 9th July 2019
Andrés Herrera García liked on 9th July 2019
Maurizio Lepora liked on 9th July 2019
Didik Tri Susanto liked on 8th July 2019