イベント ソーシングは、ここ数年で PHP コミュニティでますます人気が高まっている用語ですが、多くの開発者にとっては謎のままです。疑問は常に、どのようにして、なぜそうなるのかということですが、それは当然のことです。このチュートリアルの目的は、イベント ソーシングとは何かを実際に理解するのに役立つだけでなく、イベント ソーシングを使用する必要がある場合についても知らせることです。
従来のアプリケーションでは、アプリケーションの状態は接続先のデータベースに直接表現されます。それがどのようにしてそこに到達したのかは完全には理解できません。私たちは彼のことをこのように知っているだけです。モデルの変更を監査するツールを使用して、何が変更されたか、誰が変更したかを確認できるようにすることで、もう少し理解する方法があります。これは正しい方向への一歩でもあります。しかし、私たちはこの重要な問題をまだ理解していません。 [関連する推奨事項: laravel ビデオチュートリアル ]
なぜですか?なぜこのモデルが変更されたのですか?この変更の目的は何ですか?
ここでイベント ソーシングが活躍し、アプリケーションの状態に何が起こったのか、なぜ変化したのかの履歴ビューを保持します。イベント ソーシングを使用すると、過去に基づいて意思決定を行うことができるため、レポートを生成できます。しかし、基本的なレベルでは、アプリケーションの状態が変化した理由をイベントを通じて知ることができます。
基本的な Laravel プロジェクトを構築して、その仕組みを説明します。このアプリケーションは、時間トレースのロジックを理解し、アプリケーションのロジックに混乱しないように、シンプルになるように構築します。私たちはチームメンバーを祝うアプリを構築しています。今は正しいです。シンプルでわかりやすい。私たちはユーザーとチームを組んでおり、チーム内で何かを公に祝えるようにしたいと考えています。
新しい Laravel プロジェクトを作成しますが、認証とチーム構造と機能を有効にしたいため、Jetstream を使用します。プロジェクトを作成したら、IDE で開きます。 (ここでの正解はもちろん PHPStorm です), これで、Laravel でのイベントソーシングに飛び込む準備が整いました。
アプリケーション用に追加のモデルを作成したいと考えていますが、これが唯一のモデルです。これは Celebration
モデルです。次の Artisan コマンドを使用して作成できます:
php artisan make:model Celebration -m
移行ファイルのアップ方法を変更すると、次のようになります:
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(); }); }
We reason
を祝う理由があります。単純な文の後に、お祝いと一緒に送信したいオプションのメッセージ message が続きます。これに加えて、お祝いをしているユーザー、お祝いを送ったユーザー、そして彼らが所属しているチームという 3 つの関係があります。 Jetstream では、ユーザーは複数のチームに所属することができ、2 人のユーザーが同じチームに所属する状況が発生する可能性があるため、正しいチームでそれらのユーザーを公に祝うようにしたいと考えています。
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', ); } }
Laravel Livewire を使用して UI を制御します。ただし、イベント ソーシングの側面に重点を置きたいため、このチュートリアルではこのパッケージについては詳しく説明しません。
私が作成するほとんどのプロジェクトと同様に、規模の大小に関係なく、アプリケーションにはモジュール型レイアウト、つまりドメイン駆動設計アプローチを採用しています。これはあくまで私がやっていることであり、非常に主観的なものであるため、自分で従う必要はありません。 次のステップはドメインを設定することです。このデモでは、Culture という 1 つのドメインだけを持っています。この文化では、必要なものすべてに対して名前空間を作成しました。ただし、プロセスを理解できるように説明します。 最初のステップは、Laravel でイベント ソーシングを使用できるようにするパッケージをインストールすることでした。これを行うために、多くのバックグラウンド作業を実行するSpatie パッケージ を使用しました。
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'); }); }
如你所见,我们为我们的活动收集了大量数据。现在模型要简单得多。它应该如下所示:
declare(strict_types=1); namespace App\Models; use Spatie\EventSourcing\StoredEvents\Models\EloquentStoredEvent; final class CelebrationStoredEvent extends EloquentStoredEvent { public $table = 'celebration_stored_events'; }
当我们扩展 EloquentStoredEvent
模型时,我们需要做的就是改变它正在查看的表。模型的其余功能已经在父级上到位。
要使用这些模型,你必须创建一个存储库来查询事件。这是一个非常简单的存储库 —— 然而,这是一个重要的步骤。我将我的添加到我的域代码中,位于 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(); } }
既然我们有了存储事件和查询它们的方法,我们可以继续我们的聚合本身。同样,我将我的存储在我的域中,但可以随意将你的存储在你的应用程序上下文中。
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, ); } }
到目前为止,除了为我们连接到正确的事件存储之外,此聚合不会执行任何操作。要让它开始跟踪事件,我们首先需要创建它们。但在此之前,我们需要停下来想一想。我们希望在活动中存储哪些数据?我们想要存储我们需要的每一个属性吗?或者我们是否希望存储一个数组,就像它来自一个表单一样?我两种方法都不用,因为为什么要保持简单呢?我在所有事件中使用数据传输对象,以确保始终维护上下文并始终提供类型安全。
我构建了一个软件包,让我做这件事更容易。可以通过以下 Composer 命令安装它:
composer require juststeveking/laravel-data-object-tools
和以前一样, 我默认将我的数据对象保存在我的领域, 但你可以添加到对你最有意义的地方。 我创建了一个名为 Celebration
的数据对象,可以传递给事件和聚合器:
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, ]; } }
当我升级到 PHP 8.2 时,这会容易得多,因为我可以创建只读类 - 是的,我的包已经支持它们。
现在我们有了我们的数据对象。我们可以回到我们想要存储的事件。我已经调用了我的CelebrationWasCreated
,因为事件名称应该总是过去时。让我们看看这个事件:
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, ) {} }
因为我们使用的是数据对象,所以我们的类保持干净。所以,现在我们有了一个事件——以及一个可以发送的数据对象,我们需要考虑如何触发它。这让我们回到了聚合本身,所以让我们在聚合上创建一个可以用于此目的的方法:
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; } }
在这一点上,我们有一种方法来要求一个类记录事件。但是,这一事件还不会持续下去 —— 那是以后的事。此外,我们不会以任何方式改变应用程序的状态。那么,我们该如何做这项活动采购工作呢?这一部分是关于 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 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(); } }
我们的工作将数据对象接受到它的构造函数中,然后在处理它时存储它。处理作业时,它使用 CelebrationAggregateRoot
按 UUID 检索聚合,然后调用我们之前创建的 createCelebration
方法。在它调用了这个方法之后 - 它在聚合本身上调用了 persist
。这就是将为我们存储事件的内容。但是,同样,我们还没有改变我们的应用程序状态。我们所做的只是存储一个不相关的事件而不是创建我们想要创建的庆祝活动?那么我们缺少什么?
我们的事件也需要处理。在另一种方法中,我们使用投影仪来处理我们的事件,但我们必须手动调用它们。这是一个类似的过程,但是我们的聚合正在触发事件,我们仍然需要一个投影仪来处理事件并改变我们的应用程序状态。
让我们创建我们的投影仪,我称之为处理程序 —— 因为它们处理事件。但我会让你决定如何命名你的。
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
。在我的示例中,我使用一个动作来执行事件中的实际逻辑 - 单个类执行一项可以轻松伪造或替换的工作。所以再一次,我们把树追到下一个班级。动作,这对我来说是这样的:
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(), ); } }
这是当前执行的操作。如你所见,我的操作类本身实现了一个合同 / 接口。这意味着我将接口绑定到我的服务提供者中的特定实现。这使我可以轻松地创建测试替身 / 模拟 / 替代方法,而不会对需要执行的实际操作产生连锁反应。这不是严格意义上的事件溯源,而是通用编程。我们确实拥有的一个好处是我们的投影仪可以重放。因此,如果出于某种原因,我们离开了 Laravel Eloquent,也许我们使用了其他东西,我们可以创建一个新的操作 - 将实现绑定到我们的容器中,重放我们的事件,它应该都能正常工作。
在这个阶段,我们正在存储我们的事件并有办法改变我们的应用程序的状态 —— 但是我们做到了吗?我们需要告诉 Event Sourcing 库我们已经注册了这个 Projector/Handler 以便它知道在事件上触发它。通常我会为每个域创建一个 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, ); } }
剩下的就是确保再次注册此服务提供者。我为每个域创建一个服务提供者来注册子服务提供者 —— 但这是另一个故事和教程。
在这个阶段,我们正在存储我们的事件,并有一种办法改变我们的应用程序的状态——但是我们做到了吗?我们需要告诉 Event Sourcing 库,我们已经注册了 Projector/Handler 以便它知道在事件上触发它。通常,我会为每个域创建一个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, ); } }
剩下确保此服务提供者重新注册。我为每个域创建一个 Service Provider 来注册子服务提供者--但这是另一个故事和教程。
现在,当我们把它们放在一起时。我们可以要求我们的聚合创建一个庆祝活动,它将记录事件并将其保存在数据库中,并且作为副作用,我们的处理程序将被触发,随着新的变化改变应用程序的状态。
这似乎有点啰嗦,对吧?有没有更好的办法?可能,但在这一点上,我们知道何时更改了我们的应用程序状态。我们了解它们的制作原因。此外,由于我们的数据对象,我们知道谁进行了更改以及何时进行了更改。所以它可能不是最直接的方法,但它可以让我们更多地了解我们的应用程序。
これは好きなだけ実行することも、最も理にかなったイベント ソーシングに足を踏み入れることもできます。このチュートリアルが、今日からイベント ソーシングの使用を開始するための明確で実践的な道筋を示してくれれば幸いです。
これを読んでもまだ物足りないという方には、Laravel コースのイベントソーシング部分で使える 30% オフクーポンを Spatie が惜しみなく提供してくれます。 コースウェブサイトにアクセスし、クーポンコード LARAVEL-NEWS-EVENT-SOURCING
を使用してください。
イベント ソーシングを使用したことがありますか?どのように対処しましたか?コメント欄で教えてください!
元のアドレス: https://laravel-news.com/event-sourcing-in-laravel
翻訳アドレス: https://learnku.com/laravel/t/ 71001
プログラミング関連の知識については、プログラミング ビデオをご覧ください。 !
以上がLaravelのイベントソーシングについて詳しく解説した記事の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。