LaravelのFormRequestをちゃんと理解する

FormRequestをちゃんと理解する バリデーション
※当サイトはアフィリエイト広告を掲載しています。

FormRequestクラスを利用しての認可・バリデーションは、Webアプリケーションで非常によく使われます。私の場合、特に後者は使わないケースが無いと言って良いくらい使います。

しかしながら、毎回型にはまった利用で「何となく」で使っていませんか。

利用頻度が高いRormRequestですから、きちんと理解しておきたい。私も含めてそんな方も多いのではないでしょうか。

自身の勉強もかねて、FormRequestを徹底的にまとめましたので参考にしていただければ幸いです。

FormRequestとは

Laravelを始めたばかりで、そもそものFormRequestについての認識が曖昧な方向けに書いています。すでにご存じの方は次項に飛ばしてください。

概要

FormRequestは、ユーザーがフォーム等から送信してきた項目に対し、

  • 認可(オーソライゼイション)
  • 検証(バリデーション)

をするために使います。

本ページでは検証という言葉は使わず、以降はバリデーションとします。バリデーションの方が普通によく使うように思うからです。

Laravelが用意するFormRequestクラスには、それらを行うための機能がすでに備わっています。

私たちはFormRequestクラスを継承したクラスを利用することで、簡単に認可・バリデーションを行うことが可能です。

Requestクラスを継承している

FormRequest クラスは Illuminate\Http\Request クラスを継承しています。したがって、FormRequestRequest クラスの拡張版と考えることができ、Request クラスで提供される機能は FormRequest で直接利用できます。

例えば、$this->name$this->input('name') などのデータアクセスも同じように可能です。

FormRequestの何が良い?

実は、FormRequestクラスを利用する以外にも認可・バリデーションを行う方法はLaravelに用意されています。

例えばバリデーションを例に考えてみましょう。Validatorファサードを使う方法、Requestクラスのvalidateメソッドを使うなどがあります。

以下でも解説していますので、ご興味ある方はご覧になってみてください。

これらの方法ではなく、あえてFormRequestクラスを選択するには理由があります。それは、コントローラーからバリデーション処理を完全に切り離す(カプセル化する)ことが可能だからです。

実際、コントローラーにはバリデーションの処理はあまり書きたくありません。できるだけシンプルにした方がコードの見通しがよくなりますし、余計なことを考えたくないからです。

コントローラーが肥大化するコードはFat Controllerと呼ばれ、いけてないコードとされています。

そのため、FormRequestクラスの利用は、LaravelでのWebアプリケーション開発では重要になってきます。

Lara
Lara

簡単なバリデーションだったり、コード例を挙げるだけなら他の方法ももちろんありです。

FormRequestの子クラス作成

最初に書いた通り、FormRequest自体ではなく、FormRequestを継承した子クラスを利用します。基本的な作り方は2種類あります。

コマンドで作る

以下のようなLaravelのartisanコマンドで作成可能です。

php artisan make:request [任意のクラス名]

クラス名は、○○Requestという名称にすることが慣例になっています。ここでは、InquiryRequestというクラスを作りたいので、以下の様にしてコマンドを発行してみます。

php artisan make:request InquiryRequest

これにより、app/Http/Requests/InquiryRequest.phpが作成されます。

以下の様に、/(スラッシュ)で区切ることで、ディレクトリを分けて指定することも可能です。

php artisan make:request Contact/InquiryRequest

これにより、app/Http/Requests/Contact/InquiryRequest.phpが作成されます。

ちなみにスラッシュでなく、\(バッククォート:Windowsの場合半角の¥)で区切っても同様の結果となることを確認しました。

複製する

Laravelコマンドは便利ですが、忘れがちですよね。「なんだっけか……」と調べていたら時間がかかってしまいます。

すでに一度コマンドでFormRequestの子クラスが作成されている場合、調べる必要はありません。複製して別のクラスを作ることも可能だからです。

具体的には、複製したファイルのファイル名&クラス名を、新しいものに変えます。

例えばInquiryRequest.phpApplyRequest.php にする場合、クラス名もInquiryRequestApplyRequestとなります。

注意点としては、ファイルを別のディレクトリに移動する場合は、名前空間の変更が必要になります。

