Laravelアプリにリポジトリパターンを適用する

目的と背景

主に学習目的でwebアプリをAWSやGCPなどで運用しているのですが、なるべく低コストで運用しようとして各サービスの無料枠で色々頑張っているとデータのIO先をころころ変えたくなります

でちょうど良くLaravelのEloquentで簡単に実装したCRUDがあったので、これにリポジトリパターンを適用してアプリケーションで利用するデータのストレージエンジンの交換を容易にできる状態を実装してみました。

現状

  • 外部のAPIから取得したデータをMySQLにバッチで保存している
  • MySQLに保存されたデータをEloquentを利用して取得している

課題

  • GCPのCloudSQL(MySQL)が意外と高い(約5000円/月)

やりたいこと

  • CloudSQLをいったん停止したい
  • ViewがEloquent依存しちゃってるけどAPI叩いてもらったレスポンスでEloquentインスタンス生成すればアプリの実装をほとんど変えずにいける
  • でもパフォーマンス的に都合が悪かったりしたらまたMySQLに戻りたい

→ リポジトリパターンで交換しやすいようにした上でMySQL使わない実装に取り替えよう

実装before / after
before

↑ Eloquentのall、findメソッドを利用してコントローラでデータ取得を行なっています。今回の要件は

  • 元々のデータソースは外部のAPIからJSONで取得できるものである
  • Eloquentと、EloquentのCollectionをそれぞれ返してViewで使っているのでそれはそのまま使いたい(ViewのEloquent依存は一旦許容する)
  • 都合が悪かったらすぐ元に戻したい

なので以下の仕様を満たす実装をします。

  • Controllerからは直接データストレージを操作せずRepositoryを介する
  • 実装の交換はServiceProviderでInterfaceのbind先を変更するのみで対応できるようにする

after(実装サンプル)

↑ Repositoryにデータの取得処理を隠蔽した状態です。こうすることでEloquentとControllerを疎にしてRepositoryがどんな方法でデータを取得するのかControllerは気にしなくて良くなります。実際のRepository実装ではなくInterfaceを注入していますが、このInterfaceにどの実装を注入するかを後述のServiceProviderで解決することで実装の取り替えを容易にするという目的を達成します。

↑ Repositoryの中身です。これはEloquentでデータソースを取得するパターンのものです。

↑ Interfaceです。

↑ ServiceProviderでInterfaceとRepositoryをbindします。実装を取り替えたい場合はここでクラスを切り替えるだけで対応できます。今回は1つのInterfaceにつき常に1つのRepository実装がbindされていますが、同じInterfaceでも特定のクラスにおいては異なるクラスを注入したいという場合には以下のように制御できます。

$this->app->when(PhotoController::class)
->needs(Filesystem::class)
->give(function () {
return Storage::disk('local');
});
$this->app->when([VideoController::class, UploadController::class])
->needs(Filesystem::class)
->give(function () {
return Storage::disk('s3');
});

↑ CloudSQLをいったん落とすために取り替えたAPIからデータを取得してEloquentを生成して返す実装です。めちゃめちゃ適当ですが急ぎだったので雑なまま終わらせました。

まとめ

無事にMySQLからデータ取得していた実装をAPIからJSONで取得する実装に交換してCloudSQLを停止できました。

今回はインフラコスト都合でIO先を切り替えやすくしたいというのがあったのでRepositoryパターンをLaravelっぽく適用して対応しました。

次回はValueObjectやDomainModelなどDDD的な文脈でのPOPOを活用して複雑なビジネスロジックを複数含むアプリケーションをいかにシンプルに維持して制御するかというところを紹介したいと思います。