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
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
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
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.
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.
È 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
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
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
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
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
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)
| 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 } |
spatie/laravel-event-sourcing
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"
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
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'),
];
}
}
class ProductRegistered extends ShouldBeStored
{
public function __construct(
readonly public string $productId,
readonly public string $name,
readonly public int $quantity,
readonly public int $price,
) {}
}
php artisan make:aggregate ProductAggregate
Autogeneriamo l'aggregato
class ProductAggregate extends AggregateRoot
{
}
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
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
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();
});
}
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'];
}
php artisan make:projector ProductProjector
Autogeneriamo il ProductProjector
class ProductProjector extends Projector
{
public function onEventHappened(EventHappened $event)
{
}
}
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
php artisan make:reactor ProductReactor
Autogeneriamo il ProductProjector
class ProductReactor extends Reactor implements ShouldQueue
{
public function onEventHappened(EventHappened $event)
{
}
}
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
cnastasi
cnastasi
@laravel-italia
@grusp
https://github.com/cnastasi/event-sourcing-with-laravel
https://joind.in/event/laravelday-2023/event-sourcing-con-laravel