Event-sourcing with Laravel

Mi presento

IT Training Manager @Facile.it

Sviluppo prodotti digitali dal 2000

Metà della mia carriera dedicata al mondo PHP

Specializzato in tematiche di design & architetture del software

cnastasi

cnastasi

@laravel-italia

@grusp

Amante delle t-shirt nerd

Di cosa parleremo?

Del fatto che implementare un'architettura Event Sourced è meno complesso di quello che si pensi

Dell'esistenza di una libreria per Laravel che ci semplifica ulteriormente la vita

Dei pro ed i contro di utilizzare questo tipo di architettura

Quanti di voi hanno mai sentito parlare di EVENT sourcing?

Quanti di voi hanno sviluppato o sviluppano in EVENT sourcing?

Scenario

E-commerce 

Esempio di sviluppo "classico"

Prodotti

| ID Prodotto | Nome Prodotto | Quantità Disponibile | Prezzo Unitario |
|-------------|---------------|----------------------|-----------------|
| 1           | Prodotto A    | 10                   | $20             |
| 2           | Prodotto B    | 8                    | $30             |
| 3           | Prodotto C    | 15                   | $15             |
| ID Ordine | ID Prodotto | Quantità Ordinata | Totale Ordine |
|-----------|-------------|-------------------|---------------|
| 1         | 1           | 5                 | $100          |
| 2         | 2           | 3                 | $90           |
| 3         | 3           | 2                 | $30           |

Ordini

Esempio

I clienti del mio e-commerce cambiano spesso la quantità dell'ordine 

 

Il mio capo vorrebbe sapere quanto frequentemente questo avviene

MA

Ma il dato della quantità è unico e non è storicizzato.

Alcuni problemi

Rappresenta bene lo stato attuale del tuo sistema

MA

Perde ogni connotazione temporale

Recuperare informazioni non salvate è complesso

(es. Analisi statistiche, behaviour degli utenti, ecc...)

E' molto complesso recuperare informazioni corrotte da bug.

VERSIONE EVENT-SOURCED

E-commerce 

USE CASE: Registrazione prodotto

Aggregate

È l'entry point del dominio

Gestisce e garantisce la corretta applicazione della logica di business

Emette eventi

Definiamo l'aggregato del nostro dominio

È versionato

Può essere visto come un automa a stati finiti

Command

Definisce un operazione / azione da fare su un determinato dominio

È una struttura dati semplice, con dentro i dati operativi necessari

Può essere sincrono o asincrono

Se non serve l'asincronicità, può essere sostituito da un metodo nell'aggregato

L'aggregato può eseguire azioni attraverso dei comandi

Event

Se il comando va a buon fine, l'aggregato genererà un evento.

Rappresenta un cambio di stato avvenuto nel sistema

Deve essere memorizzato

Ha al suo interno una struttura dati semplice, ma ha tutte le informazioni necessarie

Contiene al suo interno l'identificativo dell'aggregato

Event-store

L'evento viene persistito in un event store

È la componente più importante del nostro sistema

Funge da sorgente di verità

Deve garantire atomicità e consistenza nella persistenza degli eventi

Projectors / Projections / Views

L'evento viene anche propagato tramite un event bus e ricevuto da eventuali listeners

Reagisce in base agli eventi del dominio

Produce viste denormalizzate

(chiamate proiezioni)

Possono "replicare" gli eventi passati, all'occorrenza, senza particolari side-effects

Reactors / Policies

Uno di questi listener andrà a riempire una tabella di supporto con dentro le informazioni necessarie ad una vista utente

Logica che reagisce in base agli eventi del dominio

Possono dare luogo a side effects
(come invio notifiche/mail/chiamate HTTP)

Possono a loro volta emettere eventi o comandi

Se rieseguiti, potrebbero creare problematiche

