【Laravel】「単一責任の原則」について|ベストプラクティスを読んでみた

Laravel PHP

【Laravel】「単一責任の原則」について|ベストプラクティスを読んでみた

なやむくん
ベストプラクティスの「単一責任の原則」について詳しく知りたい…。
なやむさん
どのような記述が「単一責任の原則」に当てはまるのか教えてほしい…。

などの疑問や悩みを解決してまいります。

単一責任の原則とは

コントローラにおける「単一責任の原則(Single Responsibility Principle, SRP)」は、コントローラが1つの役割に限定されるべきであるという考え方です。

なやむくん
他の記事を見ると、「同じように1つの責任だけを持つべき」だったり、「1つの役割に限定されるべき」などちょっと初心者にはわかりづらいです…。

具体的には、コントローラは「リクエストを受け取り、必要なビジネスロジックに基づいてレスポンスを返す」という役割を担うべきであり、ビジネスロジックやデータ操作を直接コントローラ内で行わないようにするということです。

なぜコントローラの単一責任が重要か

コントローラには、様々なリクエストを受けてはレスポンスを返すといった受付係のような役割を担っています。

例えば、CRUD(データベースを操作する基本的な4つの機能)をコントローラに記述するのが一般的ですが、その機能1つとってもバリデーションデータの更新など、複数のビジネスロジックを記述する必要があります。

そのすべての処理責任をコントローラに持たせていては、以下のように様々な問題に直面します。

コントローラの複数責任による問題点

  • 可読性の低下
    ビジネスロジックやデータ操作の処理が混雑すると、可読性が失われ、処理の理解に苦しんでしまう問題に直面します。

  • 再利用性の低下
    コントローラにビジネスロジックを直接書くと、同じ処理を複数の場所で再利用するのが難しくなります。

  • 保守性の低下
    コードが一箇所に集約されていないため、修正や変更時に他の箇所への影響を予測しにくくなり、意図しないバグや不具合に悩まされます。

下のコードは、CRUDのUPDATE(更新)に関する記述ですが、更新するユーザーデータを取得する処理リクエストデータのバリデーションチェックの処理リクエストデータを元に取得したユーザーデータを更新する処理と様々な責任を背負っていることが分かります。

これが、他にも複数あると考えるとかなり長ったらしいコードになってしまいます。

class UserController extends Controller
{
    public function update(Request $request, $id)
    {
        try {
            // ユーザーをデータベースから取得
            $user = User::findOrFail($id); // findではなくfindOrFailを使って、見つからない場合に例外を投げる

            // リクエストからデータをバリデーション
            $validatedData = $request->validate([
                'name' => 'required|string|max:255',
                'email' => 'required|email|max:255',
            ]);

            // ユーザー情報を更新
            $user->name = $validatedData['name'];
            $user->email = $validatedData['email'];
            $user->save();

            // 更新が成功したら、ユーザー一覧ページにリダイレクト
            return redirect()->route('users.index')->with('success', '更新処理が成功しました。')

        } catch (\Exception $e) {
            // その他の例外が発生した場合の処理
            return redirect()->back()->with('error', '更新処理が失敗しました。');
        }
    }
}

コントローラが「単一の責任だけを持つべき」というのは、コントローラにはリクエストを受け取り、処理結果をレスポンスするという責任だけ持たせてあげるということになります。

単一責任の原則に基づくコントローラの設計

それでは、悪い例で紹介したコードを「単一責任の原則」に基づいてリファクタリングしてみます。

大きく分けて以下2つのクラスを使って処理を分割していきます。

2つのクラス

  • フォームリクエストクラス
  • サービスクラス

バリデーションをフォームリクエストクラスに分割

Laravelのフォームリクエストクラス(FormRequest)は、リクエストデータがコントローラに渡される前にバリデーションと認可を行うことができる仕組みで、リクエストに対して行うバリデーションと認可のロジックをコントローラから分離するために使用されます。

バリデーションを担当するフォームリクエストクラスを作成するため、以下のコマンドを実行します。

