📦

MVCだけじゃない!LaravelにおけるService層とRepository層の活用法

2024/12/17に公開

はじめに

はじめまして!株式会社TeraDox入社4ヶ月目の吉川です。未経験からWebエンジニアへとジョブチェンジをし、この4ヶ月間で多くのことを学んできました。今回はその中から「Service層」と「Repository層」について、アウトプットさせていただきます!
未経験時代、Laravelを使ったアプリ開発に取り組みましたが、その際はMVCモデルの基本であるModel、View、Controllerのみを使い、簡単なアプリを開発していました。
しかし、入社後、弊社サービスである「My振袖」のディレクトリ構成を見た際、見たことも聞いたこともない「Service」と「Repository」というディレクトリがあり、最初は困惑しました。
入社から4ヶ月弱、My振袖の開発に携わる中で、「Service層」と「Repository層」の役割や使い方が徐々に理解できてきました。本記事では、駆け出しエンジニアの視点から、それらの役割と活用方法についてお伝えします。

対象者

  • MVCモデルの概要を理解している方
  • 企業やチームで開発するような中・大規模アプリケーション開発に関わっている、または興味のある方

ざっくり説明

  • Service層
    ビジネスロジックを管理する層。アプリケーションのビジネスロジックを実装し、上位のコントローラ層を簡潔に保つ役割を担います。
  • Repository層
    データアクセスの抽象化を担う層。DBや外部データソースへのCRUD操作を提供し、データアクセスに関する詳細を隠蔽します。

前提

MVCモデルに代表される層構造の本質的な意義は「責務の分離」です。
どのディレクトリやファイルにどの処理が記述されているのかが明確になるため、開発や運用がしやすくなります。
Service層やRepository層もこの「責務の分離」をさらに細かく行うために存在します。他の層との責務の違いについて、これから詳しく解説します。

MVCモデル + Sevice層 + Repository層の全体フロー

Service層とRepository層を含んだMVCモデルの流れは以下の通りです。Controller→Service→Repository→Modelというフローで処理を行います。必ず一直線に処理を行う必要があります。例えば、Controller層からRepository層を直接呼び出したり、Service層からModel層を直接呼び出したりしてはいけないということです。

Service層の役割と記述内容

Service層の役割としては、以下4つが挙げられます。

  1. Fat Controllerを防ぐ
  2. テストコードを書きやすくする
  3. ビジネスロジックを記述する
  4. 例外処理やトランザクション処理を記述する

1. Fat Controllerを防ぐ

Controllerが膨大な処理を抱える「Fat Controller」を防ぎます。Service層が存在することで、Controllerは、リクエストを受け取り、必要なServiceを呼び出してレスポンスを返すという役割のみに専念できます。

2. テストコードを書きやすくする

Fat Controllerを防ぐに繋がることですが、1つのメソッドで、あまりにも処理が大きすぎると大量の条件分岐が使われ、様々なパターンを想定してテストを行う必要が出てきます。これによりテストの複雑さが増してしまいます。メソッドが使用されているとMockやStabといったテストタブルの記法を使って実装することができ、より簡潔で読みやすく、書きやすいUT実装をすることができます。

3. ビジネスロジックを記述する

データの整形やバリデーション、条件分岐、外部API連携など、アプリケーションのビジネスロジックを記述します。

4. 例外処理やトランザクション処理を記述する

DB操作や外部サービスとやり取りを行う際、トランザクション処理や例外処理が必要になることがあります。Service層にこれらの処理を集約することで、処理の一貫性を保ちながら、コードが整理されます。Controllerに記述することもあるようですが、大半はService層に記述します。

Repository層の役割と記述内容

Repository層の役割としては、以下4つが挙げられます。

  1. コードの見通しが良くなる
  2. テストコードを書きやすくなる
  3. DBの変更に強くなる
  4. CRUD操作を記述する

上記でRepository層を説明する際、「隠蔽」という言葉を使用していますが、データの取得方法や内部実装の詳細を意識せずに、Repository層を使えるようにすることを意味しています。具体的には、Service層の実装者は「どのデータが取得されるのか」だけを知ればよく、「どうやってデータを取得しているか」を知らなくても済む、という状態を指します。「隠蔽」することによるメリットに紐づくRepository層の役割について述べます。

1. コードの見通しが良くなる

DB操作のロジックをRepository層に切り出すことで、Service層のコードがスッキリします。

2. テストコードを書きやすくなる