(necessaria la gestione dell'idempotenza)

Riepilogo

Dentro all'event store

| ID | Aggr | Tipo               | Ver | Dati                                                                                                    |
|-----------|------------------- |-----|---------------------------------------------------------------------------------------------------------|
| 1  | 1    | ProdottoRegistrato | 1   | { "ID Prodotto": 1, "Nome Prodotto": "Prodotto D", "Quantità Disponibile": 20, "Prezzo Unitario": $25 } |
| 2  | 1    | ProdottoAcquistato | 2   | { "ID Prodotto": 1, "Quantità Ordinata": 5, "Totale Ordine": $100 }                                     |
| 3  | 2    | ProdottoRegistrato | 1   | { "ID Prodotto": 2, "Nome Prodotto": "Prodotto E", "Quantità Disponibile": 15, "Prezzo Unitario": $35 } |
| 4  | 2    | ProdottoAcquistato | 2   | { "ID Prodotto": 2, "Quantità Ordinata": 3, "Totale Ordine": $90 }                                      |
| 5  | 3    | ProdottoRegistrato | 1   | { "ID Prodotto": 3, "Nome Prodotto": "Prodotto F", "Quantità Disponibile": 18, "Prezzo Unitario": $40 } |
| 6  | 3    | ProdottoAcquistato | 2   | { "ID Prodotto": 3, "Quantità Ordinata": 2, "Totale Ordine": $30 }                                      |

USE CASE: ordina prodotto

Presentazione Libreria

spatie/laravel-event-sourcing

Utilizzo della libreria

composer require spatie/laravel-event-sourcing

Installazione e setup

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

Implementiamo l'esempio

E-commerce 

VISIONE DI INSIEME

ComMand

readonly class RegisterProduct
{
    public function __construct(
        public string $name,
        public int    $quantity,
        public int    $price,
    ) {
        $this->validate();
    }

    private function validate():void { /* Validation logic */ }
}

Implementazione RegisterProduct

EVENT

php artisan make:event ProductRegistered

Auto generiamo il codice

class ProductRegistered
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public function __construct() { }

    public function broadcastOn(): array
    {
        return [
            new PrivateChannel('channel-name'),
        ];
    }
}

EVENT

class ProductRegistered extends ShouldBeStored
{
    public function __construct(
        readonly public string $productId,
        readonly public string $name,
        readonly public int    $quantity,
        readonly public int    $price,
    ) {}
}

AGGREGATE

php artisan make:aggregate ProductAggregate

Autogeneriamo l'aggregato

class ProductAggregate extends AggregateRoot
{
}

Aggregate

class ProductAggregate extends AggregateRoot
{
    public function registerProduct(RegisterProduct $command): static
    {
        $this->recordThat(
            new ProductRegistered(
                $this->uuid(),
                $command->name,
                $command->quantity,
                $command->price
            )
        );
    
        return $this;
    }
}

Implementare l'aggregate

VISIONE DI INSIEME

MODEL + MIGRATION

php artisan make:model -m Product

Autogeneriamo il modello e la migration

class Product extends Model
{
    use HasFactory;
}

Modello

class extends Migration
{
    public function up(): void
    {
        Schema::create('products', function (Blueprint $table) {
            $table->id();
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('products');
    }
};

Migrazione

MIGRATION

Implementiamo la migrazione

public function up(): void
{
    Schema::create('products', function (Blueprint $table) {
        $table->id();
        $table->uuid('productId')->unique();
        $table->string('name');
        $table->integer('quantity');
        $table->integer('price');
       $table->timestamps();
    });
}

MODEL

Miglioriamo il modello

/**
 * @property string $productId
 * @property string $name
 * @property int $quantity
 * @property int $price
 */
class Product extends Model
{
    protected $fillable = ['productId', 'name', 'quantity', 'price'];
}

PROJECTOR

php artisan make:projector ProductProjector

Autogeneriamo il ProductProjector

class ProductProjector extends Projector
{
    public function onEventHappened(EventHappened $event)
    {
    }
}

PROJECTOR

class ProductProjector extends Projector
{
    public function onProductRegistered(ProductRegistered $event)
    {
         $data = (array)$event;

         Product::create($data);
    }
}

Ci basta aggiungere l'evento nella firma. That's it

La libreria fa auto-discover, non c'è bisogno di configurazioni particolari

REACTOR

php artisan make:reactor ProductReactor

Autogeneriamo il ProductProjector

class ProductReactor extends Reactor implements ShouldQueue
{
    public function onEventHappened(EventHappened $event)
    {
    }
}

REACTOR

class ProductReactor extends Reactor implements ShouldQueue
{
    public function onProductRegistered(ProductRegistered $event)
    {
        // Sending mail logic
    }
}

Ci basta aggiungere l'evento nella firma. That's it

La libreria fa auto-discover, non c'è bisogno di configurazioni particolari

E' tutto rosa e fiori?

DEMO TIME

Q&A

cnastasi

cnastasi

@laravel-italia

@grusp

https://github.com/cnastasi/event-sourcing-with-laravel

https://joind.in/event/laravelday-2023/event-sourcing-con-laravel

GRazie!