それがよく分からない内は、コマンドで作った方が無難かもしれませんね。

言うまでもないことですが、複製元のファイルに特有のバリデーションルールやカスタムメソッドが存在する場合、必要に応じて変更または削除してください。

Lara
Lara

そもそものコマンドの入力を楽にするツールもありますので、いつかご紹介したいと思います。

認可とバリデーションの基本設定

初期状態のコード

コマンドで作成したInquiryRequestクラスを例に見てみましょう。

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

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

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\Rule|array|string>
     */
    public function rules(): array
    {
        return [
            //
        ];
    }
}

ここで定義されている以下の2つのメソッドを編集すれば、すぐにでも利用可能です。

  1. authrize()
  2. rules()

authrize()

authorize() メソッドは、リクエストが認可されるかどうかを決定するためにあります。このメソッドは、リクエストが実際にバリデーションを受ける前に評価されます。

戻り値としてtrue または false を返す必要があります。

    public function authorize(): bool
    {
        return false;
    }

初期状態では、上記のようにfalseを返すため、バリデーションすら行わず403 Forbiddenを返しリクエストが拒否されます。

これをそのままtrueに変更するか、条件によりtrueを返すようにすればそのまま処理が進みます。つまり認可(許可)されたことになりますね。

    public function authorize(): bool
    {
        return true;
    }

実際には条件分岐して値を返したり、パラメーターにタイプヒントして依存注入したりすることも可能です。

authrize()メソッドについて詳しく知りたい方は、以下ページで解説していますのでご覧ください。条件分岐の例などもあります。

rules()

rules()メソッドは、バリデーション用の配列を戻り値として定義します。ここで定義したルールが、コントローラーの処理が行われる前にそのままバリデーションとして実行します。

以下の様に、初期状態では空の配列=バリデーションは設定されていません。

    public function rules(): array
    {
        return [
            //
        ];
    }

ここでは例として、以下の様に定義してみます。

    public function rules(): array
    {
        return [
          'name'  => 'required|max:20',
          'tel'   => 'nullable|regex:/^[0-9]{2,4}-[0-9]{2,4}-[0-9]{3,4}$/',
          'email' => 'required|email',
          'body'  => 'required|max:1000',
        ];
    }

これは、以下の様な意味を持ちます。

  • name=必須入力で最大20文字
  • tel=入力任意、入力された時はハイフン区切りの電話番号の形式で
  • email=必須入力でメールアドレスの形式
  • body=必須入力で最大1000文字まで

このバリデーションが通らなければ、直前の画面にリダイレクトされます。

Ajax(XMLHttpRequest等)でリクエストされた場合は、422ステータスコードとエラー内容を含むJSON形式でレスポンスを返します。

こちらの指定は特に奥が深いため、いずれ解説ページを設けたいと思います。

Controllerで読み込んで利用する

利用はとても簡単。コントローラの使いたいメソッドのパラメータとして埋め込むだけです。これはLaravelのサービスコンテナという機能による「依存性の注入」と言います。

<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use App\Http\Requests\InquiryRequest;

class InquiryController extends Controller
{
    /**
     * フォーム表示
     * @return \Illuminate\View\View
     */
    public  function index()
    {
        return  view('web.inquiry.index');
    }

    /**
     * フォームPost
     * @param  InquiryRequest $request
     * @return \Illuminate\Http\Response
     */
    public  function post(InquiryRequest $request)
    {
        // ~以下省略~
    }
}

6行目のuse文で、InquiryRequestクラスを使用するための記述をします。

use App\Http\Requests\InquiryRequest;

