Home > Article > PHP Framework > An article explaining event sourcing in Laravel in detail
Event sourcing is a term that has become increasingly popular in the PHP community over the past few years, but remains a mystery to many developers. The questions are always how and why, and of course that's understandable. The purpose of this tutorial is to not only help you understand what event sourcing is in a practical way, but also to let you know when you might want to use it.
In traditional applications, our application state is represented directly in the database to which we are connected. We don't fully understand how it got there. We just know him like this. There are ways we can understand a little bit further, using tools that audit model changes so we can see what was changed and who made it. This is also a step in the right direction. However, we still don't understand this crucial issue. [Related recommendations: laravel video tutorial]
Why? Why did this model change? What is the purpose of this change?
This is where event sourcing comes into play, retaining a historical view of what happened to the application state and why it changed. Event sourcing allows you to make decisions based on the past so you can generate reports. But at a basic level, it allows you to know why the state of the application has changed. This is done through events.
I will build a basic Laravel project to walk you through how it works. We will build this application to be simple so that you can understand the logic of time tracing and not be confused by the application logic. We are building an app that celebrates team members. now it's right. Simple and easy to understand. We have teams with our users, and we want to be able to celebrate something publicly within the team.
We will create a new Laravel project, but I will use Jetstream because I want to enable authentication and team structure and functionality. Once you have created the project, open it in your IDE. (The correct answer here is of course PHPStorm ), Now we are ready to dive into event sourcing in Laravel.
We wish to create an additional model for the application, this is the only one. This is a Celebration
model, you can create it using the following Artisan command:
php artisan make:model Celebration -m
Modify your migration file up method, it should look like this:
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 have a reason to celebrate reason
, a simple sentence followed by an optional message message
that we may wish to send with the celebration. In addition to this, we have three relationships, the user who is celebrating, the user who sent the celebration, and the team they are on. With Jetstream, a user can belong to multiple teams, and there may be situations where two users are on the same team, and we want to make sure we celebrate them publicly on the correct team.
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', ); } }We can map these relationships to other related models. Although, by default I will add the other side of the relationships to every model to make their relationships clearer, whether or not it strictly requires them. This is a habit I developed in order to help others understand the model itself. Now we have the basis of our application created from a model perspective. I think we need to install some packages (dependencies) that will help us. For my application, I use
Laravel Livewire to control the UI. But I won't go into detail about this package in this tutorial because I want to make sure I can focus on the event sourcing aspect.
As with most projects I create, no matter how big or small, I adopt a modular layout for the application - a Domain Driven Design approach. This is just something I do, don't feel like you have to follow this yourself as it is very subjective. My next step is to set up my domains, for this demo I only have one domain: Culture. In the culture I created namespaces for everything I might need. But I'll go through it so you understand the process The first step was to install a package that enabled me to use event sourcing in Laravel. To do this, I used aSpatie package which did a lot of the background work for me
composer require spatie/laravel-event-sourcingAfter installation, make sure to follow the package's installation instructions as configuration and migration Need to publish. After proper installation, run migrations to get the database in the correct state.
php artisan migrateNow we can start thinking about how we want to implement event sourcing. You can achieve this in two ways: a projector to project your state or an aggregate.
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 来注册子服务提供者--但这是另一个故事和教程。
现在,当我们把它们放在一起时。我们可以要求我们的聚合创建一个庆祝活动,它将记录事件并将其保存在数据库中,并且作为副作用,我们的处理程序将被触发,随着新的变化改变应用程序的状态。
这似乎有点啰嗦,对吧?有没有更好的办法?可能,但在这一点上,我们知道何时更改了我们的应用程序状态。我们了解它们的制作原因。此外,由于我们的数据对象,我们知道谁进行了更改以及何时进行了更改。所以它可能不是最直接的方法,但它可以让我们更多地了解我们的应用程序。
You can do this as much as you want or dip your toe into event sourcing whichever makes the most sense. Hopefully this tutorial has shown you a clear and practical path to start using event sourcing today.
If you still feel unsatisfied after reading this, Spatie generously provides a 30% off coupon that can be used for the event sourcing part of their Laravel course. It’s really great! Visit Course Website and use coupon code LARAVEL-NEWS-EVENT-SOURCING
.
Have you ever used event sourcing? How did you deal with it? Tell us in the comments section!
Original address: https://laravel-news.com/event-sourcing-in-laravel
Translation address: https://learnku.com/laravel/t/ 71001
For more programming-related knowledge, please visit: Programming Video! !
The above is the detailed content of An article explaining event sourcing in Laravel in detail. For more information, please follow other related articles on the PHP Chinese website!