Maison  >  Article  >  cadre php  >  Une explication détaillée du sourcing d'événements dans Laravel

Une explication détaillée du sourcing d'événements dans Laravel

青灯夜游
青灯夜游avant
2022-12-13 20:32:371523parcourir

Une explication détaillée du sourcing d'événements dans Laravel

Le sourcing d'événements est un terme devenu de plus en plus populaire dans la communauté PHP ces dernières années, mais qui reste encore un mystère pour de nombreux développeurs. Les questions sont toujours de savoir comment et pourquoi, et bien sûr, c'est compréhensible. Le but de ce didacticiel est non seulement de vous aider à comprendre de manière pratique ce qu'est le sourcing d'événements, mais également de vous indiquer quand vous souhaiterez peut-être l'utiliser.

Dans les applications traditionnelles, notre état d'application est représenté directement dans la base de données à laquelle nous sommes connectés. Nous ne comprenons pas vraiment comment cela est arrivé là. Nous le connaissons juste comme ça. Il existe des moyens de comprendre un peu plus loin, en utilisant des outils qui auditent les modifications du modèle afin que nous puissions voir ce qui a été modifié et qui l'a fait. C’est aussi un pas dans la bonne direction. Cependant, nous ne comprenons toujours pas cette question cruciale. [Recommandations associées : tutoriel vidéo laravel]

Pourquoi ? Pourquoi ce modèle a-t-il changé ? Quel est le but de ce changement ?

C'est là que le sourcing d'événements entre en jeu, en conservant une vue historique de ce qui est arrivé à l'état de l'application et pourquoi il a changé. Le sourcing d'événements vous permet de prendre des décisions basées sur le passé afin de pouvoir générer des rapports. Mais à la base, cela permet de savoir pourquoi l’état de l’application a changé. Cela se fait via des événements.

Je vais construire un projet Laravel de base pour vous expliquer son fonctionnement. Nous allons construire cette application pour qu'elle soit simple afin que vous puissiez comprendre la logique du suivi du temps et ne pas être dérouté par la logique de l'application. Nous construisons une application qui célèbre les membres de l'équipe. C'est exact. Simple et facile à comprendre. Nous avons des équipes avec nos utilisateurs et nous voulons pouvoir célébrer quelque chose publiquement au sein de l'équipe.

Nous allons créer un nouveau projet Laravel, mais j'utiliserai Jetstream car je souhaite activer l'authentification ainsi que la structure et les fonctionnalités de l'équipe. Une fois que vous avez créé le projet, ouvrez-le dans votre IDE. (La bonne réponse ici est bien sûr PHPStorm). Nous sommes maintenant prêts à nous plonger dans le sourcing d'événements dans Laravel.

Nous souhaitons créer un modèle supplémentaire pour l'application, c'est le seul. Il s'agit d'un modèle Célébration, vous pouvez le créer à l'aide de la commande Artisan suivante : 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',
        );
    }
}

Modifiez la méthode de création de votre fichier de migration, il devrait ressembler à ceci :

composer require spatie/laravel-event-sourcing

Nous avons une raison de célébrer raison, une phrase simple, suivie d'un message facultatif que nous pourrions vouloir envoyer avec la célébration. En plus de cela, nous avons trois relations : l'utilisateur qui célèbre, l'utilisateur qui a envoyé la célébration et l'équipe dans laquelle il fait partie. Avec Jetstream, un utilisateur peut appartenir à plusieurs équipes, et il peut y avoir des situations où deux utilisateurs sont dans la même équipe

et nous voulons nous assurer de les célébrer publiquement dans la bonne équipe.

Une fois cette configuration obtenue, examinons le modèle lui-même :

php artisan migrate
Nous pouvons mapper ces relations à d'autres modèles associés. Cependant, par défaut, j'ajouterai l'autre côté des relations à chaque modèle pour rendre leurs relations plus claires, que cela les exige strictement ou non. C'est une habitude que j'ai développée afin d'aider les autres à comprendre le modèle lui-même.

Nous avons maintenant la base de notre application créée dans une perspective de modèle. Je pense que nous devons installer quelques packages (dépendances) qui nous aideront. Pour mon application, j'utilise

Laravel Livewire

pour contrôler l'interface utilisateur. Mais je n'entrerai pas dans les détails de ce package dans ce tutoriel car je veux m'assurer de pouvoir me concentrer sur l'aspect sourcing d'événements.

Comme pour la plupart des projets que je crée, quelle que soit leur taille, j'adopte une mise en page modulaire pour l'application - une approche Domain Driven Design. C'est juste quelque chose que je fais, ne vous sentez pas obligé de le suivre vous-même car c'est très subjectif.

Ma prochaine étape est de configurer mes domaines, pour cette démo je n'ai qu'un seul domaine : Culture. Dans la culture, j'ai créé des espaces de noms pour tout ce dont je pourrais avoir besoin. Mais je vais le parcourir pour que vous compreniez le processus

La première étape a été d'installer un package qui m'a permis d'utiliser le sourcing d'événements dans Laravel. Pour ce faire, j'ai utilisé un

Pack Spatie

qui a fait une grande partie du travail en arrière-plan pour moi

php artisan make:model CelebrationStoredEvent -m

Une fois installé, assurez-vous de suivre les instructions d'installation du package car la configuration et les migrations nécessitent une version. Après une installation correcte, exécutez des migrations pour remettre la base de données dans le bon état. 🎜
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');
    });
}
🎜Maintenant, nous pouvons commencer à réfléchir à la manière dont nous souhaitons mettre en œuvre le sourcing d'événements. Vous pouvez y parvenir de deux manières : un projecteur pour projeter votre état ou un agrégat. 🎜

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

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

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

Vous pouvez le faire autant que vous le souhaitez ou vous lancer dans la recherche d'événements, selon ce qui vous semble le plus logique. J'espère que ce didacticiel vous a montré un chemin clair et pratique pour commencer à utiliser le sourcing d'événements dès aujourd'hui.

Si vous sentez que vous n'êtes toujours pas satisfait après avoir lu ceci, Spatie offre généreusement un coupon de réduction de 30 % qui peut être utilisé pour la partie sourcing d'événements de leur cours Laravel, ce qui est génial ! Visitez le site Web du cours et utilisez le code promo LARAVEL-NEWS-EVENT-SOURCING.

Avez-vous déjà eu recours au sourcing événementiel ? Comment avez-vous géré cela ? Dites-le nous dans la section commentaires !

Adresse originale : https://laravel-news.com/event-sourcing-in-laravel

Adresse de traduction : https://learnku.com/laravel/t/71001

Plus de connaissances liées à la programmation, Veuillez visiter : Vidéos de programmation ! !

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Cet article est reproduit dans:. en cas de violation, veuillez contacter admin@php.cn Supprimer