次に、24行目のpost()メソッド(自作)のパラメータとして、InquiryRequestを読み込むように設定します。

    public function post(InquiryRequest $request)
    {
        // ここを通る時点で認可&バリデーションが通っている

たったのこれだけで、コントローラのメソッド(ここではpost())が呼び出される前にauthorize()、通った場合はrules()の処理が実行されます。

復習になりますが、以下です。

  • authorize()が通らなければ403エラーを返す
  • rules()のバリデーションが通らなければ直前のページにリダイレクト
    (Ajaxの場合はエラーのレスポンスを返す)

つまりこの場合、post()メソッド内部に処理が移った時点で、認可&バリデーションが通っていることが保証されます。

Lara
Lara

コントローラ内でバリデーションのことを考えなくていいので、とても良いですね!

その他基本的な使い方&設定

FormRequestが実装しているプロパティやメソッドをオーバーライド(上書き)することで、動作を変えることが可能です。

また、Requestクラスを継承しているので、同じように使用することが可能です。

リクエストデータを取得する

認可やバリデーションに関するクラスにつき、リクエストデータの取得・利用はよく取り扱います。

例えば以下では、authorize()メソッド内の5行目に$this->input('user_type')でリクエストデータを取得しています。

    public function authorize()
    {
        // リクエストの 'user_type' 入力を取得
        // $this->user_typeでも可
        $userType = $this->input('user_type');

        // 'user_type' が 'admin' であるかどうかをチェック
        return $userType === 'admin';
    }

4行目のコメントでもあるように、$this->user_typeでも同様に取得可能です。

バリデーションの最初の失敗で停止させる

通常、バリデーションは途中でエラーが生じても最後まで実施します。

例えばバリデーション項目にnameとemailがあったとします。しかしながらnameにエラーがあればemailはバリデーションさせたくない……なんて場合があるかもしれません。

以下の指定でそれが実現可能です。

/**
 * 最初のルールの失敗でバリデーションが停​​止します。
 * @var bool
 */
protected $stopOnFirstFailure = true;

このようにすると、先ほどの例で言えばnameでエラーがあった場合、即座にエラーのレスポンスに移ります。emailはバリデーションされません。

バリデーションルールのbailオプションでは、1つのバリデーションルール内において同じようなことができます。$stopOnFirstFailureはそれがバリデーション全体に対し適用されるイメージです。※全てのルールにbailを付与した場合とは動作が異なります。

リダイレクト先を変更する

通常、フォームをPostリクエストされた際、エラーが発生すると、リクエスト元のフォームがあったページへリダイレクトされます。

これを以下の様に任意のリダイレクト先へ変更することが可能です。

/**
 * バリデーション失敗時に、/dashboardにリダイレクトさせる場合
 * @var string
 */
protected $redirect = '/dashboard';

上記では'/dashboard'へリダイレクトしますが、名前付きルートで指定したい場合は、$redirectRouteを使用します。

/**
 * バリデーション失敗時に、dashboardへリダイレクトさせる場合
 * @var string
 */
protected $redirectRoute = 'dashboard';

バリデーション前に処理を行う

バリデーション前に、データを加工したい場合があります。アプリケーション側の都合でフォーマットを整えたい場合です。

これらはprepareForValidation()メソッドをオーバーライドすることで実現可能です。

例えば以下の場合、ハイフンありの電話番号を想定した際、ハイフンに似た文字を半角ハイフンにし、全角数字を半角にしています。

protected function prepareForValidation()
{
    // 入力データ取得 (例 03-1111ー2222
    $tel = $this->input('tel');

    // ハイフンに似た文字を半角ハイフンに変換(この処理はちゃんとやるともっと複雑)
    $tel = str_replace(['-', '―', '‐', 'ー'], '-', $tel);

    // 半角数字を全角数字に変換:結果→03-1111-2222
    $tel = mb_convert_kana($tel, 'n');

    // 変換後のtelをリクエストデータにセット
    $this->merge(['tel' => $tel]);
}

追加バリデーションの実行

LaravelのFormRequestは、単にバリデーションルールを提供するだけでなく、追加のカスタムバリデーションロジックも実装することができます。

これにより、標準的なバリデーションルールに収まらない複雑な条件を簡単に実装することができます。

FormRequestにはそのための方法の一つとしてwithValidator()メソッドが用意されています。このメソッド内で$validator->after() フックメソッドを定義することで、追加バリデーション機能を実現可能です。

以下の例では、emailが'admin@example.com'だった際にエラーを表示するようにしています。

use Illuminate\Validation\Validator;

class InquiryRequest extends FormRequest
{
    // ~略~

    public function withValidator(Validator $validator): void
    {
        $validator->after(function (Validator $validator) {
            if ($this->input('email') === 'admin@example.com') {
                $validator->errors()->add('email', 'このメールアドレスは使用できません。');
            }
        });
    }
}

Validatorクラスをパラメータに取りますので、1行目のように、use文で読み込んでおきましょう。

あとは9~13行目にて、$validator->after()メソッドでバリデーション処理を定義しています。この部分の’email’やエラーメッセージを変えれば、どのようにも応用できそうですね。

マニュアルによればLaravel10からはFormRequestafter()メソッドを利用して同等以上のこともできるようですが、私のLaravel10.0の環境ではなぜか利用できませんでした(要調査)。

本ページでご紹介している方法の方が簡単&Laravelの旧バージョンでも動作するので互換性が高いです。

バリデーション成功時に処理を行う

バリデーションの後にデータを加工したい、という場面もあるかもしれません。

このような処理は passedValidation() メソッドの中で行うことで、コントローラのアクションに到達する前にデータを加工することができます。

protected function passedValidation()
{
    // 'price' の値を整数として扱いたい場合
    $this->merge([
        'price' => intval($this->price)
    ]);
}

passedValidation()メソッドは、バリデーションエラーが存在する場合実行されません。

バリデーションが成功して初めて実行されることを念頭に置く必要があります。

バリデーション失敗時に処理を行う

failedValidation()メソッドをオーバーライドすることで、バリデーション失敗時に処理を行うことが可能です。

以下例では、カスタムのエラーレスポンスを生成しています。

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

class CustomRequest extends FormRequest
{
    // ~略~
    protected function failedValidation(Validator $validator)
    {
        $response = [
            'status' => 422,
            'message' => 'Validation failed!',
            'errors' => $validator->errors()
        ];

        throw new HttpResponseException(response()->json($response, 422));
        //  parent::failedValidation($validator);
    }

ここでは15行目で独自のレスポンスを返していますが、そうしない場合は必ず、親クラスの同メソッドを呼ぶのを忘れないでくださいね(16行目のコメントアウトしている行のように)。

エラーメッセージの日本語化&カスタマイズ

Laravelのインストール直後では、バリデーションエラーが英語で表示されます。そのため、日本語でのWebアプリケーションの場合、日本語化することが必須になってきます。

日本語化するには、以下の2つの方法があります。

  • Laravelの言語ファイルを作成&設定する
  • FormRequestの子クラスで定義する

Laravelの言語ファイルを作成&設定する

Webアプリケーション全体で利用するエラーメッセージの日本語化であれば、言語ファイルが好ましいでしょう。少し長くなるため、以下ページの専用解説ページをご覧ください。

FormRequestの子クラスで定義する

FormRequestの子クラス内に記述する方法もあります。ここで定義する日本語のエラーメッセージは、Laravelの言語ファイルよりも優先されます。

そのため、そのクラスでのみ使いたいエラー表示には適しているでしょう。

messages()メソッドをオーバーライドし、そこで定義可能です。

public function messages()
{
    return [
        'title.required' => 'タイトルは必須です。',
        'body.min' => '本文は最低10文字必要です。',
    ];
}

上記では'title'というバリデーション項目名の、'required'ルールのエラー等を指定しています。

以下のようにmessages()メソッドにプレースホルダー(:attribute)を用意し、attribute()メソッドで置き換える方法もあります。

public function messages()
{
    return [
        'title.required' => ':attribute は必須です。',
        'body.min' => ':attribute は最低 :min 文字必要です。',
    ];
}

public function attributes()
{
    return [
        'title' => 'タイトル',
        'body' => '本文',
    ];
}

こちらも指定には長い説明を要するため、詳しくは以下のページをご参照ください。

コントローラでデータを利用する

FormRequestは、認可とバリデーションのみを行うわけではありません。

コントローラーに処理が移った後で、バリデーションに利用したデータを取得することはごく普通にあります。

バリデーションに通したデータの取得

以下の様に、FormRequestvalidatedメソッドでバリデーション済みデータを取得可能です。

   public function post(InquiryRequest $request)
    {
        // ここを通る時点で認可&バリデーションが通っている

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

$validatedは配列で返されます。dd($validated);の出力例は以下です。

array:4 [▼ // app\Http\Controllers\InquiryController.php:30
    "name" => "鈴木"
    "tel" => "03-1111-2222"
    "email" => "info@exmple.jp"
    "body" => """
      お問い合せです。
      改行が含まれます。
    """
]

つまりはコントローラー内で$validated['name']としてアクセス可能です。

バリデーションに通した一部のデータの取得

$request->validated();は全てのバリデーション済みデータを取得しますが、余計なデータは不要なケースもあります。

例えば「会員規約に同意する」の値は、バリデーションには使うけどデータベースには登録しないので不要……という場合です。

こういったケースでは一部のデータを指定して取得する方法もあります。

具体的には$request->safe()メソッドが返すIlluminate\Support\ValidatedInputオブジェクトが持つonly()except()メソッドを使います。

  public function post(InquiryRequest $request)
  {
    // ここを通る時点で認可&バリデーションが通っている

    // バリデーション済みの'name'と'email’のみ取得
    $validated = $request->safe()->only(['name', 'email']);

    // バリデーション済みの'name'と'email’以外を取得
    $validated = $request->safe()->except(['name', 'email']);

safe()という名称の通り、バリデーションに通っているので安心して利用可能です。

もちろんバリデーションが正しく行えていれば、という条件はあります。

応用的な使い方

これまでに解説した情報だけで、FormRequestを使って大抵の認可・バリデーションや、その後のデータの利用ができると思います。

しかしながら、実際のWebアプリケーションでは「ああしたい」「こうしたい」と細かい要望が発生するのはめずらしくありません。

ここではそんな応用編の使い方をご紹介します。それぞれが丁寧に解説すると長くなってしまうので、ここでは簡単にご説明します。

ルールを分岐させる

ルールは固定で定義しても良いですが、多くの場合はそうはいきません。

リソースの作成(POSTリクエスト)と更新(PUTまたはPATCHリクエスト)で異なるバリデーションルールを適用するシーンは割とよくあります。

例えば以下では、新規登録時はメールアドレスをuniqueに。更新時は、該当ユーザーのemailはuniqueから除外します。

public function rules()
{
    if ($this->isMethod('post')) {
        return [
            'name' => 'required|string|max:255',
            'email' => 'required|email|unique:users,email',
            // ...
        ];
    }

    if ($this->isMethod('patch')) {
        return [
            'name' => 'sometimes|required|string|max:255',
            'email' => 'sometimes|required|email|unique:users,email,' . $this->route('user'),
            // ...
        ];
    }
    // 本来はデフォルトのエラーも定義が必要
    return [];
}

prepareForValidation()で加工したデータをold()で表示させたい

例えば、前項のようにprepareForValidation()でデータを加工したとします。しかしその後バリデーションエラーで直前のページにリダイレクトされた際、ビューのold()ヘルパー関数で使用することはできません。

しかしながら、ユーザビリティ向上のためなどの理由によりold()でも加工済みデータを使いたい場合もあるでしょう。

そんな場合はfailedValidation()をオーバーライドし、リクエストを上書きすることで実現可能です。

条件として、前項のprepareForValidation()でデータを加工します。その後、13行目のように、加工後のデータを使ってリクエストオブジェクトの値を書き換えます。

protected function prepareForValidation()
{
    // データを加工する
    $tel = $this->input('tel');
    $tel = str_replace(['-', '―', '‐', 'ー'], '-', $tel);
    $tel = mb_convert_kana($tel, 'n');
    $this->merge(['tel' => $tel]);
}

protected function failedValidation(Validator $validator)
{
    // 加工後のデータを使って値を書き換える
    request()->merge($this->input());

    // 親クラスのメソッドを実行(例外をスローさせる)
    parent::failedValidation($validator);
}

403エラーのページをカスタマイズしたい

厳密にはFormRequestの問題ではありませんが、関連情報なので簡単にご案内しておきます。

resources/views/errorsディレクトリに403.blade.phpという名前で新しいビューファイルを作成すればOKです。

まとめ

FormRequestクラスはなかなかできることも多く、ボリュームがありましたね。本ページでは伏せませんでしたが、発生したエラーをビューで表示する方法については以下ページをご覧ください。

認可やバリデーションはウェブアプリケーションにとっては重要な要素ですので、しっかりとした知識が必要になります。

Larave公式マニュアルよりも、ちょっとでも分かりやすくと思いまとめました。学習のお手伝いができれば幸いです。

Lara
Lara

私自身もまとめることで理解を深めることができました!

コメント

タイトルとURLをコピーしました