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


