一文詳解Laravel中的事件溯源

一文詳解Laravel中的事件溯源

事件溯源是一個在過去幾年 PHP 社區越來越流行的術語,但對很多開發人員來說仍然是個謎。這些問題總是如何以及為什么,當然這是可以理解的。本教程的目的是通過一個實際的方式不僅僅是幫你理解什么是事件溯源,同樣也會讓你知道什么時候你可能會想使用它。

在傳統應用程序中,我們的應用程序狀態直接表示在我們所連接的數據庫中。我們不能夠完全理解它是怎么到那里的。我們只知道他是這樣的。我們可以通過一些方法來進一步理解一點,使用審計模型更改的工具,這樣我們就可以看到更改了什么,是誰做的。 這也是朝著正確方向邁出的一步。然而,我們仍然不明白這個關鍵的問題。【相關推薦:laravel視頻教程

為什么?為什么這個模型發生了變化?這改變的目的是什么?

這就是事件溯源發揮作用的地方,它保留了應用程序狀態發生了什么以及為什么發生變化的歷史視圖。事件溯源允許你根據過去做出決定,從而你能夠生成報告。但是在基本層面,他能讓你知道為什么這個應用的狀態改變了,這是通過事件完成的。

我將會構建一個基礎的 laravel 項目來引導你理解它是如何工作的。我們會將這個應用構建的簡單些,以便你能理解時間溯源的邏輯而不是對于應用程序邏輯困惑。我們正在建立一個可以慶祝團隊成員的應用程序。這就對了。簡單而易于理解。我們與用戶有團隊,并且我們希望能夠在團隊中公開慶祝一些東西。

我們將新建一個 Laravel 項目,但我將使用 Jetstream,因為我想啟用身份驗證和團隊結構和功能。 一旦你創建了這個項目, 請在你的IDE中打開它。(這里的正確答案當然是 PHPStorm ), 現在我們已經準備好在 Laravel 深入事件溯源。

我們希望為應用程序創建一個附加模型,這是唯一的一個。這是一個 Celebration 模型,你可以使用以下的Artisan命令創建它:

php?artisan?make:model?Celebration?-m

修改你的遷移文件 up 方法,它看起來應該像這樣:

public?function?up():?void { ????Schema::create('celebrations',?static?function?(Blueprint?$table):?void?{ ????????$table->id();  ????????$table->string('reason'); ????????$table->text('message')->nullable();  ????????$table ????????????->foreignId('user_id') ????????????->index() ????????????->constrained() ????????????->cascadeOnDelete();  ????????$table ????????????->foreignId('sender_id') ????????????->index() ????????????->constrained('users') ????????????->cascadeOnDelete();  ????????$table ????????????->foreignId('team_id') ????????????->index() ????????????->constrained() ????????????->cascadeOnDelete();  ????????$table->timestamps(); ????}); }

我們有一個慶祝的原因?reason,一個簡單的句子,然后是我們可能希望與慶?;顒影l送的可選消?message。除此之外,我們有三個關系,正在慶祝的用戶,發送慶祝的用戶,以及他們所在的團隊。使用 Jetstream,一個用戶可以屬于多個團隊,并且可能存在兩個用戶在同一個團隊中的情況
,我們要確保我們在正確的團隊中公開慶祝他們。

一旦我們有了這個設定,讓我們看看模型本身:

declare(strict_types=1);  namespace?AppModels;  use?IlluminateDatabaseEloquentFactoriesHasFactory; use?IlluminateDatabaseEloquentModel; use?IlluminateDatabaseEloquentRelationsBelongsTo; use?IlluminateDatabaseEloquentRelationsHasMany;  final?class?Celebration?extends?Model { ????use?HasFactory;  ????protected?$fillable?=?[ ????????'reason', ????????'message', ????????'user_id', ????????'sender_id', ????????'team_id', ????];  ????public?function?user():?BelongsTo ????{ ????????return?$this->belongsTo( ????????????related:?User::class, ????????????foreignKey:?'user_id', ????????); ????}  ????public?function?sender():?BelongsTo ????{ ????????return?$this->belongsTo( ????????????related:?User::class, ????????????foreignKey:?'sender_id', ????????); ????}  ????public?function?team():?BelongsTo ????{ ????????return?$this->belongsTo( ????????????related:?Team::class, ????????????foreignKey:?'team_id', ????????); ????} }

