前回は1行1項目で3行の入力フォームの話でした。今回は難度をアップして1行3項目で3行の入力フォームの話です。

複数行・複数項目の入力フォーム

今回のフォームはこんな感じです。前回より複雑でしょう。

コントローラは、前回と同様ですが、FormRequestはItemsRequestとします。

namespace App\Http\Controllers;

use App\Http\Requests\ItemsRequest;
use Illuminate\Http\Request;

class Form2Controller extends Controller
{
    /**
     * Show the form for creating a new resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function create()
    {
         return view('form2');
    }

    /**
     * Store a newly created resource in storage.
     *
     * @param  App\Http\Requests\ItemsRequest  $request
     * @return \Illuminate\Http\Response
     */
    public function store(ItemsRequest $request)
    {
       ddd($request->validated());
    }
}

ブレードは、

...
<div class="container-fluid">
  <div class="row">
    <div class="col-md-12 col-lg-12 mt-2">
      <div class="card card-primary">

        <div class="card-header">
          <h3 class="card-title">複数行のフォーム</h3>
        </div>

        <form method="POST" action="{{ route('form2.store') }}" class="form-horizontal" novalidate="">
          @csrf
          <div class="card-body">
            @error('names')
              <div class="invalid-feedback d-block">{{ $message }}</div>
            @enderror
            <div class="table-responsive">
              <table class="table table-bordered table-hover table-sm">
                <thead>
                  <tr>
                    <th>アイテム名</th>
                    <th class="col-2">価格</th>
                    <th class="col-1">個数</th>
                  </tr>
                </thead>
                <tbody>

                  @for ($i = 0; $i < 3; $i++)
                    <tr>
                      <td>
                        <input class="form-control" maxlength="255" name="names[]" type="text" value="{{ old('names.'.$i) }}">
                        @error('names.'.$i)
                          <span class="invalid-feedback d-block">{{ $message }}</span>
                        @enderror
                      </td>
                      <td>
                        <input class="form-control" maxlength="10" name="prices[]" type="text" value="{{ old('prices.'.$i) }}">
                        @error('prices.'.$i)
                          <span class="invalid-feedback d-block">{{ $message }}</span>
                        @enderror
                      </td>
                      <td>
                        <input class="form-control" maxlength="5" name="quantities[]" type="text" value="{{ old('quantities.'.$i) }}">
                        @error('quantities.'.$i)
                        <span class="invalid-feedback d-block">{{ $message }}</span>
                        @enderror
                      </td>
                    </tr>

                  @endfor
                </tbody>
              </table>
            </div>
          </div>

          <div class="card-footer">
            <button class="btn btn-primary float-right mr-2" type="submit">保存</button>
          </div>
        </form>
      </div><!-- card -->
    </div>
  </div><!-- row -->
</div>
...

FormRequestは、


namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Validation\ValidationException;

class ItemsRequest extends FormRequest
{
    protected function prepareForValidation()
    {
        $names = $prices = $quantities = [];

        // nullを削除するために入力データを作成しなおす
        foreach($this->names as $i => $name) {
            if ($name === null) {
                continue;
            }

            array_push($names, $name);
            array_push($prices, $this->prices[$i]);
            array_push($quantities, $this->quantities[$i]);
        }

        $this->merge([
            'names'      => $names,
            'prices'     => $prices,
            'quantities' => $quantities,
        ]);
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'names'        => 'required|array', // 必ず1行の入力が必要
            'names.*'      => 'nullable|string',
            'prices.*'     => 'required_with:names.*|integer',
            'quantities.*' => 'required_with:names.*|integer|min:1',
        ];
    }

    public function attributes()
    {
        return [
            'names.*'      => 'アイテム名',
            'prices.*'     => '価格',
            'quantities.*' => '個数',
        ];
    }

    public function messages()
    {
        return [
            'names.required'             => '必ず1つの入力が必要です',
            'prices.*.required_with'     => '「アイテム名」が入力されているときは「価格」は必須です.',
            'prices.*.integer'           => '「価格」は整数の入力が必要です.',
            'quantities.*.required_with' => '「アイテム名」が入力されているときは「個数」は必須です.',
            'quantities.*.integer'       => '「個数」は整数の入力が必要です.',
            'quantities.*.min:1'         => '「個数」は最低1です.',
        ];
    }
}

