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',
}),
}
},
問題
これが期待される正常な動作なのだけど,最強の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 から変更
{
//略
}
Comments