我們可以將這些關系映射到其他相關的模型上。盡管,默認情況下,我會將關聯關系的另一端添加到每一個模型中,使它們的關聯關系更加清楚,無論它是否嚴格需要這些關聯關系。這是我養成的習慣,為了幫助他人理解模型本身。

現在我們有了從模型視角創建的我們的應用基礎。我想我們需要一些安裝一些對我們有幫助的軟件包(依賴)。對于我的應用,我使用?laravel視頻教程?來控制 UI 。但是我并不會在本教程詳細介紹這個包,因為我想確保我能專注于講事件溯源這個方面。

與我創建的大多數項目一樣,無論大小,我都為應用程序采用了模塊化布局——一個領域驅動模型設計 ( Domain Driven Design ) 方法。這只是我做的事情,不要覺得你自己必須遵循這個,因為它是非常主觀的。

我的下一步是設置我的域,對于這個演示,我只有一個域:文化。在文化中,我為我可能需要的一切創建了名稱空間。但我會經歷它,這樣你就明白了過程

第一步是安裝一個軟件包,使我能夠在Laravel中使用事件源。為此,我使用了一個?laravel視頻教程包,它為我做了大量的后臺工作

composer?require?spatie/laravel-event-sourcing

安裝后,請確保按照包的安裝說明進行操作,因為配置和遷移需要發布。正確安裝后,運行遷移,使數據庫處于正確狀態。

php?artisan?migrate

現在我們可以開始思考我們要如何實現事件溯源。你可以通過兩種方式實現這一點:投影儀來投影你的狀態或聚合。

Projector 是一個位于你的應用程序中并處理你調度的事件的類。然后,這些將更改你的應用程序的狀態。這不僅僅是簡單地更新你的數據庫。它位于中間,捕獲一個事件,存儲它,然后進行所需的更改 —— 然后 “投射” 應用程序的新狀態

另一種方法,我的首選方法,聚合 – 這些是像投影儀一樣為你處理應用程序狀態的類。我們不是在我們的應用程序中自己觸發事件,而是將其留給聚合為我們做。把它想象成一個中繼,你要求中繼做某事,它會為你處理。

在我們創建第一個聚合之前,需要在后臺做一些工作。我非常喜歡為每個聚合創建一個事件存儲,以便查詢更快,并且該存儲不會很快填滿。這在包文檔中進行了解釋,但我將親自引導你完成它,因為它在文檔中并不是最清楚的。

第一步是創建模型和遷移,因為你將來需要一種方法來查詢它以進行報告等。運行以下 artisan 命令來創建這些:

php?artisan?make:model?CelebrationStoredEvent?-m

以下代碼是你在 up 方法中進行遷移所需的代碼:

public?function?up():?void { ????Schema::create('celebration_stored_events',?static?function?(Blueprint?$table):?void?{ ????????$table->id(); ????????$table->uuid('aggregate_uuid')->nullable()->unique(); ????????$table ????????->unsignedBigInteger('aggregate_version') ????????->nullable() ????????->unique(); ????????$table->integer('event_version')->default(1); ????????$table->string('event_class');  ????????$table->json('event_properties');  ????????$table->json('meta_data');  ????????$table->timestamp('created_at');  ????????$table->index('event_class'); ????????$table->index('aggregate_uuid'); ????}); }

如你所見,我們為我們的活動收集了大量數據?,F在模型要簡單得多。它應該如下所示:

declare(strict_types=1);  namespace?AppModels;  use?SpatieEventSourcingStoredEventsModelsEloquentStoredEvent;  final?class?CelebrationStoredEvent?extends?EloquentStoredEvent { ????public?$table?=?'celebration_stored_events'; }

