Sumber acara ialah istilah yang semakin popular dalam komuniti PHP sejak beberapa tahun lalu, tetapi masih menjadi misteri kepada ramai pembangun. Soalannya sentiasa bagaimana dan mengapa, dan sudah tentu itu boleh difahami. Tujuan tutorial ini bukan sahaja untuk membantu anda memahami sumber acara secara praktikal, tetapi juga untuk memberitahu anda bila anda mungkin mahu menggunakannya.
Dalam aplikasi tradisional, keadaan aplikasi kami diwakili terus dalam pangkalan data yang kami sambungkan. Kami tidak faham sepenuhnya bagaimana ia sampai ke sana. Kita baru kenal dia macam ni. Terdapat cara kita boleh memahami sedikit lagi, menggunakan alatan yang mengaudit perubahan model supaya kita dapat melihat perkara yang diubah dan siapa yang membuatnya. Ini juga merupakan satu langkah ke arah yang betul. Walau bagaimanapun, kami masih tidak memahami isu penting ini. [Cadangan berkaitan: tutorial video laravel]
Kenapa? Mengapa model ini berubah? Apakah tujuan perubahan ini?
Di sinilah sumber acara berperanan, mengekalkan pandangan sejarah tentang perkara yang berlaku pada keadaan aplikasi dan sebab ia berubah. Penyumberan acara membolehkan anda membuat keputusan berdasarkan masa lalu supaya anda boleh menjana laporan. Tetapi pada peringkat asas, ia membolehkan anda mengetahui mengapa keadaan aplikasi telah berubah Ini dilakukan melalui acara.
Saya akan membina projek Laravel asas untuk memandu anda melalui cara ia berfungsi. Kami akan membina aplikasi ini dengan mudah supaya anda boleh memahami logik pengesanan masa dan tidak keliru dengan logik aplikasi. Kami sedang membina aplikasi yang meraikan ahli pasukan. betul tu. Mudah dan mudah difahami. Kami mempunyai pasukan dengan pengguna kami dan kami mahu dapat meraikan sesuatu secara terbuka dalam pasukan.
Kami akan mencipta projek Laravel baharu, tetapi saya akan menggunakan Jetstream kerana saya mahu mendayakan pengesahan serta struktur dan fungsi pasukan. Sebaik sahaja anda telah mencipta projek itu, bukanya dalam IDE anda. (Jawapan yang betul di sini sudah tentu PHPStorm ), Kini kami bersedia untuk menyelami sumber acara di Laravel.
Kami ingin mencipta model tambahan untuk aplikasi, ini adalah satu-satunya. Ini ialah model Celebration
, anda boleh menciptanya menggunakan arahan Artisan berikut:
php artisan make:model Celebration -m
Ubah suai kaedah pemindahan fail anda, ia sepatutnya kelihatan seperti ini:
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(); }); }
Kami mempunyai sebab untuk meraikan reason
, ayat mudah, dan kemudian mesej pilihan message
yang mungkin ingin kami hantar bersama perayaan itu. Di samping itu, kami mempunyai tiga perhubungan, pengguna yang meraikan, pengguna yang menghantar perayaan dan pasukan yang mereka sertai. Dengan Jetstream, pengguna boleh tergolong dalam berbilang pasukan dan mungkin terdapat situasi di mana dua pengguna berada dalam pasukan yang sama
dan kami ingin memastikan kami meraikan mereka secara terbuka pada pasukan yang betul.
Setelah kami mempunyai persediaan ini, mari lihat model itu sendiri:
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', ); } }
Kami boleh memetakan perhubungan ini kepada model lain yang berkaitan. Walaupun, secara lalai saya akan menambah bahagian lain perhubungan kepada setiap model untuk menjadikan perhubungan mereka lebih jelas, sama ada ia memerlukannya atau tidak. Ini adalah tabiat yang saya kembangkan untuk membantu orang lain memahami model itu sendiri.
Kini kami mempunyai asas aplikasi kami yang dibuat daripada perspektif model. Saya fikir kita perlu memasang beberapa pakej (pergantungan) yang akan membantu kita. Untuk apl saya, saya menggunakan Laravel Livewire untuk mengawal UI. Tetapi saya tidak akan menerangkan secara terperinci tentang pakej ini dalam tutorial ini kerana saya ingin memastikan saya boleh fokus pada aspek penyumberan acara.
Seperti kebanyakan projek yang saya buat, tidak kira besar atau kecil, saya menggunakan reka letak modular untuk aplikasi - pendekatan Reka Bentuk Dipacu Domain. Ini hanya sesuatu yang saya lakukan, jangan rasa anda perlu mengikuti ini sendiri kerana ia sangat subjektif.
Langkah saya seterusnya ialah menyediakan domain saya, untuk demo ini saya hanya mempunyai satu domain: Budaya. Dalam budaya saya mencipta ruang nama untuk semua yang saya perlukan. Tetapi saya akan meneruskannya supaya anda memahami prosesnya
Langkah pertama ialah memasang pakej yang membolehkan saya menggunakan sumber acara dalam Laravel. Untuk melakukan ini, saya menggunakan pakej Spatie yang melakukan banyak kerja latar belakang untuk saya
composer require spatie/laravel-event-sourcing
Selepas memasang, pastikan anda mengikuti arahan pemasangan untuk pakej sebagai konfigurasi dan penghijrahan perlu dilepaskan. Selepas pemasangan yang betul, jalankan migrasi untuk mendapatkan pangkalan data dalam keadaan yang betul.
php artisan migrate
Sekarang kita boleh mula berfikir tentang cara kita mahu melaksanakan penyumberan acara. Anda boleh mencapai ini dalam dua cara: projektor untuk menayangkan keadaan anda atau agregat.
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 来注册子服务提供者--但这是另一个故事和教程。
现在,当我们把它们放在一起时。我们可以要求我们的聚合创建一个庆祝活动,它将记录事件并将其保存在数据库中,并且作为副作用,我们的处理程序将被触发,随着新的变化改变应用程序的状态。
这似乎有点啰嗦,对吧?有没有更好的办法?可能,但在这一点上,我们知道何时更改了我们的应用程序状态。我们了解它们的制作原因。此外,由于我们的数据对象,我们知道谁进行了更改以及何时进行了更改。所以它可能不是最直接的方法,但它可以让我们更多地了解我们的应用程序。
Anda boleh melakukan ini seberapa banyak yang anda mahu atau mencelupkan kaki anda ke dalam sumber acara mengikut mana yang paling masuk akal. Semoga tutorial ini telah menunjukkan kepada anda jalan yang jelas dan praktikal untuk mula menggunakan sumber acara hari ini.
Jika anda masih berasa tidak berpuas hati selepas membaca ini, Spatie dengan murah hati memberikan kupon diskaun 30% yang boleh digunakan untuk bahagian sumber acara kursus Laravel mereka, yang sangat bagus! Lawati tapak web kursus dan gunakan kod kupon LARAVEL-NEWS-EVENT-SOURCING
.
Pernahkah anda menggunakan sumber acara? Bagaimana anda mengendalikannya? Beritahu kami di bahagian komen!
Alamat asal: https://laravel-news.com/event-sourcing-in-laravel
Alamat terjemahan: https://learnku.com/laravel/t/ 71001
Untuk lebih banyak pengetahuan berkaitan pengaturcaraan, sila lawati: Video Pengaturcaraan! !
Atas ialah kandungan terperinci Artikel yang menerangkan sumber acara dalam Laravel secara terperinci. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!