LaravelでCQRS / ESをすばやく試す方法、またはPHPで銀行を書く方法







最近、 Zinc Prodポッドキャストで、私の友人と私はCQRS / ESパターンとElixirでのその実装のいくつかの機能について議論しました。 なぜなら 私は自分の仕事でLaravelを使用しています。インターネットを掘り下げず、このフレームワークのエコシステムでこのアプローチをすすめる方法を見つけられないのは罪でした。







私は皆、カットの下に招待します、私はトピックをできるだけ抽象的に説明しようとしました。







いくつかの定義



CQRS (Command Query Responsibility Segregation)-読み取り操作と書き込み操作を別々のエンティティに割り当てます。 たとえば、マスターに書き込み、レプリカから読み取ります。 CQRS。 事実と誤解 -Zen CQRSを完全に理解するのに役立ちます。

ES (イベントソーシング)-エンティティまたはエンティティセットのすべての状態変化のストレージ。

CQRS / ESは、エンティティの状態変化のすべてのイベントをイベントテーブルに保存し、これに集約とプロジェクターを追加するアーキテクチャアプローチです。

集約 -ビジネスロジックの決定(書き込みを高速化するため)に必要なプロパティをメモリに格納し、決定(ビジネスロジック)を行い、イベントを発行します。

プロジェクター -イベントをリッスンし、個別のテーブルまたはデータベースに書き込みます(読み取りを高速化するため)。













戦いに



Laravelイベントプロジェクター -Laravel CQRS / ESライブラリ

Larabankは、CQRS / ESアプローチを備えたリポジトリです。 試用します。







ライブラリ構成は、どこを調べて何であるかを教えてくれます。 event-projector.phpファイルを確認します。 仕事を説明するのに必要なもの:









移行に注意してください。 標準のLaravelテーブルに加えて、









そして今、私は新しいアカウントを作成する要求で道を打つことを提案します。







アカウント作成



標準resource



ルーティングはAccountsControllerを記述しますstore



メソッドに興味があります







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





AccountAggregateRootは、ライブラリAggregateRootを継承します。 コントローラーが呼び出したメソッドを見てみましょう。







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





persist



メソッドは、 event-projector.php設定で指定さstoreMany



たモデル(このstoreMany



storeMany



メソッドstoreMany



呼び出します。







 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







プロジェクターAccountProjectorTransactionCountProjectorProjector



実装するため、記録と同期してイベントに応答します。







OK、アカウントが作成されます。 クライアントがそれをどのように読むかを検討することを提案します。







請求書の表示



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





アカウントのプロジェクターがQueuedProjectorインターフェースを実装している場合、イベントが順番に処理されるまでユーザーには何も表示されません。







最後に、口座からの資金の補充と引き出しがどのように機能するかを調べます。







補充と引き出し



もう一度、 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(); }
      
      





AccountAggregateRootを検討する







アカウントを補充する場合:







 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







資金を引き出す場合:







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











おわりに



LaravelのCQRS / ESで「オンボーディング」のプロセスをできる限り水なしで説明しようとしました。 コンセプトは非常に興味深いですが、機能がないわけではありません。 実装する前に、次のことを覚えておいてください。









エラーに気付いてうれしいです。








All Articles