當我們擴展?EloquentStoredEvent?模型時,我們需要做的就是改變它正在查看的表。模型的其余功能已經在父級上到位。

要使用這些模型,你必須創建一個存儲庫來查詢事件。這是一個非常簡單的存儲庫 —— 然而,這是一個重要的步驟。我將我的添加到我的域代碼中,位于?src/Domains/Culture/Repositories/?下,但您可以隨意添加對您最有意義的位置:

declare(strict_types=1);  namespace?DomainsCultureRepositories;  use?AppModelsCelebrationStoredEvent; use?SpatieEventSourcingStoredEventsRepositoriesEloquentStoredEventRepository;  final?class?CelebrationStoredEventsRepository?extends?EloquentStoredEventRepository { ????public?function?__construct( ????????protected?string?$storedEventModel?=?CelebrationStoredEvent::class, ????)?{ ????????parent::__construct(); ????} }

既然我們有了存儲事件和查詢它們的方法,我們可以繼續我們的聚合本身。同樣,我將我的存儲在我的域中,但可以隨意將你的存儲在你的應用程序上下文中。

declare(strict_types=1);  namespace?DomainsCultureAggregates;  use?DomainsCultureRepositoriesCelebrationStoredEventsRepository; use?SpatieEventSourcingAggregateRootsAggregateRoot; use?SpatieEventSourcingStoredEventsRepositoriesStoredEventRepository;  final?class?CelebrationAggregateRoot?extends?AggregateRoot { ????protected?function?getStoredEventRepository():?StoredEventRepository ????{ ????????return?app()->make( ????????????abstract:?CelebrationStoredEventsRepository::class, ????????); ????} }

到目前為止,除了為我們連接到正確的事件存儲之外,此聚合不會執行任何操作。要讓它開始跟蹤事件,我們首先需要創建它們。但在此之前,我們需要停下來想一想。我們希望在活動中存儲哪些數據?我們想要存儲我們需要的每一個屬性嗎?或者我們是否希望存儲一個數組,就像它來自一個表單一樣?我兩種方法都不用,因為為什么要保持簡單呢?我在所有事件中使用數據傳輸對象,以確保始終維護上下文并始終提供類型安全。

我構建了一個軟件包,讓我做這件事更容易。可以通過以下 Composer 命令安裝它:

composer?require?juststeveking/laravel-data-object-tools

和以前一樣, 我默認將我的數據對象保存在我的領域, 但你可以添加到對你最有意義的地方。 我創建了一個名為 Celebration?的數據對象,可以傳遞給事件和聚合器:

declare(strict_types=1);  namespace?DomainsCultureDataObjects;  use?JustSteveKingDataObjectsContractsDataObjectContract;  final?class?Celebration?implements?DataObjectContract { ????public?function?__construct( ????????private?readonly?string?$reason, ????????private?readonly?string?$message, ????????private?readonly?int?$user, ????????private?readonly?int?$sender, ????????private?readonly?int?$team, ????)?{}  ????public?function?userID():?int ????{ ????????return?$this->user; ????}  ????public?function?senderID():?int ????{ ????????return?$this->sender; ????}  ????public?function?teamUD():?int ????{ ????????return?$this->team; ????}  ????public?function?toArray():?array ????{ ????????return?[ ????????????'reason'?=>?$this->reason, ????????????'message'?=>?$this->message, ????????????'user_id'?=>?$this->user, ????????????'sender_id'?=>?$this->sender, ????????????'team_id'?=>?$this->team, ????????]; ????} }

當我升級到 PHP 8.2 時,這會容易得多,因為我可以創建只讀類 – 是的,我的包已經支持它們。

現在我們有了我們的數據對象。我們可以回到我們想要存儲的事件。我已經調用了我的CelebrationWasCreated,因為事件名稱應該總是過去時。讓我們看看這個事件:

