Heim  >  Artikel  >  PHP-Framework  >  Ein Artikel, der Event Sourcing in Laravel ausführlich erklärt

Ein Artikel, der Event Sourcing in Laravel ausführlich erklärt

青灯夜游
青灯夜游nach vorne
2022-12-13 20:32:371529Durchsuche

Ein Artikel, der Event Sourcing in Laravel ausführlich erklärt

Event Sourcing ist ein Begriff, der in den letzten Jahren in der PHP-Community immer beliebter geworden ist, für viele Entwickler jedoch immer noch ein Rätsel ist. Die Fragen sind immer wie und warum, und das ist natürlich verständlich. Der Zweck dieses Tutorials besteht nicht nur darin, Ihnen zu helfen, praktisch zu verstehen, was Event Sourcing ist, sondern Ihnen auch mitzuteilen, wann Sie es möglicherweise verwenden möchten.

Bei herkömmlichen Anwendungen wird unser Anwendungsstatus direkt in der Datenbank dargestellt, mit der wir verbunden sind. Wir verstehen nicht ganz, wie es dorthin gelangt ist. Wir kennen ihn einfach so. Es gibt Möglichkeiten, wie wir ein wenig tiefer verstehen können, indem wir Tools verwenden, die Modelländerungen prüfen, damit wir sehen können, was geändert wurde und wer sie vorgenommen hat. Auch das ist ein Schritt in die richtige Richtung. Allerdings verstehen wir dieses entscheidende Problem immer noch nicht. [Verwandte Empfehlungen: Laravel-Video-Tutorial]

Warum? Warum wurde dieses Modell geändert? Was ist der Zweck dieser Änderung?

Hier kommt Event-Sourcing ins Spiel, das eine historische Ansicht darüber behält, was mit dem Anwendungsstatus passiert ist und warum er sich geändert hat. Mithilfe von Event Sourcing können Sie Entscheidungen auf der Grundlage der Vergangenheit treffen und so Berichte erstellen. Aber auf einer grundlegenden Ebene können Sie anhand von Ereignissen erkennen, warum sich der Status der Anwendung geändert hat.

Ich werde ein grundlegendes Laravel-Projekt erstellen, um Ihnen die Funktionsweise zu erklären. Wir werden diese Anwendung so gestalten, dass sie einfach ist, damit Sie die Logik der Zeitverfolgung verstehen und nicht von der Anwendungslogik verwirrt werden. Wir entwickeln eine App, die Teammitglieder feiert. Das ist richtig. Einfach und leicht verständlich. Wir haben Teams mit unseren Benutzern und möchten innerhalb des Teams etwas öffentlich feiern können.

Wir werden ein neues Laravel-Projekt erstellen, aber ich werde Jetstream verwenden, weil ich Authentifizierung sowie Teamstruktur und -funktionalität ermöglichen möchte. Sobald Sie das Projekt erstellt haben, öffnen Sie es in Ihrer IDE. (Die richtige Antwort hier ist natürlich PHPStorm). Jetzt sind wir bereit, in das Event-Sourcing in Laravel einzutauchen.

Wir möchten ein zusätzliches Modell für die Anwendung erstellen, dies ist das einzige. Dies ist ein Celebration-Modell. Sie können es mit dem folgenden Artisan-Befehl erstellen: Celebration 模型,你可以使用以下的Artisan命令创建它:

php artisan make:model Celebration -m

修改你的迁移文件 up 方法,它看起来应该像这样:

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,一个简单的句子,然后是我们可能希望与庆祝活动发送的可选消 message

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',
        );
    }
}

Ändern Sie Ihre Migrationsdatei-Up-Methode, es sollte so aussehen:

composer require spatie/laravel-event-sourcing

Wir haben einen Grund zum Feiern Grund, ein einfacher Satz, gefolgt von einer optionalen Nachricht, die wir möglicherweise mit der Feier senden möchten. Darüber hinaus haben wir drei Beziehungen: den Benutzer, der feiert, den Benutzer, der die Feier gesendet hat, und das Team, dem er angehört. Mit Jetstream kann ein Benutzer mehreren Teams angehören, und es kann Situationen geben, in denen zwei Benutzer im selben Team sind

und wir möchten sicherstellen, dass wir sie öffentlich im richtigen Team feiern.

Sobald wir dieses Setup haben, schauen wir uns das Modell selbst an:

