Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

Laravel Event Sourcing

Avatar for Aurelia Lim Aurelia Lim
January 03, 2023

Laravel Event Sourcing

Deck for Singapore Laravel Meetup on 22 Dec 2022.

Event Sourcing is architectural pattern that records all changes made to an application’s state, in the sequence in which the changes were originally applied.

This talk covers the concept of event sourcing in the context of Laravel using spatie's package, Laravel Event Sourcing, as a springboard for discussion.

Avatar for Aurelia Lim

Aurelia Lim

January 03, 2023
Tweet

Other Decks in Technology

Transcript

  1. I want to purchase a product products id name inventory_level

    = 3 2 Who made the change When the change occurred What/How was the change performed ? ? ? ? ? ? ? ?
  2. Git Git Git log blame checkout Who made the change

    When the change occurred What/How was the change performed
  3. TLDR - each event is a transaction, and our application

    state can be derived from the event store. Event Sourcing
  4. It is an easy entry point into adopting the event

    sourcing architecture. Package built by spatie Laravel Event Sourcing
  5. Events Event Atomic in nature Represents a fact that took

    place Each change is recorded in an append only store Immutable - they are our source of truth Current application state is derived from events event_store
  6. Events Even though events are immutable, but their e ff

    ects can be altered by later events.
  7. Events Product Purchased Event Even though events are immutable, but

    their e ff ects can be altered by later events. products id name inventory_level
  8. Events Product Purchased Event Even though events are immutable, but

    their e ff ects can be altered by later events. products id name inventory_level Product Restocked Event products id name inventory_level
  9. <?php declare(strict_types=1); namespace App\Events; use Spatie\EventSourcing\StoredEvents\ShouldBeStored; class ProductPurchased extends ShouldBeStored

    { public $queue = 'purchase'; public function __construct( public readonly int $productId, public readonly int $quantity, ) { } }
  10. <?php declare(strict_types=1); namespace App\Events; use Spatie\EventSourcing\StoredEvents\ShouldBeStored; class ProductRestocked extends ShouldBeStored

    { public function __construct( public readonly int $productId, public readonly int $quantity, ) { } }
  11. Projectors Similar to event listeners. Transform the events into a

    format that is usable for our application Projectors
  12. Projectors Projections should be treated as temporary and disposable. This

    is one of their key bene f its as they can be destroyed, reimagined, and recreated at will; they should not be considered the source of truth. Projectors
  13. <?php declare(strict_types=1); namespace App\Projectors; use App\Events\ProductPurchased; use App\Events\ProductRestocked; use App\Models\Product;

    use App\Models\ProductPurchaseByDate; use Illuminate\Contracts\Queue\ShouldQueue; use Spatie\EventSourcing\EventHandlers\Projectors\Projector; class ProductInventoryProjector extends Projector implements ShouldQueue { public function onProductPurchased(ProductPurchased $event) { // Retrieve the product that was purchased $product = Product::find($event->productId); // Update the product's stock quantity $product->decrement('stock', $event->quantity); $product->save(); } public function onProductRestocked(ProductRestocked $event) { // Retrieve the product that was restocked $product = Product::find($event->productId); // Update the product's stock quantity $product->increment(‘inventory_level', $event->quantity); $product->save(); } }
  14. <?php declare(strict_types=1); namespace App\Projectors; use App\Events\ProductPurchased; use App\Events\ProductRestocked; use App\Models\Product;

    use App\Models\ProductPurchaseByDate; use Illuminate\Contracts\Queue\ShouldQueue; use Spatie\EventSourcing\EventHandlers\Projectors\Projector; class ProductInventoryProjector extends Projector implements ShouldQueue { public function onProductPurchased(ProductPurchased $event) { // Retrieve the product that was purchased $product = Product::find($event->productId); // Update the product's inventory quantity $product->decrement(‘inventory_level', $event->quantity); $product->save(); } public function onProductRestocked(ProductRestocked $event) { // Retrieve the product that was restocked $product = Product::find($event->productId); // Update the product's stock quantity $product->decrement('stock', $event->quantity); $product->save(); } }
  15. <?php declare(strict_types=1); namespace App\Providers; use App\Projectors\ProductInventoryProjector; use Illuminate\Support\ServiceProvider; use Spatie\EventSourcing\Facades\Projectionist;

    class EventSourcingServiceProvider extends ServiceProvider { public function register() { Projectionist::addProjector(ProductInventoryProjector::class); } }
  16. <?php declare(strict_types=1); namespace App\Http\Controllers; use App\Events\ProductPurchased; use App\Http\Controllers\Controller; use Illuminate\Http\JsonResponse;

    class ProductPurchaseController extends Controller { public function __invoke(PurchaseRequest $request): JsonResponse { event(new ProductPurchased($request->product_id, $request->quantity)); return new JsonResponse([ 'data' => [ 'message' => 'product purchased', ], ]); } }
  17. Hi Lori We need to implement product analytics We need

    to provide meaningful insights on consumer behaviour. What products are most popular? What time period are consumers buying the most? 2:30PM
  18. <?php declare(strict_types=1); namespace App\Projectors; use App\Events\ProductPurchased; use App\Events\ProductRestocked; use App\Models\Product;

    use App\Models\ProductPurchaseByDate; use Illuminate\Contracts\Queue\ShouldQueue; use Spatie\EventSourcing\EventHandlers\Projectors\Projector; class ProductInventoryProjector extends Projector implements ShouldQueue { public function onProductPurchased(ProductPurchased $event) { // Retrieve the product that was purchased $product = Product::find($event->productId); // Update the product's inventory quantity $product->decrement(‘inventory_level', $event->quantity); $product->save(); //Store some consumer analytics ProductPurchaseByDate::firstOrCreate( [ 'date' => $event->createdAt()->toDateString(), 'merchant_id' => $product->merchant_id, 'product_id' => $product->id, ], )->increment('purchase_count'); } public function onProductRestocked(ProductRestocked $event) { // Retrieve the product that was restocked $product = Product::find($event->productId); // Update the product's stock quantity $product->decrement('stock', $event->quantity); $product->save(); } }
  19. ╭─[Aurelias-MBP] as aurelialim in ~/Sites/example-event-sourcing using node v19.2.0 01:29:07 ╰──➤

    sail artisan event-sourcing:replay Are you sure you want to replay events to all projectors? (yes/no) [yes]: > yes Replaying 6 events... 6/6 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100% All done! ~4s (base)
  20. ProductPurchased Event event_store ProductPurchased event 1 ProductPurchased event 2 ProductPurchased

    event 3 I want to purchase a product API call to purchase a product ProductInventory Projector Products Id name Inventory
  21. Aggregates An aggregate is a class that helps you to

    make decisions based on events that happened in the past. Aggregates
  22. <?php declare(strict_types=1); namespace App\Aggregates; use App\Events\ProductPurchased; use App\Reactors\PopularProductIdentified; use Spatie\EventSourcing\AggregateRoots\AggregateRoot;

    class ProductInventoryAggregate extends AggregateRoot { private $productPurchasedCount = 0; // we need to add this method to count the amount of this the product was purchased public function applyProductPurchased() { $this->productPurchasedCount++; } public function purchaseProduct(int $productId, int $quantity ) { $this->recordThat(new ProductPurchased($productId, $quantity)); if ($this->productPurchasedCount === 8) { $this->recordThat(new PopularProductIdentified($productId)); } return $this; } }
  23. <?php declare(strict_types=1); namespace App\Http\Controllers; use App\Aggregates\ProductInventoryAggregate; use App\Http\Controllers\Controller; use Illuminate\Http\JsonResponse;

    class ProductPurchaseController extends Controller { public function __invoke(PurchaseRequest $request): JsonResponse { //instead of dispatching the event here, we let the aggregate do it for us // we used productId as the uuid $productId = $request->input('product_id'); ProductInventoryAggregate::retrieve($productId) ->purchaseProduct($productId, $request->quantity) ->persist(); } }
  24. ProductPurchased Event event_store ProductPurchased event 1 ProductPurchased event 2 ProductPurchased

    event 3 I want to purchase a product API call to purchase a product ProductInventory Projector Products Id name Inventory ProductInventory Aggregate
  25. Reactors Handle the side e ff ects of our applications.

    only get called when the original event f ires. Reactors
  26. PopularProductIdenti f ied Event I want to purchase a product

    API call to purchase a product ProductInventory Aggregate Reactor Send an email to notify the merchant that their product has been purchased x amount of times
  27. <?php declare(strict_types=1); namespace App\Reactors; use App\Events\PopularProductIdentified; use App\Mail\PopularProductIdentifiedMail; use Illuminate\Contracts\Queue\ShouldQueue;

    use Illuminate\Support\Facades\Mail; use Spatie\EventSourcing\EventHandlers\Reactors\Reactor; class PopularProductIdentifiedReactor extends Reactor implements ShouldQueue { public function __invoke(PopularProductIdentified $event) { $product = Product::find($event->productId); $email = $product->merchant->email; Mail::to($email)->send(new PopularProductIdentifiedMail($product)); } }