Laravelで同一ページ複数フォームに対応する。FormRequest×名前付きエラーバッグ使用

同一ページ複数フォームFormRequest ×名前付きエラーバッグ Laravel
※当サイトはアフィリエイト広告を掲載しています。

同一ページに、複数のフォームを存在させることがあります。そんな時、エラーは名前付きエラーバッグに入れれば良いのでは……と頭に浮かぶ方は多いかもしれません。

例えば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. フォームごとにエラーを分けたい
  2. エラー時に、送信したフォームの表示位置に移動させたい

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');

これでも全く同じ動作となります。

Lara
Lara

この方がスッキリして良いですね!こういったメソッドを知ることで、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つでも複数フォームに対応可能なはずですね。

個人的には同一ページに複数フォームがある実装はあまり行った経験がありませんが、実はちょうど実務案件で必要になり、調べた次第です。

本記事の内容が、あなたの案件で少しでも参考になれば幸いです。

コメント

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