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

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:
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?
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();
}

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.

Give the Account a UserBlacklistedEvent and use this for validation in the Account. Don't record failed attempts unless thats a reporting requirement.
<?php
class Account {
private bool blacklisted;
function blacklist() {
$this->recordThat(new Blacklisted());
}
public function subtractMoney(int $amount)
{
if ($this->blacklisted) {
throw new DomainException("Why on earth has this backend developer let fella make queries to their account. fk em - wait call security we being attaced");
}
...
<?php
class BlacklistService
{
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
public function blacklist($userId, $accountId)
{
$user = $this->userRepository->find($userId);
// You might inject AccountAggregateRoot too.
$account = AccountAggregateRoot::retrieve($user->accountUuid);
$user->blacklist();
$user->save();
$account->blacklisted(); // no need for userid, in most cases
$account->persist();
}
}
<?php
class AccountCreateService
{
function create() {
$user = $this->userRepository->find($userId);
if ($user->blacklisted) {
throw new DomainException("Why on earth has the frontend developer called this.");
}
$account = AccountAggregateRoot::retrieve($user->accountUuid);
...
Even better: don't throw a Exception in Account AR. just return void or and $this->recordThat(new SubtractionEventOnBlacklistedEvent). Since its invariant is protected. Why throw an event? or if its invarient is protected then the invalid case ins't an exceptional circumstance
The exception after all won't do anything for the user. Its unlightly you wan't them reading the exception screen. And then in this case, you could setup some alternate logging perhaps

Related

DDD - how to deal with get-or-create logic in Application Layer?

I have an DailyReport Entity in my Domain Layer. There are some fields in this object:
reportId
userId
date
tasks - Collection of things that user did in given day;
mood - how does the user felt during the whole day;
Also, there are some methods in my Application Service:
DailyReportService::addTaskToDailyReport
DailyReportService::setUserMoodInDailyReport
The thing is that both of these methods require DailyReport to be created earlier or created during function execution. How to deal with this situation?
I have found 2 solutions:
1 Create new DailyReport object before method dispatching, and after that pass reportId to them:
//PHP, simplified
public function __invoke() {
$taskData = getTaskData();
/** #var $dailyReport DailyReport|null **/
$dailyReport = $dailyReportRepository->getOneByDateAndUser('1234-12-12', $user);
//there were no report created today, create new one
if($dailyReport === null) {
$dailyReport = new DailyReport('1234-12-12', $user);
$dailyReportRepository->store($dailyReport);
}
$result = $dailyReportService->addTaskToDailyReport($taskData, $dailyReport->reportId);
//[...]
}
This one requires to put a more business logic to my Controller which i want to avoid.
2: Verify in method that DailyReport exists, and create new one if needed:
//my controller method
public function __invoke() {
$taskData = getTaskData();
$result = $dailyReportService->addTaskToDailyReport($taskData, '1234-12-12', $user);
//[...]
}
//in my service:
public function addTaskToDailyReport($taskData, $date, $user) {
//Ensure that daily report for given day and user exists:
/** #var $dailyReport DailyReport|null **/
$dailyReport = $dailyReportRepository->getOneByDateAndUser();
//there were no report created today, create new one
if($dailyReport === null) {
$dailyReport = new DailyReport($date, $user);
$dailyReportRepository->store($dailyReport);
}
//perform rest of domain logic here
}
This one reduces complexity of my UI layer and does not expose business logic above the Application Layer.
Maybe these example is more CRUD-ish than DDD, but i wanted to expose one of my use-case in simpler way.
Which solution should be used when in these case? Is there any better way to handle get-or-create logic in DDD?
EDIT 2020-03-05 16:21:
a 3 example, this is what i am talking about in my first comment to Savvas Answer:
//a method that listens to new requests
public function onKernelRequest() {
//assume that user is logged in
$dailyReportService->ensureThereIsAUserReportForGivenDay(
$userObject,
$currentDateObject
);
}
// in my dailyReportService:
public function ensureThereIsAUserReportForGivenDay($user, $date) {
$report = getReportFromDB();
if($report === null) {
$report = createNewReport();
storeNewReport();
}
return $report;
}
//in my controllers
public function __invoke() {
$taskData = getTaskData();
//addTaskToDailyReport() only adds the data to summary, does not creates a new one
$result = $dailyReportService->addTaskToDailyReport($taskData, '1234-12-12', $user);
//[...]
}
This will be executed only when user will log in for the first time/user were logged in yesterday but this is his first request during the new day.
There will be less complexity in my business logic, i do not need to constantly checking in services/controllers if there is a report created because this has been executed
previously in the day.
I'm not sure if this is the answer you want to hear, but basically I think you're dealing with accidental complexity, and you're trying to solve the wrong problem.
Before continuing I'd strongly suggest you consider the following questions:
What happens if someone submits the same report twice
What happens if someone submits a report two different times, but in the second one, it's slightly different?
What is the impact of actually storing the same report from the same person twice?
The answers to the above questions should guide your decision.
IMPORTANT: Also, please note that both of your methods above have a small window where two concurrent requests to store the rerport would succeed.
From personal experience I would suggest:
If having duplicates isn't that big a problem (for example you may have a script that you run manually or automatically every so often that clears duplicates), then follow your option 1. It's not that bad, and for human scale errors should work OK.
If duplicates are somewhat of a problem, have a process that runs asynchronously after reports are submited, and tries to find duplicates. Then deal with them according to how your domain experts want (for example maybe duplicates are deleted, if one is newer either the old is deleted or flagged for human decision)
If this is part of an invariant-level constraint in the business (although I highly doubt it given that we're speaking about reports), and at no point in time should there ever be two reports, then there should be an aggregate in place to enforce this. Maybe this is UserMonthlyReport or whatever, and you can enforce this during runtime. Of course this is more complicated and potentially a lot more work, but if there is a business case for an invariant, then this is what you should do. (again, I doubt it's needed for reports, but I write it here in the care reports were used as an example, or for future readers).

How do I inject behaviour into an aggregate?

I’m working on an aggregate where certain behaviours can be performed by multiple roles within the application. But before that happens fairly complex validation occurs. It’s this validation that differs per role. Typically it means different configuration settings are checked to determine if the action can be performed.
So, as an example: lets say i have an Order to which i can add OrderLines. If i have role Employee i might be allowed to order up to € 100,- and if i have role Purchaser i might be allowed to order up to € 1000,-.
You could solve this by providing the user instance to the addOrderLine method but that leaks the user context into the ordering context. The next logical thing, and this is what I’ve been doing, is in to inject that validation logic into the method call. I’m calling those methods policies and instantiate the right policy in the application service as i have the relevant user info available there:
<?php
class Order {
public function addItem(OrderPolicy $policy, Item $item, int $amount) {
if (!$policy->canPurchase($item->getPrice() * $amount))
throw new LimitExceededException();
/* add item here */
}
class OrderService {
public function addItem(User $user, $orderId, $itemId, int $amount) {
$order = $this->orderRepo->getForUser($user, $orderId);
$item = $this->itemRepo->get($itemId);
$policy = $this->getOrderPolicyFor($user);
$order->addItem($policy, $item, $amount);
}
}
class PurchaserOrderPolicy
{
function canPurchase($amount) {
return ($amount <= 1000);
}
}
This seems fine, but now it seems to me my ordering context has logic based on user roles (the policy classes) its not supposed to know about.
Does DDD offer any other ways of dealing with this? Maybe Specification? Does this seem fine? Where do the policy classes belong?
It seems that you have two subdomains/systems involved here: ordering system and buying policies system. You need to keep thing separated, your gut was correct. This means that the validation about the maximum order value is checked in an eventual consistent manner relative to the actual item adding. You could do this before (you try to prevent an invalid order) or after (you try to recover from an invalid order).
If you do it before then the application service could orchestrate this and reject the order.
If you do it after then you could implement this as a process and have a ApplyPoliciesToOrdersSaga that listen to the ItemWasAddedToTheOrder event (if you have an event-driven architecture) or that runs as a scheduled task/cron job and checks all orders against the policies (in a classical architecture).

Symfony Workflow Component and Security Voters?

TL;DR: how can you add custom constraints (i.e. security voters) to transitions?
My application needs some workflow management system, so I'd like to try Symfony's new Workflow Component. Let's take a Pull Request workflow as an example.
In this example, only states and their transitions are describes. But what if I want to add other constraints to this workflow? I can image some constraints:
Only admins can accept Pull Request
Users can only reopen their own Pull Request
Users can not reopen PR's older than 1 year
While you can use Events in this case, I don't think that's the best way to handle it, because an event is fired after $workflow->apply(). I want to know beforehand if a user is allowed to change the state, so I can hide or disable the button. (not like this).
The LexikWorkflowBundle solved this problem partially, by adding roles to the steps (transitions). Switching to this bundle might be a good idea, but I'd like to figure out how I can solve this problem without.
What is the best way to add custom entity constraints ('PR older than 1 year can't be reopened') and security constraints ('only admins can accept PR's', maybe by using Symfony's Security Voters) to transitions?
Update:
To clarify: I want to add permission control to my workflow, but that doesn't necessarily mean I want to tightly couple it to the Workflow Component. I'd like to stick to good practices, so the given solution should respect the single responsibility principle.
The best way I found was implementing the AuthorizationChecker in the Workflow's GuardListener.
The demo application gives a good example:
namespace Acme\DemoBundle\Entity\Listener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationChecker;
use Symfony\Component\Workflow\Event\GuardEvent;
class GuardListener implements EventSubscriberInterface
{
public function __construct(AuthorizationCheckerInterface $checker)
{
$this->checker = $checker;
}
public function onTransition(GuardEvent $event)
{
// For all action, user should be logger
if (!$this->checker->isGranted('IS_AUTHENTICATED_FULLY')) {
$event->setBlocked(true);
}
}
public function onTransitionJournalist(GuardEvent $event)
{
if (!$this->checker->isGranted('ROLE_JOURNALIST')) {
$event->setBlocked(true);
}
}
public function onTransitionSpellChecker(GuardEvent $event)
{
if (!$this->checker->isGranted('ROLE_SPELLCHECKER')) {
$event->setBlocked(true);
}
}
public static function getSubscribedEvents()
{
return [
'workflow.article.guard' => 'onTransition',
'workflow.article.guard.journalist_approval' => 'onTransitionJournalist',
'workflow.article.guard.spellchecker_approval' => 'onTransitionSpellChecker',
];
}

Authorization and Policies for Guest users

Case: I'm building a forum using Laravel's Authorization as a backbone using policies. Examples of checks I run are stuff like #can('view', $forum), and #can('reply', $topic), Gate::allows('create_topic', $forum) etc. These checks basically checks if the users role has that permission for the specific forum, topic or post. That way I can give roles very specific permissions for each forum in my application.
The issue is that all of these checks go through the Gate class, specifically a method called raw() which in its first line does this:
if (! $user = $this->resolveUser()) {
return false;
}
This presents an issue when dealing with forums. Guests to my application should also be allowed to view my forum, however as you can see from the code above, Laravels Gate class automatically returns false if the user is not logged in.
I need to be able to trigger my policies even if there is no user. Say in my ForumPolicy#view method, I do if(User::guest() && $forum->hasGuestViewAccess()) { return true }
But as you can see, this method will never trigger.
Is there a way for me to still use Laravel's authorization feature with guest users?
I'm not aware of a super natural way to accomplish this, but if it's necessary, you could change the gate's user resolver, which is responsible for finding users (by default it reads from Auth::user()).
Since the resolver is protected and has no setters, you'll need to modify it on creation. The gate is instantiated in Laravel's AuthServiceProvider. You can extend this class and replace the reference to it in the app.providers config with your subclass.
It's going to be up to you what kind of guest object to return (as long as it's truthy), but I'd probably use something like an empty User model:
protected function registerAccessGate()
{
$this->app->singleton(GateContract::class, function ($app) {
return new Gate($app, function () use ($app) {
$user = $app['auth']->user();
if ($user) {
return $user;
}
return new \App\User;
});
});
}
You could go a step further and set a special property on it like $user->isGuest, or even define a special guest class or constant.
Alternatively you could adjust your process at the Auth level so that all logged-out sessions are wrapped in a call to Auth::setUser($guestUserObject).
I just released a package that allows permission logic to be applied to guest users. It slightly modifies Laravel's Authorization to return a Guest object instead of null when no user is resolved. Also every authorization check now makes it to the Gate instead of failing authorization instantly because there isn't an authenticated user.

Dynamic custom ACL in zend framework?

I need a solution where authenticated users are allowed access to certain Controllers/Actions based not on their user type :ie. admin or normal user (although I may add this using standard ACL later) but according to the current status of their user.
For example :
Have they been a member of the site for more than 1 week?
Have they filled in their profile fully?
Actually, now that I think about it, kind of like they have on this site with their priviledges and badges.
For dynamic condition-based tests like you are describing, you can use dynamic assertions in your Zend_Acl rules.
For example:
class My_Acl_IsProfileComplete implements Zend_Acl_Assert_Interface
{
protected $user;
public function __construct($user)
{
$this->user = $user;
}
public function assert(Zend_Acl $acl,
Zend_Acl_Role_Interface $role = null,
Zend_Acl_Resource_Interface $resource = null,
$privilege = null)
{
// check the user's profile
if (null === $this->user){
return false;
}
return $this->user->isProfileComplete(); // for example
}
}
Then when defining your Acl object:
$user = Zend_Auth::getInstance()->getIdentity();
$assertion = new My_Acl_Assertion_IsProfileComplete($user);
$acl->allow($role, $resource, $privilege, $assertion);
Of course, some of the details depend upon the specifics of what you need to check and what you can use in your depend upon what you store in your Zend_Auth::setIdentity() call - only a user Id, a full user object, etc. And the roles, resources, and privileges are completely app-specific. But hopefully this gives the idea.
Also, since the assertion object requires a user object at instantiation, this dynamic rule cannot be added at Bootstrap. But, you can create the core Acl instance with static rules during bootstrap and then register a front controller plugin (to run at preDispatch(), say) that adds the dynamic assertion. This way, the Acl is fully populated by the time you get to your controllers where presumably you would be checking them.
Just thinking out loud.

Categories