🩺

[Laravel] フォームリクエストを使って入力値を加工する(バリデーション失敗で元の画面リダイレクトしても加工できる)

2022/01/01に公開

Laravelでフォームを扱うときは、フォームリクエストを使うと便利です。
フォームリクエストは、主にバリデーションを別クラスに切り離してコントローラーをシンプルにする目的で使われます。

その他に、入力値の加工や整形もできます。
これを今回の記事で取り扱います。

前提

お知らせ管理の新規入力を想定します。

項目名 name属性 UI バリデーションルール
日付 publish_year, publish_month, publish_day プルダウン 必須、日付形式
タイトル title テキストエリア 必須、最大60文字
本文 body テキストエリア 必須、最大140文字
  • 入力フォームのvalue属性はold関数を使っています。

加工した入力値をバリデーション

prepareForValidation()メソッドを使って、データを加工してからバリデーションを行います。
例えば下記のようなことができます。

  • 全角英数を半角英数にしてから英数のバリデーションを試みる
  • 年月日のプルダウンを連結させて日付が正しいかどうかをバリデートする

変換後のデータを「パラメータ名 => 変換後の値」という形の連想配列で用意します。
mergeメソッドの引数に用意したデータを入れて実行すればバリデーション用のデータを加工できます。

バリデーション 8.x Laravel

prepareForValidation()の実験

意味のない例ですが、実験としてリクエストオブジェクトを書き換えて最大文字数を超えるデータを作ってみます。
タイトルを61文字に書き換えてバリデートしています。
タイトルのルールはrules()メソッドにあるとおり必須と最大60文字です。
結果バリデーションは失敗します。
こんなことをするとタイトルに何を入れようが入力画面に差し戻されます。

<?php
// App/Http/Requests/AnnouncementRequest.php

namespace App\Http\Requests;

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

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

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        $data = $this->all();

        $rules = [
            'publish' => ['required', 'date_format:Y-m-d'],
            'title'   => ['required', 'max:60'],
            'body'    => ['required', 'max:140'],
        ];


        return $rules;
    }

    public function messages()
    {
        return [
            'required'    => ':attributeを入力してください。',
            'max'         => ':attributeは:max文字以内で入力してください。',
            'date_format' => ':attributeを正しく入力してください。',
        ];
    }

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

    protected function prepareForValidation()
    {
        $data = [];
        $data['title'] = 'あああああいいいいいうううううえええええおおおおおあああああいいいいいうううううえええええおおおおおあああああいいいいいあ'; // 61文字

        $this->merge($data);
    }
}

タイトル欄は最大文字数を超えていないのに最大文字数超えのバリデーションエラーが表示されます。

注意するべきは、この処理だけではバリデーションのためのデータを作っているに過ぎないということです。
この処理だけでは次の画面の表示は加工前のままです。
加工後のデータを表示させる方法は後述します。

少し実用的な使い方

この例は日付をプルダウン3つで選択しています。
正しい日付かをバリデートするために、3つのパラメータを連結しています。
タイトル、本文は全角英数 -> 半角英数に変換しています。

<?php
// App/Http/Requests/AnnouncementRequest.php

// 略
namespace App\Http\Requests;

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

class AnnouncementRequest extends FormRequest
{
    // 略(attributes()メソッドまで内容同じ)

    protected function prepareForValidation()
    {
        $data = [];
        $data['publish'] = sprintf('%04d-%02d-%02d', $this->publish_year, $this->publish_month, $this->publish_day);
        if ($data['publish'] == '0000-00-00') $data['publish'] = null;
        $data['title'] = mb_convert_kana($this->title, 'aKV');
        $data['body'] = mb_convert_kana($this->body, 'aKV');

        $this->merge($data);
    }
}

バリデーション成功で次の画面に進んだときに加工後のデータを表示させる

$request->validated()メソッドで取得する

リクエストオブジェクトのvalidated()メソッドでバリデート済のデータを配列で取得できます。