php artisan make:request UpdateUserRequest

このコマンドを実行すると、app/Http/Requestsディレクトリに新しいクラスファイルが作成されます。

ファイルが作成できたら、ファイルの内容を以下のように編集します。

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class UpdateUserRequest extends FormRequest
{
    /**
     * このリクエストが認可されているかどうかを決定
     */
    public function authorize()
    {
        // trueを返すと認可が許可され、falseだと拒否されます
        // 認可ロジックをここで定義できます
        return true;
    }

    /**
     * バリデーションルールを定義
     */
    public function rules()
    {
        return [
            'name' => ['required', 'string', 'max:255'],
            'email' => ['required', 'email', 'max:255'],
        ];
    }
}

rules()バリデーションルールを定義する部分です。

このrule()の返り値として配列を返し、それぞれの入力項目(フィールド)に適用するルールを設定します。

ビジネスロジックをサービスクラスに分割

LaravelのサービスクラスService)は、ビジネスロジックをコントローラから分離して扱いやすくするクラスです。

サービスクラスを使うことで複雑な処理をモジュール化し、保守性や再利用性を向上させることができます。

コントローラはリクエストの処理やレスポンスの生成に集中し、ビジネスロジックはサービスクラスにお任せすることで、ファイルごとの役割が明確になります。

以下コマンドを実行して、ディレクトリを作成します。

mkdir app/Services
touch app/Services/UserService.php

ファイルが作成できたら、以下のようにコードを記述します。

namespace App\Services;

use App\Models\User;
use Illuminate\Database\Eloquent\ModelNotFoundException;

class UserService
{
    /**
     * ユーザー情報を更新する
     *
     * @param int $id
     * @param array $data
     * @return void
     * @throws ModelNotFoundException
     */
    public function updateUser(int $id, array $data)
    {
        // ユーザーが存在しない場合、ModelNotFoundExceptionを投げる
        $user = User::findOrFail($id);

        // ユーザー情報を更新
        $user->update($data);
    }
}

updateUser()を作成し、その中にコントローラに記述していたビジネスロジックをまとめています。

コントローラのリファクタリング

コードを役割ごとに分割したら、最後にコントローラのコードをリファクタリングします。

namespace App\Http\Controllers;

use App\Http\Requests\UpdateUserRequest; // 追加
use App\Services\UserService; // 追加
use Illuminate\Http\Request;

class UserController extends Controller
{
    public function __construct(private UserService $userService)
    {}
    public function update(UpdateUserRequest $request, $id)
    {
        try {
            $this->userService->updateUser($id, $request->validated());
            return redirect()->route('users.index')->with('success', '更新処理が成功しました。');

        } catch (\Exception $e) {
            return redirect()->back()->with('error', '更新処理が失敗しました。');
        }
    }
}

だいぶすっきりしました。

このようにフォームリクエストではバリデーションを、サービスクラスではビジネスロジックをコントローラから分離することで、コントローラーの役割をよりシンプルにすることができたと思います。

まとめ

「単一責任の原則」を意識することにより、コントローラは1つの責任(今回で言うとリクエスト→レスポンスの受付業務)に集中させ、ビジネスロジックやバリデーションは専用のサービスクラスやフォームリクエストに分けることで、コードをより読みやすく、保守しやすくすることができます。

「単一責任の原則」は、今回のLaravelだけでなく他のプログラミングにおいても当てはまる考え方なので、ぜひコードを書く上で意識してみてください。

参考記事

Laravelのベストプラクティスに関する情報は、以下がとても参考になりました。

laravel-best-practices/japanese.md at master · alexeymezenin/laravel-best-practices

  • この記事を書いた人
  • 最新記事

みつた

完全未経験&異業種から30歳の年でIT企業に転職。

Web系開発言語が好き。
どちらかというとバックエンドよりもフロントエンドが好き(現時点では…)

最近はサウナと観葉植物にハマっている。
野球が好きで一応投手。

-Laravel, PHP