您好,登錄后才能下訂單哦!
這篇文章主要介紹了Laravel中的事件溯源實例代碼分析的相關(guān)知識,內(nèi)容詳細(xì)易懂,操作簡單快捷,具有一定借鑒價值,相信大家閱讀完這篇Laravel中的事件溯源實例代碼分析文章都會有所收獲,下面我們一起來看看吧。
我們將新建一個 Laravel 項目,但我將使用 Jetstream,因為我想啟用身份驗證和團隊結(jié)構(gòu)和功能。 一旦你創(chuàng)建了這個項目, 請在你的IDE中打開它。(這里的正確答案當(dāng)然是 PHPStorm ), 現(xiàn)在我們已經(jīng)準(zhǔn)備好在 Laravel 深入事件溯源。
我們希望為應(yīng)用程序創(chuàng)建一個附加模型,這是唯一的一個。這是一個 Celebration
模型,你可以使用以下的Artisan命令創(chuàng)建它:
php artisan make:model Celebration -m
修改你的遷移文件 up 方法,它看起來應(yīng)該像這樣:
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(fā)送的可選消 message
。除此之外,我們有三個關(guān)系,正在慶祝的用戶,發(fā)送慶祝的用戶,以及他們所在的團隊。使用 Jetstream,一個用戶可以屬于多個團隊,并且可能存在兩個用戶在同一個團隊中的情況
,我們要確保我們在正確的團隊中公開慶祝他們。
一旦我們有了這個設(shè)定,讓我們看看模型本身:
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
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',
);
}
}
我們可以將這些關(guān)系映射到其他相關(guān)的模型上。盡管,默認(rèn)情況下,我會將關(guān)聯(lián)關(guān)系的另一端添加到每一個模型中,使它們的關(guān)聯(lián)關(guān)系更加清楚,無論它是否嚴(yán)格需要這些關(guān)聯(lián)關(guān)系。這是我養(yǎng)成的習(xí)慣,為了幫助他人理解模型本身。
現(xiàn)在我們有了從模型視角創(chuàng)建的我們的應(yīng)用基礎(chǔ)。我想我們需要一些安裝一些對我們有幫助的軟件包(依賴)。對于我的應(yīng)用,我使用 Laravel Livewire 來控制 UI 。但是我并不會在本教程詳細(xì)介紹這個包,因為我想確保我能專注于講事件溯源這個方面。
與我創(chuàng)建的大多數(shù)項目一樣,無論大小,我都為應(yīng)用程序采用了模塊化布局——一個領(lǐng)域驅(qū)動模型設(shè)計 ( Domain Driven Design ) 方法。這只是我做的事情,不要覺得你自己必須遵循這個,因為它是非常主觀的。
我的下一步是設(shè)置我的域,對于這個演示,我只有一個域:文化。在文化中,我為我可能需要的一切創(chuàng)建了名稱空間。但我會經(jīng)歷它,這樣你就明白了過程
第一步是安裝一個軟件包,使我能夠在Laravel中使用事件源。為此,我使用了一個 Spatie package包,它為我做了大量的后臺工作
composer require spatie/laravel-event-sourcing
安裝后,請確保按照包的安裝說明進行操作,因為配置和遷移需要發(fā)布。正確安裝后,運行遷移,使數(shù)據(jù)庫處于正確狀態(tài)。
php artisan migrate
現(xiàn)在我們可以開始思考我們要如何實現(xiàn)事件溯源。你可以通過兩種方式實現(xiàn)這一點:投影儀來投影你的狀態(tài)或聚合。
Projector 是一個位于你的應(yīng)用程序中并處理你調(diào)度的事件的類。然后,這些將更改你的應(yīng)用程序的狀態(tài)。這不僅僅是簡單地更新你的數(shù)據(jù)庫。它位于中間,捕獲一個事件,存儲它,然后進行所需的更改 —— 然后 “投射” 應(yīng)用程序的新狀態(tài)
另一種方法,我的首選方法,聚合 - 這些是像投影儀一樣為你處理應(yīng)用程序狀態(tài)的類。我們不是在我們的應(yīng)用程序中自己觸發(fā)事件,而是將其留給聚合為我們做。把它想象成一個中繼,你要求中繼做某事,它會為你處理。
在我們創(chuàng)建第一個聚合之前,需要在后臺做一些工作。我非常喜歡為每個聚合創(chuàng)建一個事件存儲,以便查詢更快,并且該存儲不會很快填滿。這在包文檔中進行了解釋,但我將親自引導(dǎo)你完成它,因為它在文檔中并不是最清楚的。
第一步是創(chuàng)建模型和遷移,因為你將來需要一種方法來查詢它以進行報告等。運行以下 artisan 命令來創(chuàng)建這些:
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');
});
}
如你所見,我們?yōu)槲覀兊幕顒邮占舜罅繑?shù)據(jù)。現(xiàn)在模型要簡單得多。它應(yīng)該如下所示:
declare(strict_types=1);
namespace App\Models;
use Spatie\EventSourcing\StoredEvents\Models\EloquentStoredEvent;
final class CelebrationStoredEvent extends EloquentStoredEvent
{
public $table = 'celebration_stored_events';
}
當(dāng)我們擴展 EloquentStoredEvent
模型時,我們需要做的就是改變它正在查看的表。模型的其余功能已經(jīng)在父級上到位。
要使用這些模型,你必須創(chuàng)建一個存儲庫來查詢事件。這是一個非常簡單的存儲庫 —— 然而,這是一個重要的步驟。我將我的添加到我的域代碼中,位于 src/Domains/Culture/Repositories/
下,但您可以隨意添加對您最有意義的位置:
declare(strict_types=1);
namespace Domains\Culture\Repositories;
use App\Models\CelebrationStoredEvent;
use Spatie\EventSourcing\StoredEvents\Repositories\EloquentStoredEventRepository;
final class CelebrationStoredEventsRepository extends EloquentStoredEventRepository
{
public function __construct(
protected string $storedEventModel = CelebrationStoredEvent::class,
) {
parent::__construct();
}
}
既然我們有了存儲事件和查詢它們的方法,我們可以繼續(xù)我們的聚合本身。同樣,我將我的存儲在我的域中,但可以隨意將你的存儲在你的應(yīng)用程序上下文中。
declare(strict_types=1);
namespace Domains\Culture\Aggregates;
use Domains\Culture\Repositories\CelebrationStoredEventsRepository;
use Spatie\EventSourcing\AggregateRoots\AggregateRoot;
use Spatie\EventSourcing\StoredEvents\Repositories\StoredEventRepository;
final class CelebrationAggregateRoot extends AggregateRoot
{
protected function getStoredEventRepository(): StoredEventRepository
{
return app()->make(
abstract: CelebrationStoredEventsRepository::class,
);
}
}
到目前為止,除了為我們連接到正確的事件存儲之外,此聚合不會執(zhí)行任何操作。要讓它開始跟蹤事件,我們首先需要創(chuàng)建它們。但在此之前,我們需要停下來想一想。我們希望在活動中存儲哪些數(shù)據(jù)?我們想要存儲我們需要的每一個屬性嗎?或者我們是否希望存儲一個數(shù)組,就像它來自一個表單一樣?我兩種方法都不用,因為為什么要保持簡單呢?我在所有事件中使用數(shù)據(jù)傳輸對象,以確保始終維護上下文并始終提供類型安全。
我構(gòu)建了一個軟件包,讓我做這件事更容易??梢酝ㄟ^以下 Composer 命令安裝它:
composer require juststeveking/laravel-data-object-tools
和以前一樣, 我默認(rèn)將我的數(shù)據(jù)對象保存在我的領(lǐng)域, 但你可以添加到對你最有意義的地方。 我創(chuàng)建了一個名為 Celebration
的數(shù)據(jù)對象,可以傳遞給事件和聚合器:
declare(strict_types=1);
namespace Domains\Culture\DataObjects;
use JustSteveKing\DataObjects\Contracts\DataObjectContract;
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,
];
}
}
當(dāng)我升級到 PHP 8.2 時,這會容易得多,因為我可以創(chuàng)建只讀類 - 是的,我的包已經(jīng)支持它們。
現(xiàn)在我們有了我們的數(shù)據(jù)對象。我們可以回到我們想要存儲的事件。我已經(jīng)調(diào)用了我的CelebrationWasCreated
,因為事件名稱應(yīng)該總是過去時。讓我們看看這個事件:
declare(strict_types=1);
namespace Domains\Culture\Events;
use Domains\Culture\DataObjects\Celebration;
use Spatie\EventSourcing\StoredEvents\ShouldBeStored;
final class CelebrationWasCreated extends ShouldBeStored
{
public function __construct(
public readonly Celebration $celebration,
) {}
}
因為我們使用的是數(shù)據(jù)對象,所以我們的類保持干凈。所以,現(xiàn)在我們有了一個事件——以及一個可以發(fā)送的數(shù)據(jù)對象,我們需要考慮如何觸發(fā)它。這讓我們回到了聚合本身,所以讓我們在聚合上創(chuàng)建一個可以用于此目的的方法:
declare(strict_types=1);
namespace Domains\Culture\Aggregates;
use Domains\Culture\DataObjects\Celebration;
use Domains\Culture\Events\CelebrationWasCreated;
use Domains\Culture\Repositories\CelebrationStoredEventsRepository;
use Spatie\EventSourcing\AggregateRoots\AggregateRoot;
use Spatie\EventSourcing\StoredEvents\Repositories\StoredEventRepository;
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;
}
}
在這一點上,我們有一種方法來要求一個類記錄事件。但是,這一事件還不會持續(xù)下去 —— 那是以后的事。此外,我們不會以任何方式改變應(yīng)用程序的狀態(tài)。那么,我們該如何做這項活動采購工作呢?這一部分是關(guān)于 Livewire 中的實現(xiàn)的,我現(xiàn)在將向你介紹它。
我喜歡通過調(diào)度一個事件來管理這個過程,因為它更高效。如果你考慮如何與應(yīng)用程序交互,你可以從 Web 訪問它,通過 API 端點發(fā)送請求,或者發(fā)生 CLI 命令可能運行的事件 —— 可能是一個 Cron 作業(yè)。在所有這些方法中,通常,你需要即時響應(yīng),或者至少您不想等待。我將在我的 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();
}
在這一點上,我們有一種方法來要求一個類記錄事件。但是,這一事件還不會持續(xù)下去 —— 那是以后的事。此外,我們不會以任何方式改變應(yīng)用程序的狀態(tài)。那么,我們該如何做這項活動采購工作呢?這一部分是關(guān)于 Livewire 中的實現(xiàn)的,我現(xiàn)在將向你介紹它。
我喜歡通過調(diào)度一個事件來管理這個過程,因為它更高效。如果你考慮如何與應(yīng)用程序交互,你可以從 Web 訪問它,通過 API 端點發(fā)送請求,或者發(fā)生 CLI 命令可能運行的事件 —— 可能是一個 Cron 作業(yè)。在所有這些方法中,通常,你需要即時響應(yīng),或者至少你不想等待。我將在我的 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();
}
當(dāng)我驗證來自組件的用戶輸入,可以分派處理一個新的作業(yè),然后結(jié)束這個流程。我使用我的包將一個新的數(shù)據(jù)對象傳遞給作業(yè)。它有一個 Facade,可以讓我用一系列屬性來為類添加——到目前為止它工作得很好。那么這是怎么實現(xiàn)的呢?讓我們來看看。
declare(strict_types=1);
namespace App\Jobs\Team;
use Domains\Culture\Aggregates\CelebrationAggregateRoot;
use Domains\Culture\DataObjects\Celebration;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Str;
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();
}
}
我們的工作將數(shù)據(jù)對象接受到它的構(gòu)造函數(shù)中,然后在處理它時存儲它。處理作業(yè)時,它使用 CelebrationAggregateRoot
按 UUID 檢索聚合,然后調(diào)用我們之前創(chuàng)建的 createCelebration
方法。在它調(diào)用了這個方法之后 - 它在聚合本身上調(diào)用了 persist
。這就是將為我們存儲事件的內(nèi)容。但是,同樣,我們還沒有改變我們的應(yīng)用程序狀態(tài)。我們所做的只是存儲一個不相關(guān)的事件而不是創(chuàng)建我們想要創(chuàng)建的慶祝活動?那么我們?nèi)鄙偈裁矗?/p>
我們的事件也需要處理。在另一種方法中,我們使用投影儀來處理我們的事件,但我們必須手動調(diào)用它們。這是一個類似的過程,但是我們的聚合正在觸發(fā)事件,我們?nèi)匀恍枰粋€投影儀來處理事件并改變我們的應(yīng)用程序狀態(tài)。
讓我們創(chuàng)建我們的投影儀,我稱之為處理程序 —— 因為它們處理事件。但我會讓你決定如何命名你的。
declare(strict_types=1);
namespace Domains\Culture\Handlers;
use Domains\Culture\Events\CelebrationWasCreated;
use Spatie\EventSourcing\EventHandlers\Projectors\Projector;
use Infrastructure\Culture\Actions\CreateNewCelebrationContract;
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
。在我的示例中,我使用一個動作來執(zhí)行事件中的實際邏輯 - 單個類執(zhí)行一項可以輕松偽造或替換的工作。所以再一次,我們把樹追到下一個班級。動作,這對我來說是這樣的:
declare(strict_types=1);
namespace Domains\Culture\Actions;
use App\Models\Celebration;
use Domains\Culture\DataObjects\Celebration as CelebrationObject;
use Illuminate\Database\Eloquent\Model;
use Infrastructure\Culture\Actions\CreateNewCelebrationContract;
final class CreateNewCelebration implements CreateNewCelebrationContract
{
public function handle(CelebrationObject $celebration): Model|Celebration
{
return Celebration::query()->create(
attributes: $celebration->toArray(),
);
}
}
這是當(dāng)前執(zhí)行的操作。如你所見,我的操作類本身實現(xiàn)了一個合同 / 接口。這意味著我將接口綁定到我的服務(wù)提供者中的特定實現(xiàn)。這使我可以輕松地創(chuàng)建測試替身 / 模擬 / 替代方法,而不會對需要執(zhí)行的實際操作產(chǎn)生連鎖反應(yīng)。這不是嚴(yán)格意義上的事件溯源,而是通用編程。我們確實擁有的一個好處是我們的投影儀可以重放。因此,如果出于某種原因,我們離開了 Laravel Eloquent,也許我們使用了其他東西,我們可以創(chuàng)建一個新的操作 - 將實現(xiàn)綁定到我們的容器中,重放我們的事件,它應(yīng)該都能正常工作。
在這個階段,我們正在存儲我們的事件并有辦法改變我們的應(yīng)用程序的狀態(tài) —— 但是我們做到了嗎?我們需要告訴 Event Sourcing 庫我們已經(jīng)注冊了這個 Projector/Handler 以便它知道在事件上觸發(fā)它。通常我會為每個域創(chuàng)建一個 EventSourcingServiceProvider
,這樣我就可以在一個地方注冊所有的處理程序。我的看起來如下:
declare(strict_types=1);
namespace Domains\Culture\Providers;
use Domains\Culture\Handlers\CelebrationHandler;
use Illuminate\Support\ServiceProvider;
use Spatie\EventSourcing\Facades\Projectionist;
final class EventSourcingServiceProvider extends ServiceProvider
{
public function register(): void
{
Projectionist::addProjector(
projector: CelebrationHandler::class,
);
}
}
剩下的就是確保再次注冊此服務(wù)提供者。我為每個域創(chuàng)建一個服務(wù)提供者來注冊子服務(wù)提供者 —— 但這是另一個故事和教程。
在這個階段,我們正在存儲我們的事件,并有一種辦法改變我們的應(yīng)用程序的狀態(tài)——但是我們做到了嗎?我們需要告訴 Event Sourcing 庫,我們已經(jīng)注冊了 Projector/Handler 以便它知道在事件上觸發(fā)它。通常,我會為每個域創(chuàng)建一個EventSourcingServiceProvider
,以便可以在一個位置注冊所有處理程序。如下:
declare(strict_types=1);
namespace Domains\Culture\Providers;
use Domains\Culture\Handlers\CelebrationHandler;
use Illuminate\Support\ServiceProvider;
use Spatie\EventSourcing\Facades\Projectionist;
final class EventSourcingServiceProvider extends ServiceProvider
{
public function register(): void
{
Projectionist::addProjector(
projector: CelebrationHandler::class,
);
}
}
剩下確保此服務(wù)提供者重新注冊。我為每個域創(chuàng)建一個 Service Provider 來注冊子服務(wù)提供者--但這是另一個故事和教程。
現(xiàn)在,當(dāng)我們把它們放在一起時。我們可以要求我們的聚合創(chuàng)建一個慶?;顒?,它將記錄事件并將其保存在數(shù)據(jù)庫中,并且作為副作用,我們的處理程序?qū)⒈挥|發(fā),隨著新的變化改變應(yīng)用程序的狀態(tài)。
關(guān)于“Laravel中的事件溯源實例代碼分析”這篇文章的內(nèi)容就介紹到這里,感謝各位的閱讀!相信大家對“Laravel中的事件溯源實例代碼分析”知識都有一定的了解,大家如果還想學(xué)習(xí)更多知識,歡迎關(guān)注億速云行業(yè)資訊頻道。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。