Laravel8+InertiaでFormRequestを使ってバリデーション

Laravel

Laravelのバリデーションはエラーが出た場合自動的にセッションにエラー内容を格納して元のページへリダイレクトまでやってくれる。この記事ではそのエラー内容をどうやってView側で表示させるかという点を解説します。

環境

  • Laravel8+jetstream+Inertia(Vue.js)

前提条件

フォーム画面

モデル

「Bookmark」モデルが存在する。構造は下記のような感じ。

コントローラー

「BookmarkController」というBookmarkモデルのCRUDコントローラーがあるとし,データを作成する「store」メソッドは下記の通り。

public function store(Request $request)
    {
        $validator = Validator::make($request->all(), [
            'title' => ['required', 'string', 'max:255'],
            'number' => ['required', 'string', 'max:100'],
            'completed' => ['boolean','nullable']
        ])->validatewithBag('saveBookmark');

        $bookmark = new Bookmark;
        $bookmark->user_id = Auth::id();
        $bookmark->title = $request->title;
        $bookmark->number = $request->number;
        $bookmark->completed = $request->boolean('completed');
        $bookmark->save();
        return redirect()->route('bookmarks.index', $parameters = [], $status = 303, $headers = []);
    }

このような新規作成用のフォームから作成しようとする。ちなみにマンガタイトル=title,読んだ話数=number,最後まで読み終えた場合はチェック=completedにそれぞれ対応している。

先ほどのBookmarkControllerのバリデーションルールでは下記のようになっている。バリデーションクラスのmakeメソッドへ$requestに入っているフォーム値すべてと,バリデーションルールを渡し,validatewithBagメソッドへバッグ名を渡してチェインすることでバリデーションまで実行する。

バッグというのはフォームが同一ページ内に複数ある場合などに識別するためのものと考えてもらえばOK。

$validator = Validator::make($request->all(), [
            'title' => ['required', 'string', 'max:255'],
            'number' => ['required', 'string', 'max:100'],
            'completed' => ['boolean','nullable']
])->validatewithBag('saveBookmark');

このバリエーションルールでは,「title」と「number」が必須(required),「completed」は必須ではない(nullable)項目として設定している。もし,「title」か「number」どちらかが(もしくは両方)記入されていない場合はバリデーションエラーとなる。

ビュー

/resources/js/Pages/Bookmark/Create.vue

<template>
    <app-layout>
        <template #header>
            <h2 class="font-semibold text-xl text-gray-800 leading-tight">
                ブックマーク作成
            </h2>
        </template>
            <div>
                <div class="max-w-7xl mx-auto py-10 sm:px-6 lg:px-8">
                    <jet-form-section @submitted="saveBookmark">
                        <template #title>
                            ブックマーク作成
                        </template>

                        <template #description>
                            マンガのタイトルと,どこまで読んだのかを記録しましょう!
                        </template>
                        
                        <template #form>
                            <div class="col-span-6 sm:col-span-4">
                                <jet-validation-errors bag="saveBookmark" class="mb-4" />
                            </div>
                            <div class="col-span-6 sm:col-span-4">
                                <jet-label for="title" value="マンガタイトル" />
                                <jet-input id="title" type="text" class="mt-1 block w-full" v-model="form.title" ref="title"  autofocus autocomplete="title" />
                                <jet-input-error :message="form.error('title')" class="mt-2" />
                                
                            </div>

                            <div class="col-span-6 sm:col-span-4">
                                <jet-label for="number" value="読んだ話数" />
                                <jet-input id="number" type="text" class="mt-1 block w-full" v-model="form.number" ref="number" autocomplete="number" />
                                <jet-input-error :message="form.error('number')" class="mt-2" />
                                
                            </div>

                            <div class="col-span-6 sm:col-span-4">
                                <label for="completed">
                                    <input id="completed" type="checkbox" class="mt-1" v-model="form.completed"  />
                                    最後まで読み終えた場合はチェック
                                </label>
                                <jet-input-error :message="form.error('completed')" class="mt-2" />
                                
                            </div>
                        </template>

                        <template #actions>
                            <inertia-link :href="route('bookmarks.index')">ブックマーク一覧へ戻る</inertia-link>

                            <jet-action-message :on="form.recentlySuccessful" class="mr-3">
                                保存しました
                            </jet-action-message>

                            <jet-button :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
                                保存
                            </jet-button>
                        </template>
                    </jet-form-section>
                </div>
            </div>
        
    </app-layout>
</template>

<script>
    import AppLayout from '../../Layouts/AppLayout'
    import JetActionMessage from './../../Jetstream/ActionMessage'
    import JetButton from './../../Jetstream/Button'
    import JetFormSection from './../../Jetstream/FormSection'
    import JetInput from './../../Jetstream/Input'
    import JetInputError from './../../Jetstream/InputError'
    import JetLabel from './../../Jetstream/Label'
    import JetValidationErrors from './../../Jetstream/ValidationErrors' 

    export default {
        components: {
            AppLayout,
            JetActionMessage,
            JetButton,
            JetFormSection,
            JetInput,
            JetInputError,
            JetLabel,
            JetValidationErrors,
        },
        props:{
            errors: {
                type: Object,
            }
        },
        data() {
            return {
                form: this.$inertia.form({
                    title: '',
                    number: '',
                    completed: '',
                }, {
                    bag: 'saveBookmark',
                }),
            }
        },

        methods: {
            saveBookmark() {
                this.$inertia.post(route('bookmarks.store'), this.form, {
                    preserveScroll: true,
                    onSuccess: () => {
                        //
                    }
                })
            },
        },
    }
