📜 ⬆️ ⬇️

How to quickly try CQRS / ES in Laravel or write a bank in PHP


Recently, in a podcast on Zinc Products , my colleagues and I discussed the CQRS / ES pattern and some features of its implementation in Elixir. Because I use Laravel in my work, it was a sin not to rummage through the Internet and not to find out how to approach this approach in the ecosystem of this framework.


I invite everyone under the cat, tried to describe the topic as briefly as possible.


Little definitions


CQRS (Command Query Responsibility Segregation) - selection of read and write operations into separate entities. For example, we write to the master, we read from the replica. CQRS. Facts and Fallacies - will help to thoroughly learn Zen CQRS.
ES (Event Sourcing) - storing all state changes of an entity or set of entities.
CQRS / ES is an architectural approach where we save all events of a state change of an entity in the event table and add an aggregate and a projector to it.
Aggregate - stores in memory the properties necessary for making decisions by the business of logic (to speed up writing), makes decisions (business logic) and publishes events.
Projector - listens to events and writes in separate tables or bases (to speed up reading).



To battle


Laravel event projector - CQRS / ES library for Laravel
Larabank is a repository with a CQRS / ES implemented approach. And take it on trial.


The library configuration will tell you where to look and tell you what it is. See the event-projector.php file. From the necessary to describe the work:



Pay attention to migration. In addition to standard Laravel tables, we have



And now I propose to go along with the request to create a new account.


Account creation


Standard resource routing describes Accounts Controls . We are interested in the store method


 public function store(Request $request) { $newUuid = Str::uuid(); //   ,   uuid  //      AccountAggregateRoot::retrieve($newUuid) //           ->createAccount($request->name, auth()->user()->id) //          ->persist(); return back(); } 

AccountAggregateRoot inherits the AggregateRoot library. Let's see the methods that the controller called.


 //  uuid      public static function retrieve(string $uuid): AggregateRoot { $aggregateRoot = (new static()); $aggregateRoot->aggregateUuid = $uuid; return $aggregateRoot->reconstituteFromEvents(); } public function createAccount(string $name, string $userId) { //        //  ,   recordThat,  ,    , // ..     ) $this->recordThat(new AccountCreated($name, $userId)); return $this; } 

The persist method calls the storeMany method storeMany the model specified in the configuration event-projector.php as stored_event_model in our case StoredEvent


 public static function storeMany(array $events, string $uuid = null): void { collect($events) ->map(function (ShouldBeStored $domainEvent) use ($uuid) { $storedEvent = static::createForEvent($domainEvent, $uuid); return [$domainEvent, $storedEvent]; }) ->eachSpread(function (ShouldBeStored $event, StoredEvent $storedEvent) { //   ,     // QueuedProjector* Projectionist::handleWithSyncProjectors($storedEvent); if (method_exists($event, 'tags')) { $tags = $event->tags(); } //         $storedEventJob = call_user_func( [config('event-projector.stored_event_job'), 'createForEvent'], $storedEvent, $tags ?? [] ); dispatch($storedEventJob->onQueue(config('event-projector.queue'))); }); } 

* QueuedProjector


The AccountProjector and TransactionCountProjector projectors implement the Projector so that they will react to events synchronously with their recording.


Ok, account created. I propose to consider how the client will read it.


Invoice display


 //    `accounts`     id public function index() { $accounts = Account::where('user_id', Auth::user()->id)->get(); return view('accounts.index', compact('accounts')); } 

If the invoice projector implements the QueuedProjector interface, then the user will not see anything until the event is processed in turn.


Finally, we will study how the deposit and withdrawal of money works.


Deposit and withdrawal


Again, look at the AccountsController controller:


 //    uuid  //       //   ,     public function update(Account $account, UpdateAccountRequest $request) { $aggregateRoot = AccountAggregateRoot::retrieve($account->uuid); $request->adding() ? $aggregateRoot->addMoney($request->amount) : $aggregateRoot->subtractMoney($request->amount); $aggregateRoot->persist(); return back(); } 

Consider AccountAggregateRoot


when replenishing an account:


 public function addMoney(int $amount) { $this->recordThat(new MoneyAdded($amount)); return $this; } //    ""  recordThat // AggregateRoot*? //     apply(ShouldBeStored $event), //       'apply' . EventClassName  // ,     `MoneyAdded` protected function applyMoneyAdded(MoneyAdded $event) { $this->accountLimitHitInARow = 0; $this->balance += $event->amount; } 

* AggregateRoot


when withdrawing funds:


 public function subtractMoney(int $amount) { 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)); } protected function applyMoneySubtracted(MoneySubtracted $event) { $this->balance -= $event->amount; $this->accountLimitHitInARow = 0; } 


Conclusion


I tried as much as possible without water to describe the onboarding process in CQRS / ES on Laravel. The concept is very interesting, but not without features. Before embedding remember:



I will be glad to see the errors.


')

Source: https://habr.com/ru/post/448000/


All Articles