フォームリクエストのrules()メソッドに記載していないパラメータは取得できないようになっています。

$request->safe()メソッドで取得する

リクエストオブジェクトのsafe()メソッドでも取得できます。
Laravel8.55で追加されたメソッドです。
validated()との違いはValidatedInputオブジェクトで取得するところです。
このオブジェクトは、merge()、only()、except()にメソッドチェーンできます。

Laravel 8.55~ やや存在感薄めの safe メソッドで遊んでみる

これもvalidated()メソッド同様フォームリクエストのrules()メソッドに記載していないパラメータは取得できないようになっています。

passedValidation()メソッドで加工する

passedValidation()メソッドを使うとリクエストオブジェクトに加工後のデータを入れることができます。
これをコントローラーで参照できます。
加工前と加工後も同じ処理をする場合はpassedValidation()の中でprepareForValidation()を呼び出せばよいでしょう。

// App/Http/Requests/AnnouncementRequest.php

// 略
namespace App\Http\Requests;

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

class AnnouncementRequest extends FormRequest
{
    // 略

    protected function passedValidation()
    {
        $this->prepareForValidation();
    }
}

ここでも注意があります。
passedValidation()が反映されるのはコントローラー側でリクエストオブジェクトのall()メソッドの返り値です。
validated()やsafe()には反映されないので注意が必要です。
また、加工後のデータを取得できるのは、バリデーションに成功したときのみです。
失敗した場合はそのままでは加工前のデータが表示されてしまいます。
失敗した場合も加工後のデータを表示させたい場合はひと手間加える必要があります。
次で説明します。

バリデーション失敗で元の画面にリダイレクトしたときも加工後のデータを表示させる

フォームリクエストを使ってバリデーションに失敗すると元の画面にリダイレクトします。
このとき、セッションに元の画面の入力値が保存されていて、old関数で取り出すことができます。
フォームのビューファイルで、value属性に「{{ old('name属性') }}」という書き方がされるのはこのためです。
そうすることで元の入力値が表示されるわけです。

ただし、ここで表示されるのは加工前の入力値です。
加工後の入力値を表示させたい場合はどうすればよいでしょう?

回答

バリデーションに失敗したときの処理をカスタマイズすることで実現できます。

    protected function failedValidation(Validator $validator)
    {
        // 加工後のデータを使ってリクエストオブジェクトを書き換える
        request()->merge($this->input());

        // 基底クラスの処理を実行
        parent::failedValidation($validator);
    }

Laravel Request does not return the modified request on validation fail - Stack Overflow

補足説明

バリデーションに失敗すると、failedValidation()というメソッドが呼ばれます。

    /**
     * Handle a failed validation attempt.
     *
     * @param  \Illuminate\Contracts\Validation\Validator  $validator
     * @return void
     *
     * @throws \Illuminate\Validation\ValidationException
     */
    protected function failedValidation(Validator $validator)
    {
        throw (new ValidationException($validator))
                    ->errorBag($this->errorBag)
                    ->redirectTo($this->getRedirectUrl());
    }

failedValidation()はFormRequestクラス(作ったフォームリクエストクラスの基底クラスです。)に書かれているものです。
そのためこのメソッドをオーバーライドして処理を追加します。

基底クラスの処理の前に「request()->merge($this->input())」でリクエストオブジェクトを書き換えます。
(request()で、リクエストオブジェクトを取得します。リクエストオブジェクトがもつmerge()メソッドでオブジェクトを書き換えます。「$this->input()」はprepareForValidation()メソッドで加工した値です。)
セッションには加工後のデータが入る -> 差し戻された画面でも加工後のデータが表示される、というように実現できます。

    protected function failedValidation(Validator $validator)
    {
        // 加工後のデータを使ってリクエストオブジェクトを書き換える
        request()->merge($this->input());

        // 基底クラスの処理を実行
        parent::failedValidation($validator);
    }

Discussion