declare(strict_types=1);  namespace?DomainsCultureEvents;  use?DomainsCultureDataObjectsCelebration; use?SpatieEventSourcingStoredEventsShouldBeStored;  final?class?CelebrationWasCreated?extends?ShouldBeStored { ????public?function?__construct( ????????public?readonly?Celebration?$celebration, ????)?{} }

因為我們使用的是數據對象,所以我們的類保持干凈。所以,現在我們有了一個事件——以及一個可以發送的數據對象,我們需要考慮如何觸發它。這讓我們回到了聚合本身,所以讓我們在聚合上創建一個可以用于此目的的方法:

declare(strict_types=1);  namespace?DomainsCultureAggregates;  use?DomainsCultureDataObjectsCelebration; use?DomainsCultureEventsCelebrationWasCreated; use?DomainsCultureRepositoriesCelebrationStoredEventsRepository; use?SpatieEventSourcingAggregateRootsAggregateRoot; use?SpatieEventSourcingStoredEventsRepositoriesStoredEventRepository;  final?class?CelebrationAggregateRoot?extends?AggregateRoot { ????protected?function?getStoredEventRepository():?StoredEventRepository ????{ ????????return?app()->make( ????????????abstract:?CelebrationStoredEventsRepository::class, ????????); ????}  ????public?function?createCelebration(Celebration?$celebration):?CelebrationAggregateRoot ????{ ????????$this->recordThat( ????????????domainEvent:?new?CelebrationWasCreated( ????????????????celebration:?$celebration, ????????????), ????????);  ????????return?$this; ????} }

在這一點上,我們有一種方法來要求一個類記錄事件。但是,這一事件還不會持續下去 —— 那是以后的事。此外,我們不會以任何方式改變應用程序的狀態。那么,我們該如何做這項活動采購工作呢?這一部分是關于 Livewire 中的實現的,我現在將向你介紹它。

我喜歡通過調度一個事件來管理這個過程,因為它更高效。如果你考慮如何與應用程序交互,你可以從 Web 訪問它,通過 API 端點發送請求,或者發生 CLI 命令可能運行的事件 —— 可能是一個 Cron 作業。在所有這些方法中,通常,你需要即時響應,或者至少您不想等待。我將在我的 Livewire 組件上向你展示我為此使用的方法:

public?function?celebrate():?void { ????$this->validate();  ????dispatch(new?TeamMemberCelebration( ????????celebration:?Hydrator::fill( ????????????class:?Celebration::class, ????????????properties:?[ ????????????????'reason'?=>?$this->reason, ????????????????'message'?=>?$this->content, ????????????????'user'?=>?$this->identifier, ????????????????'sender'?=>?auth()->id(), ????????????????'team'?=>?auth()->user()->current_team_id, ????????????] ????????), ????));  ????$this->closeModal(); }

在這一點上,我們有一種方法來要求一個類記錄事件。但是,這一事件還不會持續下去 —— 那是以后的事。此外,我們不會以任何方式改變應用程序的狀態。那么,我們該如何做這項活動采購工作呢?這一部分是關于 Livewire 中的實現的,我現在將向你介紹它。

我喜歡通過調度一個事件來管理這個過程,因為它更高效。如果你考慮如何與應用程序交互,你可以從 Web 訪問它,通過 API 端點發送請求,或者發生 CLI 命令可能運行的事件 —— 可能是一個 Cron 作業。在所有這些方法中,通常,你需要即時響應,或者至少你不想等待。我將在我的 Livewire 組件上向你展示我為此使用的方法:

public?function?celebrate():?void { ????$this->validate();  ????dispatch(new?TeamMemberCelebration( ????????celebration:?Hydrator::fill( ????????????class:?Celebration::class, ????????????properties:?[ ????????????????'reason'?=>?$this->reason, ????????????????'message'?=>?$this->content, ????????????????'user'?=>?$this->identifier, ????????????????'sender'?=>?auth()->id(), ????????????????'team'?=>?auth()->user()->current_team_id, ????????????] ????????), ????));  ????$this->closeModal(); }