Service層の役割と同様に、メソッドが使用されているとMockやStabといったテストタブルの記法を使って実装することができ、より簡潔で読みやすく、書きやすいUT実装をすることができます。

3. DBの変更に強くなる

例えば、SQLを使っていたものをNoSQLに変更する場合、Repository層の実装を変えるだけで済みます(もちろん実際には頻繁に起こることではありませんが、対応が楽になります)。

4. CRUD操作を記述する

Repository層は、DBに対する基本的な操作(保存=Create、取得=Read、更新=Update、削除=Delete)をまとめて記述する場所です。注意点として、データの整形・加工はRepository層では行わず、Service層で行います。

具体例

ユーザー管理システムにおけるユーザー登録のフローを例として、各層の実装方法について説明します。

Controller層

Controller層では、Service層のregisterUserメソッドを呼び出しています。後述したUserServiceを見ていただくとわかると思いますが、複数のビジネスロジックが記述されており、これらの記述がControllerに記載されていないことで、RequestとResponseのみを扱うよう整理されており、Fat Controllerを防いでいることが分かります。

UserController.php
<?php

namespace App\Http\Controllers;

use App\Services\UserService;
use Illuminate\Http\Request;

class UserController extends Controller
{
    protected $userService;

    public function __construct(UserService $userService)
    {
        $this->userService = $userService;
    }

    public function register(Request $request)
    {
        $validatedData = $request->validate([
            'name' => 'required|string|max:255',
            'email' => 'required|email|unique:users',
            'password' => 'required|min:8'
        ]);

        // Service層のregisterUserメソッドを呼び出す
        $user = $this->userService->registerUser($validatedData);
        return route('users.complete', ['user' => $user]);
    }
}

Service層

Service層では、Repository層のcreateメソッドを呼び出しています。Service層ではビジネスロジックを扱うと前述した通り、ユーザー作成処理では以下のビジネスロジックが存在しています。また、Service層の役割の1つである例外処理も記述されています。

  • メールアドレスの重複チェック
  • パスワードのハッシュ化
  • ユーザー作成
  • ウェルカムメールの送信
UserService.php
<?php

namespace App\Services;

use App\Repositories\UserRepository;
use Illuminate\Support\Facades\Hash;
use App\Exceptions\UserAlreadyExistsException;

class UserService
{
    protected $userRepository;

    public function __construct(UserRepository $userRepository)
    {
        $this->userRepository = $userRepository;
    }

    public function registerUser(array $data)
    {
        try {
            return DB::transaction(function () use ($data) {
                // メールアドレスの重複チェック
                $existingUser = $this->userRepository->findByEmail($data['email']);
                if ($existingUser) {
                    throw new UserRegistrationException('このメールアドレスは既に登録されています。');
                }

                // パスワードのハッシュ化
                $data['password'] = Hash::make($data['password']);

                // ユーザー作成
                $user = $this->userRepository->create($data);

                // ウェルカムメールの送信
                $this->sendWelcomeEmail($user);

                return $user;
            });
        // 例外処理
        } catch (UserRegistrationException $e) {
            Log::warning('User registration failed: ' . $e->getMessage());
            throw $e;
        } catch (\Exception $e) {
            Log::error('Unexpected error during user registration: ' . $e->getMessage());
            throw new UserRegistrationException('ユーザー作成中に予期せぬエラーが発生しました。', 0, $e);
        }
    }
}

Repository層

Repository層では、Eloquentを使用してDBへユーザー情報を登録しています。

UserRepository.php
<?php

namespace App\Repositories;

use App\Models\User;

class UserRepository
{
    public function create(array $data)
    {
        return User::create($data);
    }
}

Model層

Model層では、データ構造の定義やリレーションの定義、アクセサとミューテータの実装を行います。

User.php
<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable;

    protected $fillable = [
        'name',
        'email',
        'password',
        'role',
        'status',
    ];
    // 以下省略

まとめ

今回は、MVCモデルにService層とRepository層を加えた設計について説明しました。責任を分離することでコードの管理がしやすくなり保守性が高くなることを実感しています。中・大規模アプリケーション開発に携わる際は、ぜひ取り入れてみてください!

ちなみに余談ですが、Laravelの公式ドキュメントにはService層やRepository層といった概念は登場しないんですよね。個人でLaravelを学習しているときは、気づかないのも無理はないと思いました。私自身も最初は「なんだこれ?」となりましたが、使ってみるとその便利さに納得です!

参考文献

TeraDoxテックブログ

Discussion