溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Laravel中的事件溯源實例代碼分析

發(fā)布時間:2022-12-14 09:09:17 來源:億速云 閱讀:137 作者:iii 欄目:編程語言

這篇文章主要介紹了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è)資訊頻道。

向AI問一下細(xì)節(jié)

免責(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)容。

AI