カテゴリー
Laravel

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

Laravelのバリデーションはエラーが出た場合自動的にセッションにエラー内容を格納して元のページへリダイレクトまでやってくれる。ただ,Inertiaを使っている場合,Nginxだと502 Bad Gatewayのエラーが出たり,エラー内容がerrorに格納されない現象がでたので解決方法を調べてみた。

環境

  • 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']
        ])->validate();

        $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に入っているフォーム値すべてと,バリデーションルールを渡し,validateメソッドへチェインすることでバリデーションまで実行する。

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

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

問題

これが期待される正常な動作なのだけど,最強の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だとなぜ動かないのか謎。

解決方法

validate()メソッドにエラーメッセージセットとリダイレクトを任せるのではなく自分でセッションにエラーを格納し,リダイレクトさせるとしっかりフォームのほうでもエラーメッセージが表示されるので,なんかすっきりしないもののこれで解決させることに。

    public function store(Request $request)
    {
        //バリデーションだけ行う(エラー時の自動遷移なし)
        $validator = Validator::make($request->all(), [
            'title' => ['required', 'string', 'max:255'],
            'number' => ['required', 'string', 'max:100'],
            'completed' => ['boolean','nullable']
        ]);

        //バリデーションチェックに失敗したら
        if ($validator->fails()) {
            $errors = new ViewErrorBag(); //エラーメッセージを作成
            $errors->put('default', $validator->errors()); //defaultエラーバッグにバリデーションエラーをセット
            $request->session()->flash('errors', $errors); //セッションのフラッシュメッセージにエラーメッセージをセット
            return redirect()->back(); //リクエスト元のページにリダイレクト
        }

        $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 = []);
    }

FormRequestを使用した方法

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

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

artisan make:request BookmarkStoreRequest

このコマンドでFormRequestクラスの雛形ファイルが「app\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()メソッドでチェックするルールを返すようにする。

failedValidation()メソッドがバリデーションに失敗したときに呼び出されるメソッドなのでこのメソッドを追加してオーバーライドする

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

    /**
     * @Override
     * 勝手にリダイレクトさせない
     * @param  \Illuminate\Contracts\Validation\Validator  $validator
     */
    protected function failedValidation(Validator $validator)
    {
        $errors = new ViewErrorBag();
        $errors->put('default', $validator->errors());
        $this->session()->flash('errors', $errors);
        throw new HttpResponseException(
            redirect()->back()
        );
    }

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

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

Vueファイルはこんな感じ
/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 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" required autofocus autocomplete="title" />
                                <jet-input-error v-if="errors.title" :message="errors.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 v-if="errors.number" :message="errors.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 v-if="errors.completed" :message="errors.completed[0]" 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>

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です