前回は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)
で行ごとのエラーの表示としています。
結果は以下のように表示が改善されたエラー出力となります。
最後に
いろいろな形態の入力フォームがあるなか、前回も今回も追加専用のフォームの話でしたが、もちろんコードを変更すれば編集のフォームともなります。
メルマガ購読の申し込みはこちらから。