</script>

エラーメッセージ表示例

エラーメッセージの表示方法

エラーの表示方法は二種類ある。Inertiaから渡されるJSONデータを見ると、”errorBags”と、”errors”と二種類エラーが入っており、”errorBags”がまとめて表示、”errors”が各要素ごとのエラー表示に使われている。

{
	"component": "Bookmark/Create",
	"props": {
		"errorBags": {
			"saveBookmark": {
				"title": [
					"タイトルを入力してください。"
				],
				"number": [
					"話数は必ず指定してください。"
				]
			}
		},
		"errors": {
			"saveBookmark": {
				"title": "タイトルを入力してください。",
				"number": "話数は必ず指定してください。"
			}
		}
	}
}

エラーメッセージをまとめて表示

このうちエラーメッセージをまとめて表示させているコードはこの行。JetstreamのValidationErrorsコンポーネントを利用し、エラーバッグ名を渡してどのフォームに対するエラーを表示するのかを指定している。

<jet-validation-errors bag="saveBookmark" class="mb-4" />

エラーごとにエラーのあるフォーム要素の近くに表示

対して、個別にエラーを表示させているのは各フォーム要素の下にあるこれら。「title」や「number」といった要素の名前を渡してどの要素に対するエラーを表示するのかを指定している。

<jet-input-error :message="form.error('title')" class="mt-2" />
<jet-input-error :message="form.error('number')" class="mt-2" />
formについて

:message=”form.error(‘number’)” この意味は、formの’number’というタイトルのerrorを取得するメソッドを呼び出し、messageに代入するというもの。

formは、Vue.jsのデータで定義されている。inertiaのformから作成されている。ここではbagに’saveBookmark’というバッグ名を渡しているので、帰ってきたエラーの中から’saveBookmark’というバッグ名に対応するものを表示するという流れ。

        data() {
            return {
                form: this.$inertia.form({
                    title: '',
                    number: '',
                    completed: '',
                }, {
                    bag: 'saveBookmark',
                }),
            }
        },

問題

2021年5月現在このバグは解消済み。もしエラーが出たりバリデーションのメッセージが出ない場合はLaravel,jetstream,Inertiaのアップデートを行ってみるといいかも。

これが期待される正常な動作なのだけど,最強のLaravel開発環境をDockerを使って構築する【新編集版】を参考に作成したDocker+nginxの環境だと,502 Bad Gatewayエラーが出る。

ネットで検索すると「Inertiaはデフォルトではエラーメッセージを自動でセッションに格納しないから自分でそのコードを書かないとだめだよ」という情報が見つかるも,v.0.2.9のアップデートで自動的にエラーを追加するようにしてやったぜというリリースが見つかり,使用しているバージョンが0.3なのでこれは対策済みということが判明。では一体なぜ?

別の情報ではNginxの設定ファイル「default.conf」にバッファーサイズの設定を下記のように加えると(ファイルの一番上に)

fastcgi_buffers 16 16k;
fastcgi_buffer_size 32k;

502 Bad Gatewayエラーは出なくなったものの今度はバリデーションエラーのメッセージが出ない。帰ってくるJSONのpropsを見ても「error」「errorBags」共に空。

php artisan serve

で起動させた開発用サーバーの場合はちゃんと動作し,バリデーションエラーが表示される。Nginxだとなぜ動かないのか謎。

FormRequestを使用した方法

作成だけなら上記の方法でもいいのだけど,更新の際も同様にバリデーションチェックを行う必要があるので,そうなると同じ処理を「store」「update」両方で行うことになり美しさにかけるし,コントローラーの肥大化にもなる。

そこでLaravelに用意されているFormRequestという,別のファイルでバリデーションを行い,コントローラーにはバリデート済みのデータだけを渡す仕組みを使用する。

artisan make:request BookmarkStoreRequest

このコマンドでFormRequestクラスの雛形ファイルが「app\Http\Requests」フォルダに作成される。

    public function authorize()
    {
        return true; //falseから変更
    }
public function rules()
    {
        return [
            'title' => ['required', 'string', 'max:255'],
            'number' => ['required', 'string', 'max:100'],
            'completed' => ['boolean','nullable']
        ];
    }

rules()メソッドでチェックするルールを返すようにする。

エラーバッグの名前を付けるには、クラスにprotectedで$errorBag変数を追加。すると、全体は以下のようになる。

class BookmarkStoreRequest extends FormRequest
{
    protected $errorBag = 'saveBookmark';

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

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'title' => ['required', 'string', 'max:255'],
            'number' => ['required', 'string', 'max:100'],
            'completed' => ['boolean','nullable']
        ];
    }
}

このFormRequestを使用するには,コントローラーのstoreやupdateメソッドで引数としてRequestクラスをとっていたのをFormRequestクラスに変更する

//略
use App\Http\Requests\BookmarkStoreRequest;
//略

public function store(BookmarkStoreRequest $request) //Request $request から変更
{
//略
}

この記事を書いた人

PHPが好物な個人開発プログラマ。フリーランスエンジニアとしてWebサービス作ったりしてます。15年の経験を生かしてMENTAでメンターもやってます。WordPressやPHPでお困りのことがあればご相談に乗りますのでDMください。

Follow on SNS
Laravel
SOHO MIND

Comments

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