laravel-json-api / laravel

JSON:API for Laravel applications
MIT License
551 stars 42 forks source link

[Question] Event Sourcing with Eloquent Resources? #290

Closed tsterker closed 4 months ago

tsterker commented 4 months ago

Apologies if there is a better way to submit such open-ended questions. In this case, please let me know or simply close the issue.

What I want to do

I want to adopt event sourcing (via verbs) for my application that is using laravel-json-api (the go-to package I don't want to live without).

In essence, it means that there should not be any direct model creation, but I would fire events instead with the relevant request data.

Given that everything would still be backed by Eloquent models, I think the existing implementation is a great start for all read and the main thing that would differ is how I create/update my models.

FYI: I have not yet extensively thought about or researched how well REST/JSON:API and event sourcing go together. But from a conceptual point of view, I don't see any conflict at this point.

What I'm currently doing

I'm currently simply implementing custom controller actions to hand over the validated data to my events. I ensure that my events are "committed" to be reflected in the DB and then I simply query and return the eloquent model I expected to be created via the event.

Example:

// routes/api.php
JsonApiRoute::server('v0')
    ->prefix('v0')
    ->resources(function (ResourceRegistrar $server) {
        $server
            ->resource('posts', PostController::class)
            ->only('store', 'index', 'read');
    });
// app/Http/Controllers/Api/V0/PostController.php

// ...

    public function store(Route $route, StoreContract $store)
    {
        $request = ResourceRequest::forResource(
            $resourceType = $route->resourceType()
        );

        $data = $request->validated();

        PostCreated::fire(
            post_id: $postId = snowflake_id(),
            title: $data['title'],
            body: $data['body'],
        );

        Verbs::commit();

        return DataResponse::make(Post::find($postId)->sole())
            ->withQueryParameters(ResourceQuery::queryOne($resourceType))
            ->didCreate();
    }

// ...

What I Imagine

I was thinking about if/how I should eventually implement my own "non-eloquent" Resource (?), where I would somehow be mapping schemas to event states. Or maybe I should keep the mapping to Eloquent models, but somehow be able map certain API routes/actions to certain events being fired instead of the mapped eloquent models to be updated directly.

I think I would need to put some more thought into this to understand the different moving parts, but for now I was wondering how I could:

I acknowledge that this is a long-winded post. In essence, I would like to

haddowg commented 4 months ago

I would suggest asking this in the questions channel of this package's slack community here: https://join.slack.com/t/laraveljsonapi/shared_invite/zt-e3oi2r4y-8nkmhzpKnPQViaXrkPJHtQ

but your approach seems sensible, I suspect you could create your own re-usable action traits for the write actions and then its simply a matter of swapping the eloquent ones for your own, but that would depend on how you convert the request to an event... if they are close to your eloquent model schema then you can likely repurpose the ModelHydrator to use your schema to map it to a model.

lindyhopchris commented 4 months ago

Hi! Thanks for the question. Your approach is exactly how I would do it too. The only thing I'd do different is I'd resolve the ResourceQuery before dispatching the event - because there's no point dispatching the request if the query is going to fail.

So I'd do this:

// app/Http/Controllers/Api/V0/PostController.php

// ...

    public function store(Route $route, StoreContract $store)
    {
        $request = ResourceRequest::forResource(
            $resourceType = $route->resourceType()
        );

        $query = ResourceQuery::queryOne($resourceType);

        $data = $request->validated();

        PostCreated::fire(
            post_id: $postId = snowflake_id(),
            title: $data['title'],
            body: $data['body'],
        );

        Verbs::commit();

        return DataResponse::make(Post::find($postId)->sole())
            ->withQueryParameters($query)
            ->didCreate();
    }

// ...

In the future I might make it easier to override the command that does the modification, so that overriding the controller is not necessary. But for the moment, definitely use this approach.