Laravel Event Sourcing (Spatie) - Using projections within business rules

萝らか妹 提交于 2019-12-10 12:04:21

问题


I know that the general concept behind event sourcing is that the state of the application should be able to be replayed from the event stream.

Sometimes, however, we need to get information for business rules from other parts of the system. i.e. An account has a user. A user has a blacklist status which is required to check if they can access/edit the account.

In the below example (purely for demonstration purposes), a user tries to subtract $10 from their account. If a user has been blacklisted, then we do not want to allow them to remove any funds from the account but we do want to record that they have tried to.

After the request is made, we could query the user model to see if the blacklist exists. If true then we can record it and throw the exception.

The user table/model is currently not event-sourced.

Now when we try to replay the event stream to re-build the projections with the state of the user is not being stored in events, it is no longer possible.

So assuming my current example does not work my questions are:

  1. If we were to move the user into an event stored system (in a different aggregate but all events within the same event-stream) then would it be acceptable to use read models within business rules?

  2. Is there any way we can mix event-sourced and CRUD into the same system when they may depend on each other for business rules.

public function subtractMoney(int $amount)
{
    if ($this->accountOwnerIsBlacklisted()){
        $this->recordThat(new UserActionBlocked());

        throw CouldNotSubtractMoney::ownerBlocked();
    }

    if (!$this->hasSufficientFundsToSubtractAmount($amount)) {
        $this->recordThat(new AccountLimitHit());

        if ($this->needsMoreMoney()) {
            $this->recordThat(new MoreMoneyNeeded());
        }

        $this->persist();

        throw CouldNotSubtractMoney::notEnoughFunds($amount);
    }

    $this->recordThat(new MoneySubtracted($amount));
}

private function accountOwnerIsBlacklisted(): bool
{
    return $this->accountRepositry()->ownerUser()->isBlackListed();
}

回答1:


Since you are basically working with DDD (without mentioning it) the answer could lie in the definitions there. In DDD you are supposed to define the boundaries of each aggregate root. Each aggregate root should not store any dependencies to other aggregate roots (The Spatie package doesn't even support it). It should only be made up of the events, which then become the single source of truth.

Given your example, it seems that the blocking of a user is not due to negative events on his account, but rather due to something that happened in relation to his user (account owner). The keyword here seems to be "owner". If you want to store the fact that the user action of trying to withdraw money happened, then you could still apply the event, but the reason would, in this case, come from another aggregate "the user". It doesn't matter if the user itself is event sourced, but the user entity has the method to check if the user is blocked, and therefore it is the business rule in your system that he is not allowed to make withdrawals from the account. If you cannot model these two together, then I would suggest that you design a domain service which can handle this command. Try to keep them as a part of your model to avoid making your domain model anaemic, if you can.

<?php 
class AccountWithdrawalService
{
    public function __construct(UserRepository $userRepository)
    {
        $this->userRepository = $userRepository;
    }

    public function withdraw($userId, $accountId, $amount)
    {
        $user = $this->userRepository->find($userId);

        // You might inject AccountAggregateRoot too.
        $account = AccountAggregateRoot::retrieve($accountId);

        if(!$user->isBlackListed())
        {
            $account->subtractMoney($amount);
        }
        else
        {
            // Here we record the unhappy road :-( 
            $account->moneySubtractionBlocked($amount);
        }

        $account->persist();
    }
}

PS: A further possibility is to inject your userRepository in the actual method handling the withdrawal, as long as the userRepository is not a full dependency of the AccountAggregateRoot. This, I believe, is highly discussed.



来源:https://stackoverflow.com/questions/58455116/laravel-event-sourcing-spatie-using-projections-within-business-rules

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!