當我驗證來自組件的用戶輸入,可以分派處理一個新的作業,然后結束這個流程。我使用我的包將一個新的數據對象傳遞給作業。它有一個 Facade,可以讓我用一系列屬性來為類添加——到目前為止它工作得很好。那么這是怎么實現的呢?讓我們來看看。

declare(strict_types=1);  namespace?AppJobsTeam;  use?DomainsCultureAggregatesCelebrationAggregateRoot; use?DomainsCultureDataObjectsCelebration; use?IlluminateBusQueueable; use?IlluminateContractsQueueShouldQueue; use?IlluminateFoundationBusDispatchable; use?IlluminateQueueInteractsWithQueue; use?IlluminateQueueSerializesModels; use?IlluminateSupportStr;  final?class?TeamMemberCelebration?implements?ShouldQueue { ????use?Queueable; ????use?Dispatchable; ????use?SerializesModels; ????use?InteractsWithQueue;  ????public?function?__construct( ????????public?readonly?Celebration?$celebration, ????)?{}  ????public?function?handle():?void ????{ ????????CelebrationAggregateRoot::retrieve( ????????????uuid:?Str::uuid()->toString(), ????????)->createCelebration( ????????????celebration:?$this->celebration, ????????)->persist(); ????} }

我們的工作將數據對象接受到它的構造函數中,然后在處理它時存儲它。處理作業時,它使用?CelebrationAggregateRoot?按 UUID 檢索聚合,然后調用我們之前創建的?createCelebration?方法。在它調用了這個方法之后 – 它在聚合本身上調用了?persist。這就是將為我們存儲事件的內容。但是,同樣,我們還沒有改變我們的應用程序狀態。我們所做的只是存儲一個不相關的事件而不是創建我們想要創建的慶?;顒??那么我們缺少什么?

我們的事件也需要處理。在另一種方法中,我們使用投影儀來處理我們的事件,但我們必須手動調用它們。這是一個類似的過程,但是我們的聚合正在觸發事件,我們仍然需要一個投影儀來處理事件并改變我們的應用程序狀態。

讓我們創建我們的投影儀,我稱之為處理程序 —— 因為它們處理事件。但我會讓你決定如何命名你的。

declare(strict_types=1);  namespace?DomainsCultureHandlers;  use?DomainsCultureEventsCelebrationWasCreated; use?SpatieEventSourcingEventHandlersProjectorsProjector; use?InfrastructureCultureActionsCreateNewCelebrationContract;  final?class?CelebrationHandler?extends?Projector { ????public?function?__construct( ????????public?readonly?CreateNewCelebrationContract?$action, ????)?{}  ????public?function?onCelebrationWasCreated(CelebrationWasCreated?$event):?void ????{ ????????$this->action->handle( ????????????celebration:?$event->celebration, ????????); ????} }

我們的投影機 / 處理程序,無論你選擇如何稱呼它,都將從容器中為我們解析 – 然后它將尋找一個以?on?為前綴的方法,后跟事件名稱本身。所以在我們的例子中,onCelebrationWasCreated。在我的示例中,我使用一個動作來執行事件中的實際邏輯 – 單個類執行一項可以輕松偽造或替換的工作。所以再一次,我們把樹追到下一個班級。動作,這對我來說是這樣的:

declare(strict_types=1);  namespace?DomainsCultureActions;  use?AppModelsCelebration; use?DomainsCultureDataObjectsCelebration?as?CelebrationObject; use?IlluminateDatabaseEloquentModel; use?InfrastructureCultureActionsCreateNewCelebrationContract;  final?class?CreateNewCelebration?implements?CreateNewCelebrationContract { ????public?function?handle(CelebrationObject?$celebration):?Model|Celebration ????{ ????????return?Celebration::query()->create( ????????????attributes:?$celebration->toArray(), ????????); ????} }

