Cómo probar rápidamente CQRS / ES en Laravel o escribir un banco en PHP


Recientemente, en el podcast Zinc Prod , mis amigos y yo discutimos el patrón CQRS / ES y algunas características de su implementación en Elixir. Porque Utilizo Laravel en mi trabajo, fue un pecado no profundizar en Internet y no encontrar cómo puede saborear este enfoque en el ecosistema de este marco.


Invito a todos los que están debajo del corte, traté de describir el tema de la manera más abstracta posible.


Algunas definiciones


CQRS (segregación de responsabilidad de consulta de comando): asignación de operaciones de lectura y escritura en entidades separadas. Por ejemplo, escribimos al maestro, leemos desde la réplica. CQRS. Hechos y conceptos erróneos : ayuda a obtener un conocimiento profundo de Zen CQRS.
ES (Event Sourcing): almacenamiento de todos los cambios de estado de una entidad o conjunto de entidades.
CQRS / ES es un enfoque arquitectónico en el que guardamos todos los eventos del cambio de estado de una entidad en la tabla de eventos y agregamos un agregado y un proyector a esto.
Agregado : almacena en la memoria las propiedades necesarias para tomar decisiones de lógica de negocios (para acelerar la escritura), toma decisiones (lógica de negocios) y publica eventos.
Proyector : escucha eventos y escribe en tablas o bases de datos separadas (para una lectura más rápida).



En batalla


Proyector de eventos Laravel - biblioteca CQRS / ES para Laravel
Larabank es un repositorio con un enfoque CQRS / ES. Lo llevaremos a prueba.


La configuración de la biblioteca le dirá dónde buscar y qué es. Nos fijamos en el archivo event -jector.php . De los necesarios para describir el trabajo:


  • projectors - projectors registro;
  • reactors - registre reactores. Reactor: en esta biblioteca se agregan efectos secundarios al procesamiento de eventos, por ejemplo, en este repositorio, si intenta exceder el límite de retiro tres veces, se escribe el evento MoreMoneyNeeded y se envía un mensaje al usuario sobre sus dificultades financieras;
  • replay_chunk_size : tamaño del fragmento de reproducción. Una de las características de ES es la capacidad de restaurar el historial de eventos. Proyector de eventos Laravel preparado para una pérdida de memoria durante una operación de este tipo utilizando esta configuración.

Presta atención a la migración. Además de las tablas estándar de Laravel, tenemos


  • stored_events : la tabla principal de ES con varias columnas de datos no estructurados para datos de metaeventos, almacenamos los tipos de eventos en una fila. Columna importante aggregate_uuid : almacena el uido del agregado para recibir todos los eventos relacionados con él;
  • accounts : la tabla del proyector de cuentas de usuario es necesaria para el rápido retorno de los datos actuales sobre el estado del saldo;
  • transaction_counts : una tabla del proyector del número de transacciones del usuario, necesarias para la devolución rápida del número de transacciones completadas.

Y ahora propongo salir a la carretera con una solicitud para crear una nueva cuenta.


Creación de cuenta


El enrutamiento de resource estándar describe AccountsController . Estamos interesados ​​en el método de la store


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

AccountAggregateRoot hereda la biblioteca AggregateRoot . Veamos los métodos que llamó el controlador.


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

El método persist llama al método storeMany el modelo especificado en la configuración de stored_event_model como stored_event_model en nuestro caso, 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'))); }); } 

* Proyector en cola


Los proyectores AccountProjector y TransactionCountProjector implementan el Projector por lo tanto, responderán a los eventos sincrónicamente con su grabación.


Ok, se ha creado una cuenta. Propongo considerar cómo lo leerá el cliente.


Visualización de la factura


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

Si el proyector de cuentas implementa la interfaz del Proyector en cola , el usuario no verá nada hasta que el evento se procese por turno.


Finalmente, estudiaremos cómo funciona la reposición y el retiro de dinero de la cuenta.


Recarga y retirada


Nuevamente, mire el AccountController :


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

Considere AccountAggregateRoot


al reponer la cuenta:


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

* AgregadoRoot


al retirar fondos:


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


Conclusión


Traté de describir el proceso de "incorporación" en CQRS / ES en Laravel lo más libre de agua posible. El concepto es muy interesante, pero no sin características. Antes de implementar, recuerde:


  • eventual consistencia;
  • es deseable usar DDD en dominios separados; no debe hacer un sistema grande completamente en este patrón;
  • Los cambios en el esquema de la tabla de eventos pueden ser muy dolorosos;
  • Responsablemente, vale la pena abordar la elección de la granularidad de los eventos, cuanto más concretos sean los eventos, más estarán en la mesa y se necesitarán más recursos para trabajar con ellos.

Estaré encantado de notar errores.

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


All Articles