[Laravel]Fat Controllerを避ける策について ~備忘録~

## はじめに
この記事はプログラミング初学者による備忘録用の記事であり、また、少しでも他の初学者のお役に立てればと思い書いています。

今回は、プログラミング初学者である私が今まで学んできたFat Controllerを避ける策について、備忘録としてまとめておきたいと思います。

間違いなどがございましたら、ご指摘のほどよろしくお願い致します。



## Controllerクラスの責務を明確にする
LaravelにはMVCパターンが採用されており、Viewはページの表示やユーザーからの入力を受け付け、Controllerはリクエストを受け取ってViewへの出力指示・Modelへの処理指示を行い、ModelはDBに関する処理を実行します。

全てのアクションをControllerクラスに記載してしまうと、コード量が増え修正等が発生した際に処理が把握し切れなくなり、エラー発生のリスクが高まってしまいます。
従って、Controllerクラスでは、``リクエストを受け取ってViewへの出力指示・Modelへの処理指示を行うことだけ``を徹底してください。

## Fat Controllerの解決策

上記の前提条件に追加で以下を徹底すると、Controllerクラスが肥大化せず、綺麗なControllerクラスを維持することができると思います。

1. バリデーションはRequestクラスに定義する
2. リクエストデータを処理するコードは、Requestクラスに記述する
3. Eloquentのマスアサインメント機能を利用してデータを扱う
4. DBに関連する全てのロジックをModelクラス又はRepositoryクラスに入れる

ほとんどRequestクラスに関することですが、予めご了承ください。

## 1.バリデーションはRequestクラスに定義する
バリデーションはControllerクラスではなく、``Requestクラス``で定義します。
``Requestクラス``で定義することで、独自のバリデーションをカプセル化することができます。

