同一ページに、複数のフォームを存在させることがあります。そんな時、エラーは名前付きエラーバッグに入れれば良いのでは……と頭に浮かぶ方は多いかもしれません。
例えばLaravelの公式マニュアルには、以下の様にあります。
$validatedData = $request->validateWithBag('post', [
'title' => ['required', 'unique:posts', 'max:255'],
'body' => ['required'],
]);
しかし私は、バリデーションは可能な限りFormRequest
を継承したクラスで行っています。その場合、自動でバリデーションされてしまうので、上記の方法が採れません。
そこで本ページでは、FormRequest
を使用しての、名前付きエラーバッグにエラーを入れる方法。そして、同一ページに複数フォームを実装する方法をご紹介します。
状況(何が問題!?)
エラーが重複して表示されてしまう
同一ページの別フォームであっても、同じフォーム要素名があると、エラー時に両方エラーを表示してしまいます。
極端な例だとこんな感じです。form1, form2いずれの送信ボタンを押しても、同様の状況になります。
コントローラーからはエラーバッグに入れられない
以下のようにInquiryRequest
を注入していますので、コントローラー内を処理する前にバリデーションが行われています。
そのため、コントローラー内で名前付きエラーバッグに入れることができません。
public function confirm(InquiryRequest $request)
{
$validates = $request->validated();
return view(
'web.contact.inquiry_confirm',
[
'inputs' => $validates,
]
);
}
冒頭のように、自分でバリデーションするなら、validateWithBag
メソッドを使えば良いのですが、それができないのです。
$validatedData = $request->validateWithBag('post', [
'title' => ['required', 'unique:posts', 'max:255'],
'body' => ['required'],
]);
やりたいこと
- フォームごとにエラーを分けたい
- エラー時に、送信したフォームの表示位置に移動させたい
1はすでに書いた通り。論理的に考えれば、名称を変えればいけるのではないかと思われます。例えば、フォーム要素名にform1_name
などプレフィックスをつけるなどです。
ですが本ページでは、同じ名称でもいける方法を目指してみたいと思います。
2は、例えばフォーム2を送信させた際のエラー時には、フォーム2の部分に移動させたいです。
Laravelを使わない場合のフォームであれば、フォーム2にアンカーとしてid="form2"
と指定し、<form action="#form2">
のようにしていたことを実現したいと思います。
エラーバッグに入れる
まずはLaravelの名前付きエラーバッグを利用して、エラーを入れるところから始めます。
フォームリクエストで名前付きエラーバッグを使う
フォームリクエスト利用時には、何もしないとエラーはデフォルトのエラーバッグに入ります。
名前付きエラーバッグに入れるには、failedValidation
メソッドをオーバーライドすると良さそうです。
このメソッドはその名の通り、バリデーションに失敗した際に呼ばれます。
以下はform1
に対応する、InquiryRequest
クラスです。
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Contracts\Validation\Validator; // !!追加
use Illuminate\Validation\ValidationException; // !!追加
class InquiryRequest extends FormRequest
{
protected function failedValidation(Validator $validator)
{
$errorBag = 'form1';
$response = redirect()->to($this->getRedirectUrl(). '#form1')
->withErrors($validator, $errorBag)
->with($errorBag, $this->except(['password']));
throw new ValidationException($validator, $response);
}
// 以下略
14行目
リダイレクトURLの最後に、フォームのアンカー#form1
を追加しています。これでexample.jp/inquiry#form1
のようなURLとすることができます。
※後述しますが、別の方法を見つけましたのでそれを推奨します。
15行目
エラーバッグの名称を第2引数として指定します。
16行目
入力データをセッションに引き継いでいます。本来はwithInput
メソッドを使うことで、ビューではold
ヘルパー関数でデータを取得するのが一般的です。
……が、old
は名前付きエラーバッグのエラー取得に対応してないっぽいので、with
で独自にセッションでデータを引き継ぐことにしました。
with
メソッドは第1引数にセッション名を指定します。今回は名前付きエラーバッグと同じ名称にしましたが任意です。
本筋には影響しませんが、password
だけは入力データを引き継ぐ必要は無いので、except
メソッドで除外しました。
18行目
バリデーションの例外を投げます。
追記
先ほどのコードは、手作業でアンカーを足しました。
$response = redirect()->to($this->getRedirectUrl(). '#form1')
->withErrors($validator, $errorBag)
->with($errorBag, $this->except(['password']));
その後分かったのですが、withFragment
という専用のメソッドがあるようです。
$response = redirect()->to($this->getRedirectUrl())
->withErrors($validator, $errorBag)
->with($errorBag, $this->except(['password']))
->withFragment('form1');
これでも全く同じ動作となります。
この方がスッキリして良いですね!こういったメソッドを知ることで、Laravelの基礎力が上がってくると思います。
Bladeの対応
名前付きエラーバッグを指定したため、通常の方法ではエラーが取れなくなります。また、ユーザー入力データの取得方法も変わってきます。
そのため、各種指定を修正する必要があります。
全体のエラー有無の判定
以下の様に、$errors->form1
と名前付きバッグの名称を指定すればOKです。
@if ($errors->form1->any())
<div class="error-index">
<p>⚠️エラーが発生しました。以下の赤文字のエラー箇所をご確認ください。</p>
</div>
@endif
all
メソッドも同じ感じで使えます。後述しますが、has
は使えなかったので、全く同じ感じでは使えないのかもしれません(このあたりは手空き時に要調査)。
@foreach ($errors->form1->all() as $error)
{{ $error }}
@endforeach
個別のエラー有無の判定
$errors->form1->has('name')
や、$errors->has('name', 'form1')
とやりたくなるところですが、これらはエラーにはならずとも期待した動作にはなりません。
以下のgetBag
でバッグを取得し、has
することで期待通りのデータが得られました。
@if($errors->getBag('form1')->has('name'))
エラー
@else
エラーではありません
@endif
@errorディレクティブ
対して@errorは、以下の様に、第2引数に名前付きエラーバッグの名称を指定するだけでOK。こちらの方が簡単ですね。
@error('name', 'form1')
<div class="error">{{ $message }}</div>
@enderror
データの取得
先ほども触れましたが、old
関数ではデータを取得できません。代わりにsession
関数で直接セッションからデータを取り出します。
<input name="name" type="text" value="{{ session('form1.name') }}">
アンカーリンクを作る
以下の様に、フォームにIDをform1
としてふっておきます。そうすることで、アンカーとして利用できるため、example.jp/inquiry#form1
のようにアンカー付きでのエラー表示時に、この部分が表示できるようになります。
<form id="form1" action="{{ route('inquiry.confirm') }}" method="post">
まとめ
今回はform1だけの解説にとどめましたが、form2も全く同様のことを繰り返して作れば、同一ページ中に複数フォーム対応が可能になります。
2つどころか、3つでも4つでも複数フォームに対応可能なはずですね。
個人的には同一ページに複数フォームがある実装はあまり行った経験がありませんが、実はちょうど実務案件で必要になり、調べた次第です。
本記事の内容が、あなたの案件で少しでも参考になれば幸いです。
コメント