エラーの出力はこんな感じです。

エラーの出力を見やすくする

先のエラーの出力、個数や価格の列の幅が短いので醜いですね。

考えついたのは、エラーのテキストを一緒にして1行すべてを使って表示しては。ということで、エラーを加工が必要です。Laravelにはしっかりそのための関数もあるのです。

ItemRequest.phpに以下のように、failedValidation()なる関数を追加します。


namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Validation\ValidationException;

class ItemsRequest extends FormRequest
{
...
    /**
     * Override the parent function
     *
     * @param  \Illuminate\Contracts\Validation\Validator  $validator
     * @return void
     *
     * @throws \Illuminate\Validation\ValidationException
     */
    protected function failedValidation(Validator $validator)
    {
        $errorsPerRow = [];

        foreach ($validator->errors()->getMessages() as $field => $messages) {
            $i = explode('.', $field)[1] ?? null; // 例えば、name.1なら1を取り出す

            $errorsPerRow['row.'.$i][] = $messages[0]; // 各行でのすべてのエラーを配列に
        }

        foreach ($errorsPerRow as $key => $messages) {
            $validator->errors()->add($key, implode(' ', $messages)); // 各行でのエラーを1つにまとめる
        }

        throw (new ValidationException($validator))
            ->errorBag($this->errorBag)
            ->redirectTo($this->getRedirectUrl());
    }
}

上の関数は、バリデーションが失敗したときにコールされる関数で、関数内で、各行のすべての項目のエラーを合わせて、row.0などのようなキーにエラーメッセージを割り当てて、新たなエラーを作成します。

そしてそれを、ブレードで以下のように出力できるようにします。

...
        <form method="POST" action="{{ route('form2.store') }}" class="form-horizontal" novalidate="">
          @csrf
          <div class="card-body">
            @error('names')
              <div class="invalid-feedback d-block">{{ $message }}</div>
            @enderror
            <div class="table-responsive">
              <table class="table table-bordered table-hover table-sm">
                <thead>
                  <tr>
                    <th>アイテム名</th>
                    <th class="col-2">価格</th>
                    <th class="col-1">個数</th>
                  </tr>
                </thead>
                <tbody>

                  @for ($i = 0; $i < 3; $i++)
                    <tr>
                      <td>
                        <input class="form-control" maxlength="255" name="names[]" type="text" value="{{ old('names.'.$i) }}">
                       </td>
                      <td>
                        <input class="form-control" maxlength="10" name="prices[]" type="text" value="{{ old('prices.'.$i) }}">
                       </td>
                      <td>
                        <input class="form-control" maxlength="5" name="quantities[]" type="text" value="{{ old('quantities.'.$i) }}">
                       </td>
                    </tr>
                    @error('row.'.$i)
                    <tr>
                      <td colspan="3">
                        <span class="invalid-feedback d-block">{{ $message }}</span>
                      </td>
                    </tr>
                    @enderror
                  @endfor
                </tbody>
              </table>
            </div>
          </div>

          <div class="card-footer">
            <button class="btn btn-primary float-right mr-2" type="submit">保存</button>
          </div>
        </form>
...

各項目のエラー表示を削除して、 @error('row.'.$i)で行ごとのエラーの表示としています。

結果は以下のように表示が改善されたエラー出力となります。

最後に

いろいろな形態の入力フォームがあるなか、前回も今回も追加専用のフォームの話でしたが、もちろんコードを変更すれば編集のフォームともなります。

By khino