php artisan migrate
Wir können diese Beziehungen anderen verwandten Modellen zuordnen. Allerdings füge ich standardmäßig die andere Seite der Beziehungen zu jedem Modell hinzu, um ihre Beziehungen klarer zu machen, unabhängig davon, ob sie unbedingt erforderlich sind oder nicht. Dies ist eine Gewohnheit, die ich entwickelt habe, um anderen zu helfen, das Modell selbst zu verstehen.

Jetzt haben wir die Grundlage unserer Bewerbung modellhaft erstellt. Ich denke, wir müssen einige Pakete (Abhängigkeiten) installieren, die uns helfen. Für meine App verwende ich

Laravel Livewire

zur Steuerung der Benutzeroberfläche. Ich werde in diesem Tutorial jedoch nicht näher auf dieses Paket eingehen, da ich sicherstellen möchte, dass ich mich auf den Event-Sourcing-Aspekt konzentrieren kann.

Wie bei den meisten Projekten, die ich erstelle, egal wie groß oder klein, verwende ich für die Anwendung ein modulares Layout – einen Domain Driven Design-Ansatz. Das ist nur etwas, was ich tue. Ich habe nicht das Gefühl, dass Sie dem selbst folgen müssen, da es sehr subjektiv ist.

Mein nächster Schritt besteht darin, meine Domains einzurichten. Für diese Demo habe ich nur eine Domain: Kultur. In der Kultur habe ich Namensräume für alles erstellt, was ich brauchen könnte. Aber ich werde es durchgehen, damit Sie den Prozess verstehen

Der erste Schritt bestand darin, ein Paket zu installieren, das es mir ermöglichte, Event Sourcing in Laravel zu verwenden. Dazu habe ich ein „Spatie-Paket“ verwendet, das einen Großteil der Hintergrundarbeit für mich erledigt hat. Führen Sie nach der ordnungsgemäßen Installation Migrationen durch, um die Datenbank in den richtigen Zustand zu versetzen.

php artisan make:model CelebrationStoredEvent -m

Jetzt können wir darüber nachdenken, wie wir Event Sourcing umsetzen wollen. Sie können dies auf zwei Arten erreichen: einen Projektor zur Projektion Ihres Zustands oder ein Aggregat.

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 来注册子服务提供者--但这是另一个故事和教程。

现在,当我们把它们放在一起时。我们可以要求我们的聚合创建一个庆祝活动,它将记录事件并将其保存在数据库中,并且作为副作用,我们的处理程序将被触发,随着新的变化改变应用程序的状态。

这似乎有点啰嗦,对吧?有没有更好的办法?可能,但在这一点上,我们知道何时更改了我们的应用程序状态。我们了解它们的制作原因。此外,由于我们的数据对象,我们知道谁进行了更改以及何时进行了更改。所以它可能不是最直接的方法,但它可以让我们更多地了解我们的应用程序。

Sie können dies so oft tun, wie Sie möchten, oder sich mit Event-Sourcing befassen, je nachdem, was am sinnvollsten ist. Hoffentlich hat Ihnen dieses Tutorial einen klaren und praktischen Weg aufgezeigt, wie Sie noch heute mit der Nutzung von Event Sourcing beginnen können.

Wenn Sie nach der Lektüre das Gefühl haben, dass Sie immer noch nicht zufrieden sind, bietet Spatie großzügig einen 30 %-Rabattgutschein an, der für den Event-Sourcing-Teil seines Laravel-Kurses verwendet werden kann, was großartig ist! Besuchen Sie die Kurs-Website und verwenden Sie den Gutscheincode LARAVEL-NEWS-EVENT-SOURCING.

Haben Sie schon einmal Event Sourcing genutzt? Wie sind Sie damit umgegangen? Sagen Sie es uns im Kommentarbereich!

Originaladresse: https://laravel-news.com/event-sourcing-in-laravel

Übersetzungsadresse: https://learnku.com/laravel/t/71001

Mehr programmbezogene Kenntnisse, Bitte besuchen Sie: Programmiervideos! !

Das obige ist der detaillierte Inhalt vonEin Artikel, der Event Sourcing in Laravel ausführlich erklärt. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Dieser Artikel ist reproduziert unter:learnku.com. Bei Verstößen wenden Sie sich bitte an admin@php.cn löschen