NAKKA-Kの技術ブログ

技術に関する知見や考え方などを投稿します。

Laravelを使ったAPI開発でController内のバリデーションをFormRequestに抽出して幸せになろう

私の開発しているプロジェクトでは

  • Laravel 5.7
  • React/Redux

を使って開発しています。

バックエンドのLaravelはAPIを実装しています。 LaravelでAPIを実装すると通常のwebで作るより処理が煩雑になる気がします。 その上、いろんな処理をControllerにまとめて書いてしまった経験はそれなりにあるのではないでしょうか?

今回はリクエストのバリデーション処理をControllerに書いている状態から、LaravelのFormRequestを使って責務を抽出した手順をご紹介します。

(軽く調べた感じLaravel5.5以降でないと違う書き方になるようです)

Policyについての記事もあります。

nakka-k.hatenablog.com

FormRequestとは

FormRequestとはリクエストのバリデーションルールを定義するLaravelの仕組みです。 FormRequestをコントローラーメソッドにDIしてあげると、バリデーションが通った時だけコントローラー内の処理が走ります。

これによりコントローラーはリクエストに不正な値が入っている可能性を除外できるため、自分の処理だけに専念できます。

Controllerにバリデーションを書いていた時

APIでバリデーションを書くと割と面倒でした。 例えばこれはログインする時に書きそうなバリデーションです。

(after内の処理にいい例がなかったので、ちょっと特殊な例になってしまいましたがあまり気にしないでください)

    public function login(Request $request) {
        $validator = \Validator::make($request->all(), [
            'email'    => 'required|string|max:64',
            'password' => 'required|string|max:64',
        ]);

        $validator->after(function ($validator) {
            $user = \App\User::where('name', $this->input('name'))->first();

            // 同名ユーザが存在して、ログイン中ユーザと同ユーザであればエラー
            if(null !== $user && $this->user()->id === $user->id){
                $validator->errors()->add('name', '既にログインしています。');
            }
        });

        if ($validator->fails()) {
            return response()->json([
                'status' => 400,
                'errors' => $validator->errors()
            ], 400);
        }

        // request is valid ......
    }

ほぼ全てのPOST系コントローラーにこれと同等な処理が書かれていました。 一律でエラー時のレスポンス構造を設定する方法などもありそうですが、開発初期だったためこの書き方で統一していました。

コントローラーにバリデーションを書いてしまうと、常にコントローラーがバリデーションについて責任を持たなければなりませんし、約10行近い処理がメソッドを占領してしまいます。

これは非常によくありません。

そこでバリデーションをFormRequestに置き換えることになりました。

FormRequestを作成する

まずArtisanCLIコマンドのmake:requestを使用して、FormRequestのファイルを作成しましょう。 今回はログイン時のバリデーションをしたいのでLoginRequestとしましょう。

php artisan make:request LoginRequest

app/Http/Requests以下にLoginRequest.phpファイルが作成されます。 このファイルに先ほどコントローラーに書いていたバリデーションを移植します。

まず作成されたファイルはこのようになっています。

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

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

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            //
        ];
    }
}

ここで一番気をつけるべきなのはpublic function authorize()です。 デフォルトでfalseを返すようになっていますが、特にアクセス権限などを求めないリクエストであればtrueを返すように書き換えてください。

バリデーションルールを定義

ではバリデーションを移植していきましょう。 public funciton rules()がありますね。 ここにバリデーションのルールを書きます。

    public function rules()
    {
        return [
            'email'    => 'required|string|max:64',
            'password' => 'required|string|max:64',
        ];
    }

ルールはこれでOKです。

ですがこのままではバリデーションエラーが発生した時にHTMLを返してしまいますので、JSONを返すようにメソッドをオーバーライドしていきます。

レスポンスを定義

function failedValidation(Validator $validator)を定義します。 ここではバリデーションを走らせて失敗した後のvalidatorが引数に渡されます。 ですから$validator->errors()などを駆使してレスポンスのjsonを生成しましょう。 そして戻り値にはHttpResponseException(response)を返します。

    protected function failedValidation(Validator $validator) {
        $res = response()->json([
            'status' => 400,
            'errors' => $validator->errors(),
        ], 400);
        throw new HttpResponseException($res);
    }

afterなどの追加処理を定義

$validator->after()も移植しましょう。 function withValidator(Validator $validator)を定義します。 ここではバリデーションが実行される前に呼び出されるので、validatorを使って何か事前処理を書きたい場合に使います。

    public function withValidator(Validator $validator) {
        $validator->after(function ($validator) {
            $user = \App\User::where('name', $this->input('name'))->first();

            // 同名ユーザが存在して、ログイン中ユーザと同ユーザであればエラー
            if(null !== $user && $this->user()->id === $user->id){
                $validator->errors()->add('name', '既にログインしています。');
            }
        });
    }

完成したFormRequest

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Http\Exceptions\HttpResponseException;


class LoginRequest extends FormRequest
{
    public function authorize()
    {
        return true;
    }

    public function rules()
    {
        return [
            'email'    => 'required|string|max:64',
            'password' => 'required|string|max:64',
        ];
    }

    protected function failedValidation(Validator $validator) {
        $res = response()->json([
            'status' => 400,
            'errors' => $validator->errors(),
        ], 400);
        throw new HttpResponseException($res);
    }

    public function withValidator(Validator $validator) {
        $validator->after(function ($validator) {
            $user = \App\User::where('name', $this->input('name'))->first();

            // 同名ユーザが存在して、ログイン中ユーザと同ユーザであればエラー
            if(null !== $user && $this->user()->id === $user->id){
                $validator->errors()->add('name', '既にログインしています。');
            }
        });
    }
}

FormRequestを組み込む

作成したLoginRequestをコントローラーに組み込んでバリデーションに使用してもらいましょう。 コントローラーメソッドの引数にあったRequestLoginRequestに変更するだけです。

use App\Http\Requests\LoginRequest; // インポートを忘れずに


    public function login(LoginRequest $request) {
        // request is valid ......
    }

なんということでしょう! あの煩雑で責務の混ざり合った見るたびに嫌気のするコントローラーが、こんなにも開放的で責務の分離された美しいメソッドに生まれ変わりました! これには開発者さんも幸福感を隠しきれません。

まとめ

Controller内のバリデーションをFormRequestに抽出するだけ多くのメリットをもたらします。 何か事情がなければ最初からFormRequestを使った方がいいですね。

プロジェクトで途中からFormRequestを使うようにした場合やLaravel初学者が多い場合は、一律でFormRequestを使うように周知しておくのをお勧めします。 もし抽出後の綺麗なコントローラーに誰かがバリデーションをそのまま追加してしまった日には

なんということをしてくれたのでしょう! あの開放的で責務の分離された美しいメソッドが、煩雑で責務の混ざり合った見るたびに嫌気のするコントローラーに逆戻りです! これには開発者さんも般若の顔を隠しきれません。

と叫ぶことになってしまいますからお気をつけください。

参考