【Laravel】Service層を使用しControllerの肥大化を防止する

2023/09/28に公開

はじめに

Service層に切り分け、Controllerの処理を簡潔にする方法について
今まで学んだことを備忘録として残します。

Service層に切り分けるメリット

  • 可読性が高くなる
    • ControllerはHTTPリクエストを受けて、最終的にHTMLもしくはJSONなどを返すことが本来の目的
  • 再利用しやすくなる
    • 一度書いた処理を他の場所でも使えるようになる
  • 単体でテストを行いやすくなる
    • 一つ一つの機能(メソッド)を独立してテストできる
  • ソースコードを調査しやすい
    • 責任が明確に分かれるので、どこを探せばいいかがわかりやすい

など多くのメリットがあります。

ControllerとService層の切り分け例

ビフォー

簡易的ですが、以下は記事登録でControllerに処理を集めた時のビフォー例です。

ArticleController
    public function store(StoreArticleRequest $request)
    {
        try {
	  $article = new Article();
          $article->fill([
              'title' => $request->title,
              'body' => $request->body,
              'start_date' => $request->start_date,
          ])->save();
          return response()->json(['success' => '登録に成功しました。']);
        } catch (Exception $e) {
          return response()->json(['error' => '登録に失敗しました。'], 400);
        }
    }

アフター

以下はService層に切り分けた時のアフター例です。
このように切り分けると、各々の処理の責任を分けることができ、
Controllerをよりシンプルにすることができます。

ArticleController
    public function store(StoreArticleRequest $request, CreateArticleService $service)
    {
        try {
	  $article = $service->handle($request);
          return response()->json(['success' => '登録に成功しました。']);
        } catch (Exception $e) {
          return response()->json(['error' => '登録に失敗しました。'], 400);
        }
    }
App\Services\CreateArticleService.php
<?php
namespace App\Services;
// 一部省略
class CreateArticleService
{
    public function handle(Request $request)
    {
      try {
	  $article = new Article();
          $article->fill([
              'title' => $request->title,
              'body' => $request->body,
              'start_date' => $request->start_date,
          ])->save();
	  return $article;
      } catch (Exception $e) {
          throw new Exception('登録に失敗しました。');
      }
    }
}

今回の「ビフォー」では、既にLaravelの標準機能であるFormRequest
バリデーションを行なっており、適切に切り分けられています。
また、Middlewareによるリクエストの前後処理、Policiesによる認可処理など、
標準機能を使用することで、Controllerの肥大化をより防止することができます。

補足

それぞれのModelごとにServiceクラスを作成する、または各Modelに直接登録処理などを
記述する方法は、あまり効率的ではない場合が多いです。

これは、主に以下の理由のためです。

  • 一つの処理で複数のモデルを触る必要がある場合に、どのモデルに処理を記述すればいいかが不明確でわからない
  • メソッドがたくさん追加され、可読性が悪くなる
  • ソースコードを追う場合にどこを見ればいいかわかりづらい

そのため、ユーザー側から見たときの処理の内容(記事の登録、記事の更新など)を基準に
Serviceクラスを作成し処理を整理すると、
どこに何の処理を記述すればいいかが明確で、コードも追いやすくなります。

これにより、基本的にはユーザー中心の基準に従うと、
コードの整理・管理がしやすくなります。

さいごに

今回の例はシンプルなものでしたが、Controllerが肥大化しやすいのは
よくあることだと思います。

まだ改善できることは多くあると思いますが、
責任が明確に分かれることで可読性が高く、テストも行いやすくなるので、
積極的にService層への切り分けを行っていきたいと思います。

Discussion