這是當前執行的操作。如你所見,我的操作類本身實現了一個合同 / 接口。這意味著我將接口綁定到我的服務提供者中的特定實現。這使我可以輕松地創建測試替身 / 模擬 / 替代方法,而不會對需要執行的實際操作產生連鎖反應。這不是嚴格意義上的事件溯源,而是通用編程。我們確實擁有的一個好處是我們的投影儀可以重放。因此,如果出于某種原因,我們離開了 Laravel Eloquent,也許我們使用了其他東西,我們可以創建一個新的操作 – 將實現綁定到我們的容器中,重放我們的事件,它應該都能正常工作。

在這個階段,我們正在存儲我們的事件并有辦法改變我們的應用程序的狀態 —— 但是我們做到了嗎?我們需要告訴 Event Sourcing 庫我們已經注冊了這個 Projector/Handler 以便它知道在事件上觸發它。通常我會為每個域創建一個?EventSourcingServiceProvider,這樣我就可以在一個地方注冊所有的處理程序。我的看起來如下:

declare(strict_types=1);  namespace?DomainsCultureProviders;  use?DomainsCultureHandlersCelebrationHandler; use?IlluminateSupportServiceProvider; use?SpatieEventSourcingFacadesProjectionist;  final?class?EventSourcingServiceProvider?extends?ServiceProvider { ????public?function?register():?void ????{ ????????Projectionist::addProjector( ????????????projector:?CelebrationHandler::class, ????????); ????} }

剩下的就是確保再次注冊此服務提供者。我為每個域創建一個服務提供者來注冊子服務提供者 —— 但這是另一個故事和教程。

在這個階段,我們正在存儲我們的事件,并有一種辦法改變我們的應用程序的狀態——但是我們做到了嗎?我們需要告訴 Event Sourcing 庫,我們已經注冊了 Projector/Handler 以便它知道在事件上觸發它。通常,我會為每個域創建一個EventSourcingServiceProvider,以便可以在一個位置注冊所有處理程序。如下:

declare(strict_types=1);  namespace?DomainsCultureProviders;  use?DomainsCultureHandlersCelebrationHandler; use?IlluminateSupportServiceProvider; use?SpatieEventSourcingFacadesProjectionist;  final?class?EventSourcingServiceProvider?extends?ServiceProvider { ????public?function?register():?void ????{ ????????Projectionist::addProjector( ????????????projector:?CelebrationHandler::class, ????????); ????} }

剩下確保此服務提供者重新注冊。我為每個域創建一個 Service Provider 來注冊子服務提供者–但這是另一個故事和教程。

現在,當我們把它們放在一起時。我們可以要求我們的聚合創建一個慶祝活動,它將記錄事件并將其保存在數據庫中,并且作為副作用,我們的處理程序將被觸發,隨著新的變化改變應用程序的狀態。

這似乎有點啰嗦,對吧?有沒有更好的辦法?可能,但在這一點上,我們知道何時更改了我們的應用程序狀態。我們了解它們的制作原因。此外,由于我們的數據對象,我們知道誰進行了更改以及何時進行了更改。所以它可能不是最直接的方法,但它可以讓我們更多地了解我們的應用程序。

你可以根據需要盡可能多地進行此操作,也可以將腳趾浸入事件溯源中,這是最有意義的。希望本教程為你展示了一條清晰實用的路徑,讓你從今天開始使用事件溯源。

如果看完后你覺得意猶未盡, Spatie 很大方的提供了一張 7 折優惠券,可以用在他們 Laravel 課程里的事件溯源部分,真是太棒了!訪問?laravel視頻教程?并使用優惠券碼?LARAVEL-NEWS-EVENT-SOURCING。

你曾經使用過事件溯源嗎?你是怎么處理的?在評論區告訴我們!

原文地址:https://laravel-news.com/event-sourcing-in-laravel譯文地址:https://learnku.com/laravel/t/71001

更多編程相關知識,請訪問:laravel視頻教程!!

? 版權聲明
THE END
喜歡就支持一下吧
點贊7 分享