複雑なバリデーションルールを外部クラスにて実装する

..

フォームの仕様によっては、Laravelで使用できる個々のバリデーションルールだけでは対応しきれないことがあります。

込み入ったバリデーションの実装はRuleオブジェクトで行うことが多いようにも思いますが、この記事では実務で使っている「外部クラスにリクエストを渡してそこでバリデーションをかける」という方法を記載します。

ドキュメントの参考箇所 https://readouble.com/laravel/8.x/ja/validation.html#adding-after-hooks-to-form-requests

リクエストクラス内の記述

勤め先では、具体的な処理はControllerクラスに対応したServiceクラスを宣言し、そこに記述しています。
なのでそこにバリデーション処理を記述して、Requestクラスから呼び出します。

// 処理を行うクラスをuse
use HogeService;

class XXXXRequest extends FormRequest
{
    // 諸々のバリデーションルール
    
    
    
    /**
    * バリデータインスタンスの設定
    *
    * @param  \Illuminate\Validation\Validator  $validator
    * @return void
    */
    public function withValidator($validator)
    {
        $validator->after(function ($validator) {
            // 処理クラスへ入力値を渡す
            $errors = XXXXService::メソッド名($this->all());
            foreach ((array)$errors as $error) {
                // エラーメッセージを生成
                $validator->errors()->add($error['key'], $error['message']);
            }
        });
    }
}

普通のバリデーションをかけたあと、処理を行っている外部クラスに入力値を渡してバリデーションを行います。

以下の例は、認証されていない状態で開くことが想定されたページにて、入力されたログインIDとパスワードが正しいかどうかを判定するための処理です。

class XXXXService {
    
    
    
    /**
     * バリデーション
     *
     * @param [type] $data
     * @return void
     */
    public function メソッド名($data) {
        // $errorsという配列を作る
        $errors = array();

        $data_login_id = $data['login_id'];
        $data_password = $data['password'];

        // 検索クエリを生成
        $login_id_query = User::where('login_id', $data_login_id);
        // login_idカラムを検索
        $email = $login_id_query->exists();
        // login_idと同じレコード内のパスワードと一致するか検索
        $password = $login_id_query->where('password', $data_password)->exists();

        // 以下、各変数がfalseの場合はエラーメッセージを返す
        if (!$email) {
            $errors[] = array(
                'key' => 'login_id',
                'message' =>  '登録されていないユーザーIDです。',
            );
        } 
        if (!$password)) {
            $errors[] = array(
                'key' => 'password',
                'message' => 'パスワードが違います。',
            );
        }

        return $errors;
    }
}

この実装の場合は、何かの理由でServiceクラスへ値が渡らなかった際にエラーが発生することがあります。
「バリデーションエラーをcount関数で数えた際に戻り値が0じゃなければreturn」といった処理を書くことで回避することができます。

    /**
    * バリデータインスタンスの設定
    *
    * @param  \Illuminate\Validation\Validator  $validator
    * @return void
    */
    public function withValidator($validator)
    {
        $validator->after(function ($validator) {
            // 他のバリデーションルールでエラーが出たら何もせず返す
            if (count($this->validator->errors()) != 0) {
                return;
            }
            $errors = XXXXService::メソッド名($this->all());
            foreach ((array)$errors as $error) {
                $validator->errors()->add($error['key'], $error['message']);
            }
        });
    }

今の現場では、DBを参照する処理は全てサービスクラスに記述する方針で開発しています。なのでこういった方法を取っているのだと思いますが、果たして最適解なのかはよくわかりません。(個人的には、普通に他のルールと並記できたほうが見通しがいいような気がする)

今の現場ではプロジェクト内になるべくファイルを増やしたくないようなのでこうなってます。

Ruleオブジェクトを使ったバリデーションは試したことがないので、個人開発の方ではそちらを試してみようと思っています。