>より複雑なバリデーションシナリオの場合は、「フォームリクエスト」を作成することをお勧めします。フォームリクエストは、独自のバリデーションおよび認可ロジックをカプセル化するカスタムリクエストクラスです。
``引用:``[Laravel8.x バリデーション](https://readouble.com/laravel/8.x/ja/validation.html#form-request-validation)

### Requestクラスにバリデーションルールを定義する手順
#### 1.make:request Artisan CLIコマンドでRequestクラスを作成
下記のコマンドを実行してRequestクラスを作成します。

```.php
php artisan make:request ArticleRequest
```

#### 2.作成したRequestクラスのrulesメソッドにバリデーションを定義する
下記のように、Artisanコマンドを実行後、自動的に``authorize``と``rules``の2つのメソッドが作成されます。

``authorize``メソッドは、現在認証されているユーザーがリクエストによって表されるアクションを実行できるかどうかを判断し、``rules``メソッドはリクエスト中のデータを検証するバリデーションルールを返します。

```.php
class ArticleRequest extends Request
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize(): bool
    {
        return true;
    }

    /**
     * リクエストに適用するバリデーションルールを取得
     *
     * @return array
     */
    public function rules()
    {
        return [
            'title' => 'required|unique:posts|max:255',
            'body' => 'required',
        ];
    }
}
```

#### 3.バリデーションを定義後、Controllerクラスのメソッドでタイプヒント指定する
バリデーションルールを実行する為に、Controllerクラスのメソッドで先ほど作成したRequestクラスをタイプヒント指定します。

下記のように、タイプヒント指定することで、フォームリクエストはコントローラメソッドが呼び出される前にバリデーションを実行してくれます。
結果、Controllerクラスでバリデーションに関するロジックを記述する必要がなくなり、MVCパターンでのController本来の役割だけを果たすようになります。

```.php
public function store(ArticleRequest $request)
{
    // 送信されたリクエストは正しい

    // バリデーション済みデータの取得
    $validated = $request->validated();
}
```

## 2. リクエストデータを処理するコードは、Requestクラスに記述する
Requestの値を使った処理を行うコードは、Requestクラスへ移動させましょう。

```Request.php
# 略
    public function rules(): array
    {
        return [
            'body' => 'required | max:200',
            'tags' => 'nullable | json | regex:/^(?!.*\s).+$/u|regex:/^(?!.*\/).*$/u',
        ];
    }

    public function passedValidation(): void
    {
        $this->tags = collect(json_decode($this->tags))
            ->slice(0, 5)
            ->map(function ($requestTag) {
                return $requestTag->text;
            });
    }
```

バリデーションルールはRequestクラスに書いてあると思うので、入力パラメータに変更があった際も、Requestクラスのみを変更すればいい状態にしておくと、変更漏れによるエラーが起こりにくくなると思います。

## 3.Eloquentのマスアサインメント機能を利用してデータを扱う
Laravelでは、デフォルトでマスアサインメント機能が有効になっています。
Eloquentのマスアサインメント機能を使うと、1行で簡潔にデータを扱うことができます。

<br>

### ・マスアサインメント機能を利用しないコードと利用したコードの比較
下記のように、マスアサインメント機能を利用しない場合はstoreメソッド等で複数行のコードを記述する必要がありますが、マスアサインメント機能を利用した場合は1行で完結にデータを操作することが可能です。

```.php
# マスアサインメント機能を利用しない場合 Bad

$article = new Article;
$article->title = $request->title;
$article->content = $request->content;
$article->verified = $request->verified;
// Add category to article
$article->category_id = $category->id;
$article->save();

# マスアサインメント機能を利用した場合 Good

$category->article()->create($request->validated());
```

:::note warn
マスアサインメントとは
意図せぬリクエストによって悪意のあるデータが挿入されてしまう脆弱性を``マスアサインメント``といいます。
:::

### ・マスアサインメント機能を利用する際の設定
マスアサインメント機能を利用する際は、下記のような設定を行う必要があります。

1. FormRequestでバリデーション等を定義する
1. Modelクラスで``fillable``を指定する
1. Requestクラスを利用する為に、Controllerクラスのメソッドの引数でタイプヒント指定し、``$request->validated()``を使ってバリデーション後のリクエストデータを配列で取得する

#### 1.Requestクラスでバリデーション等を定義する
バリデーション処理は、コントローラからRequestクラスに移動させます。

下記のように、Requestクラスにて``rulesメソッド``を定義してください。

```UserRequest.php
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;

class UserRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    public function rules()
    {
            return [
                'name' => 'required | string | min:1 | max:25',
                'email' => 'required | string | email | max:255 |' . Rule::unique('users')->ignore(Auth::id()),
                'introduction' => 'string | max:255 | nullable',
                'image' => 'file | mimes:jpeg,png,jpg,bmb | max:2048 | nullable',
            ];
    }
```

#### 2.Modelクラスで``fillable``を設定する
マスアサインメント機能を使用する際は、下記のようにModelクラスで``fillable``を使い、**データの変更を許可するカラムを指定する**必要があります。

```Model.php
# 略
class User extends Authenticatable
{
    use Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name', 'email', 'introduction', 'image',
    ];
}
```

#### 3.定義したRequestクラスを利用する為に、Controllerクラスのメソッドの引数でタイプヒント指定します
Controllerクラスのメソッドの引数でタイプヒント指定した後に、``$request->validated();``を使うことで、Requestクラスのインスタンスを使用したバリデーション済みのリクエストデータを配列で取得することができます。

``Illuminate\Http\Requestオブジェクト``によって提供されるvalidateメソッドは、バリデーションに失敗すると``Illuminate\Validation\ValidationException例外``が投げられ、適切なエラーレスポンスが自動的にユーザーに返送されます。

```.php
# 定義したRequestクラスを利用する際はメソッドの引数でタイプヒントします(メソッドインジェクション)

public function update(UserRequest $request)
{    
    # バリデーション済みのリクエストデータを配列で取得
    $validated = $request->validated();

    # フォームリクエストやバリデータのインスタンスに対してsafeメソッドを呼び出すこともできます。
    # このオブジェクトはonly、except、allメソッドを用意しており、バリデーション済みデータのサブセットや配列全体を取得できます。
    $validated = $request->safe()->only(['name', 'email']);

    $validated = $request->safe()->except(['name', 'email']);
}
```

## 4. DBに関連する全てのロジックをModelクラス又はRepositoryクラスに入れる
FatControllerになる主な原因として、Modelクラスが機能していないということが考えられます。

Modelクラスに書くべき処理がControllerに書いてあると、結果的に可読性が悪くなったり、1つのコード変更が他のコードに影響を及ぼしたりと、エラーが発生してしまう可能性が高くなります。

従って、DBに関する全てのロジックはControllerクラスから排除し、別のクラスに記述することをオススメします。
[Laravelのベストプラクティス](https://github.com/alexeymezenin/laravel-best-practices/blob/master/japanese.md#%E3%83%90%E3%83%AA%E3%83%87%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3)にも、同じようなことが書かれているので、信憑性は高いと思います。

```.php
# 略
class ArticleController extends Controller
{
# Bad 
# DBに関連するロジックがControllerクラスにあるケース
    public function index(Request $request, Article $article)
    {
        return $article->with(['user', 'tags'])
            ->latest()
            ->paginate(10);
    }
}

# Good 
# DBに関連するロジックがModelクラスにあるケース
class Article extends Model
{
    public function getArticleIndex(Article $article, $request)
    {
        return $article->with(['user', 'tags'])
            ->latest()
            ->paginate(10);
    }
}
```

## おわりに
以上で、Fat Controllerを避けることができると思います。
独学で身につけた知識なので、間違っている可能性の方が高いと思います。

もし間違いなどがございましたら、ご指摘のほどよろしくお願い致します。

## 参考文献
https://github.com/alexeymezenin/laravel-best-practices/blob/master/japanese.md

https://readouble.com/laravel/8.x/ja/validation.html