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!
Eventsourcing with Laravel
By Nastasi Christian
Eventsourcing with Laravel
- 237