Comment essayer rapidement CQRS / ES dans Laravel ou écrire une banque en PHP


Récemment, dans le podcast Zinc Prod , mes amis et moi avons discuté du modèle CQRS / ES et de certaines fonctionnalités de sa mise en œuvre dans Elixir. Parce que J'utilise Laravel dans mon travail, c'était un péché de ne pas plonger dans Internet et de ne pas trouver comment on peut siroter cette approche dans l'écosystème de ce framework.


J'invite tout le monde sous la coupe, j'ai essayé de décrire le sujet le plus abstraitement possible.


Quelques définitions


CQRS (Command Query Responsibility Segregation) - allocation des opérations de lecture et d'écriture en entités distinctes. Par exemple, nous écrivons au maître, lisons à partir de la réplique. CQRS. Faits et idées fausses - Aide à acquérir une connaissance approfondie du Zen CQRS.
ES (Event Sourcing) - stockage de tous les changements d'état d'une entité ou d'un ensemble d'entités.
CQRS / ES est une approche architecturale dans laquelle nous enregistrons tous les événements d'un changement d'état d'une entité dans la table d'événements et y ajoutons un agrégat et un projecteur.
Agrégat - stocke en mémoire les propriétés nécessaires pour prendre des décisions de logique métier (pour accélérer l'écriture), prend des décisions (logique métier) et publie des événements.
Projecteur - écoute les événements et écrit dans des tables ou des bases de données distinctes (pour une lecture plus rapide).



Au combat


Projecteur d'événements Laravel - Bibliothèque CQRS / ES pour Laravel
Larabank est un référentiel avec une approche CQRS / ES. Nous allons le prendre pour le procès.


La configuration de la bibliothèque vous dira où chercher et dira de quoi il s'agit. Nous regardons le fichier event-projecteur.php . Du nécessaire pour décrire le travail:


  • projectors - enregistrer les projecteurs;
  • reactors - enregistrer les réacteurs. Reactor - dans cette bibliothèque ajoute des effets secondaires au traitement des événements, par exemple, dans ce référentiel, si vous essayez de dépasser la limite de retrait trois fois, l'événement MoreMoneyNeeded est écrit et un message est envoyé à l'utilisateur au sujet de ses difficultés financières;
  • replay_chunk_size - taille du morceau de relecture. L'une des fonctionnalités d'ES est la possibilité de restaurer l'historique des événements. Projecteur d'événements Laravel préparé pour une fuite de mémoire lors d'une telle opération en utilisant ce paramètre.

Faites attention à la migration. En plus des tables Laravel standard, nous avons


  • stored_events - la table ES principale avec plusieurs colonnes de données non structurées pour les métadonnées, nous stockons les types d'événements dans une rangée. Importante colonne aggregate_uuid - stocke l'uuid de l'agrégat pour recevoir tous les événements qui lui sont liés;
  • accounts - le tableau du projecteur de comptes d'utilisateurs, est nécessaire pour le retour rapide des données actuelles sur l'état du solde;
  • transaction_counts - une table du projecteur du nombre de transactions utilisateur, nécessaire pour un retour rapide du nombre de transactions terminées.

Et maintenant, je propose de prendre la route avec une demande de création d'un nouveau compte.


Création de compte


Le routage de resource standard décrit AccountsController . Nous sommes intéressés par la méthode du store


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

AccountAggregateRoot hérite de la bibliothèque AggregateRoot . Examinons les méthodes appelées par le contrôleur.


 //  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; } 

La méthode persist appelle la méthode storeMany le modèle spécifié dans la configuration event-projecteur.php comme stored_event_model dans notre cas, 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'))); }); } 

* Projecteur en file d'attente


Les projecteurs AccountProjector et TransactionCountProjector implémentent Projector ils répondront donc aux événements de manière synchrone avec leur enregistrement.


Ok, un compte a été créé. Je propose de considérer comment le client le lira.


Affichage des factures


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

Si le projecteur de comptes implémente l'interface QueuedProjector , l'utilisateur ne verra rien tant que l'événement n'aura pas été traité à son tour.


Enfin, nous étudierons le fonctionnement du réapprovisionnement et du retrait d'argent du compte.


Recharge et retrait


Encore une fois, regardez le AccountsController :


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

Considérez AccountAggregateRoot


lors de la reconstitution du compte:


 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


lors du retrait de fonds:


 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


J'ai essayé de décrire le processus «d'intégration» dans CQRS / ES sur Laravel comme étant sans eau possible. Le concept est très intéressant, mais pas sans fonctionnalités. Avant la mise en œuvre, n'oubliez pas:


  • cohérence éventuelle;
  • il est souhaitable d'utiliser DDD dans des domaines séparés, vous ne devez pas créer un grand système complètement sur ce modèle;
  • les modifications du schéma de la table d'événements peuvent être très douloureuses;
  • De manière responsable, il convient d'aborder le choix de la granularité des événements, plus il y a d'événements concrets, plus ils seront sur la table et plus il faudra de ressources pour travailler avec eux.

Je serai heureux de constater des erreurs.

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


All Articles