BlogEvent sourcing in Laravel: voordelen, events, aggregates en projections

Event sourcing in Laravel: voordelen, events, aggregates en projections

Event sourcing is in Laravel interessant zodra je meer wilt dan alleen de huidige status van een record opslaan. In plaats van steeds de laatste toestand te bewaren, leg je elke relevante intentie vast als event. Daardoor krijg je volledige historie, kun je eenvoudiger debuggen en bouw je complexere domeinlogica zonder dat je database-tabellen steeds zwaarder worden.

Waarom event sourcing in Laravel voordelen heeft

Het grootste voordeel is inzicht. Je ziet niet alleen dat een bestelling of abonnement nu actief is, maar ook welke stappen daartoe hebben geleid. Dat helpt bij audits, debugging en rapportages.

Daarnaast maakt event sourcing het makkelijker om write-logica en read-logica van elkaar te scheiden. Je intenties leven in aggregates en events, terwijl projections geoptimaliseerde tabellen opbouwen voor queries in je applicatie. Zeker bij SaaS-platformen of backoffices met veel statusovergangen geeft dat rust in je codebase.

Je kan ook later, op basis van de opgeslagen intenties, rapportages maken die eerder niet bestonden. Op die manier geeft event sourcing nieuwe inzichten, ook later in het project.

Spatie event sourcing installeren met Composer

Voor Laravel is het package van Spatie een pragmatische keuze. Je installeert het met Composer:

composer require spatie/laravel-event-sourcing

Publiceer daarna de migraties en configuratiebestanden en voer de migraties uit:

php artisan vendor:publish --provider="Spatie\EventSourcing\EventSourcingServiceProvider" --tag="event-sourcing-migrations"
php artisan vendor:publish --provider="Spatie\EventSourcing\EventSourcingServiceProvider" --tag="event-sourcing-config"
php artisan migrate

Daarmee ontvang je de tabellen voor de opgeslagen evenementen. Vanaf dat moment kan Laravel je evenementen duurzaam vastleggen en opnieuw afspelen wanneer dat nodig is.

Projection models opzetten

Een projection model is het model dat je applicatie gebruikt voor schermen, filters en exports. In veel projecten is dit een gewone Eloquent-tabel die door events wordt bijgewerkt. Stel dat je abonnementen wilt volgen, dan kun je bijvoorbeeld een projection model voor abonnementen maken:

<?php
declare(strict_types=1);

namespace App\Models;

class Subscription extends Projection
{
    protected $guarded = [];
}

Dit model is erg kaal met een reden; alle intenties lopen via het aggregate. Alle mutaties vinden plaats in de projector. Als je de projector en events goed inricht, zie je alle data terug in elke intentie. Projecties zijn daarnaast niet te muteren zonder expliciet ->writeable() aan te roepen, dus is een mutatie echt een keuze.

Events maken

Definieer de gebeurtenissen die in je domein belangrijk zijn. Denk aan een gebruiker die zich registreert, een abonnement dat start of een factuur die wordt betaald. Zo'n event bevat alleen de data die nodig is om de gebeurtenis te beschrijven:

<?php
declare(strict_types=1);

namespace App\Subscriptions\Events;

use Spatie\EventSourcing\StoredEvents\ShouldBeStored;

class SubscriptionCreated extends ShouldBeStored
{
    public function __construct(
        public readonly string $uuid,
        public readonly string $email,
        public readonly string $plan,
    ) {
      //
    }
}

Het belangrijke verschil met een standaard Laravel event is dat deze events worden opgeslagen als bron van waarheid. Je modelstatus volgt dus uit de events, niet andersom. Zorg er daarom voor dat je evenement alle benodigde data in de constructor heeft zitten. De projector moet de wijziging reflecteren op basis van deze properties.

Aggregates gebruiken voor domeinlogica

Een aggregate bepaalt welke events wel of niet mogen plaatsvinden. Daar hoort businesslogica thuis, zoals het valideren van een statusovergang of het voorkomen van dubbele acties. In Spatie maak je daarvoor meestal een aggregate root:

<?php
declare(strict_types=1);

namespace App\Aggregates;

use App\Events\Subscription\SubscriptionCreated;
use Spatie\EventSourcing\AggregateRoots\AggregateRoot;

class SubscriptionAggregate extends AggregateRoot
{
    public function create(string $uuid, string $email, string $plan): self
    {
        $this->recordThat(new SubscriptionCreated(
            uuid: $uuid,
            email: $email,
            plan: $plan,
        ));

        return $this;
    }
}

Vanuit een action, controller of service laad je vervolgens de aggregate in op basis van het UUID, voer je de domeinactie uit en sla je de nieuwe events op. Daarmee houdt je complexe beslissingen uit je controllers.

Projections bijwerken vanuit events

Tot slot koppel je een projector aan je events, zodat je projection models automatisch worden bijgewerkt. Daarmee vertaal je domeinevents naar een leesmodel dat perfect is voor dashboards en indexpagina's:

<?php
declare(strict_types=1);

namespace App\Projectors;

use App\Domain\Subscriptions\Events\SubscriptionCreated;
use App\Models\Subscription;
use Spatie\EventSourcing\EventHandlers\Projectors\Projector;

class SubscriptionProjector extends Projector
{
    public function onSubscriptionCreated(SubscriptionCreated $event): void
    {
        (new Subscription)
            ->writeable()
            ->fill([
                'uuid' => $event->uuid,
                'email' => $event->email,
                'plan' => $event->plan,
                'status' => 'active',
            ])
            ->save();
    }
}

Hier zie je waarom event sourcing prettig schaalt. Je kunt later extra projections toevoegen voor rapportages, notificaties of audit-overzichten zonder je bestaande domeinlogica opnieuw te ontwerpen. Je events blijven hetzelfde, maar de afgeleide modellen kunnen meegroeien met nieuwe productvragen.

Wanneer dit in Laravel een goede keuze is

Event sourcing is niet voor elk CRUD-project nodig. Maar zodra je veel statusovergangen, audit-eisen, integraties of complexe bedrijfsregels hebt, kan het een sterk fundament zijn. Je krijgt beter zicht op wat er in het systeem gebeurt en je houdt domeinlogica expliciet en testbaar.

Bij Snoeren Development maken we veel gebruik van Spatie Event Sourcing om niet alleen de huidige staat, maar ook de weg ernaar toe op te slaan. Dit geeft ontzettend veel inzicht en bevordert de veiligheid van het systeem. Als je alle intenties, dus niet alleen mutaties, vastlegd geeft dat een helder beeld van je applicatie. Je kan bijvoorbeeld opslaan dat een gebruiker 3x een actie heeft geprobeerd uit te voeren en daar weer logica aan hangen.