Laravelでフォーム送信後にフォームページに戻ってしまう原因と解決策
現在、Laravelを使用して「おすすめの教材」と「自分が作ったオリジナルプロダクト(オリプロ)」を共有するサイトを制作しています。その過程で、フォーム送信後に意図したページではなく、再びフォームページに戻ってしまう問題に遭遇しました。この問題の原因と解決策について、記事としてまとめていこうと思います。
実行環境
実行環境はphp Apacheコンテナとmysqlを使用したdbコンテナを使用しています。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="//demo.productionready.io/main.css" />
<link rel="stylesheet" href="{{ asset('css/material_index.css') }}">
<link rel="stylesheet" href="{{ asset('css/post_material.css') }}">
<link rel="stylesheet" href="{{ asset('css/menu-select.css') }}">
<title>教材投稿ページ</title>
</head>
<body>
<div class="post-material-item">
<form action="{{ route('materials.store') }}" method="POST" enctype="multipart/form-data" id="post-material">
@csrf
<div class="layout-top">
<a href="{{ route('materials.index') }}" class="back">←</a>
<button class="submit" id="form-submit">投稿</button>
</div>
<div class="material-flex-container">
<div class="post-material-img">
<label for="image" class="post-material-image-label">
<img class="material-book-sample-image" id="material-book-sample-image" src="{{ session('material_image', asset('assets/images/sample_material_image.jpg')) }}" alt="" >
<p>カバー画像を変更</p>
</label>
<input class="post-material-img-upload custom-file-input" type="file" id="image" name="material-image" accept="" >
<div class="input-error">
<p class="error-img-message" id="image-error">画像を選択してください。</p>
</div>
</div>
<div class="post-material-title-review-container">
<div class="post-material-title">
<input class="post-material-title-text" name="material-title" type="text" class="" placeholder="教材タイトル" value="{{ old('material-title') }}" required />
@error('material-title')
<p class="error-message">{{ $message }}</p>
@enderror
</div>
<div class="post-material-thoughts">
<textarea
name="material-thoughts"
class="post-material-thoughts-text"
rows="8"
placeholder="教材の感想を入力"
required
></textarea>
</div>
</div>
</div>
<div class="post-material-rate-text">
<label for="post-material-rate-text">評価</label>
<div class="post-material-rate rate-form">
<input id="star5" type="radio" name="material-rate" value="5" required>
<label for="star5" class="star">★</label>
<input id="star4" type="radio" name="material-rate" value="4">
<label for="star4" class="star">★</label>
<input id="star3" type="radio" name="material-rate" value="3">
<label for="star3" class="star">★</label>
<input id="star2" type="radio" name="material-rate" value="2">
<label for="star2" class="star">★</label>
<input id="star1" type="radio" name="material-rate" value="1">
<label for="star1" class="star">★</label>
</div>
</div>
<div class="post-material-price">
<label for="material_price">価格</label>
<input
id = "material_price"
class="post-material-price-text"
type="number"
name="material-price"
placeholder="金額を入力"
min="0"
step="1"
oninput="this.value = this.value.replace(/^0+/, '');"
/>
</div>
<div class="post-material-url">
<label for="url">URL</label>
<input type="url" id="url" name="material-url">
</div>
<div class="post-material-tags" id="post-material-tags">
<p>タグ設定(5つまで)</p>
<select name="select1" id="select1" class="post-material-tags-select" required>
<option value="">選択してください</option>
<option value="1">Ruby</option>
<option value="2">PHP</option>
<option value="3">SQL</option>
<option value="4">HTML</option>
<option value="5">CSS</option>
<option value="6">JavaScript</option>
<option value="7">GitHub</option>
<option value="8">Linux</option>
<option value="9">docker</option>
<option value="10">AWS</option>
<option value="11">その他</option>
</select>
</div>
</form>
<script src="{{ asset('/js/post_material.js') }}"></script>
</div>
コントローラー
public function store(MaterialRequest $request)
{
// バリデーションを実行してダメなら投稿フォームにリダイレクト、成功したらバリデーション後のデータが配列として渡される
$validated = $request->validated();
// 教材を保存するためインスタンスを作成
$material = new Material();
//画像をstorageに保存する
if ($request->hasFile('material-image')) { //画像が投稿されていたら
//storage/app/public/material_images に保存
$path = $request->file('material-image')->store('material_images', 'public');
// パスをimage_dirにくれてやる
$material->image_dir = '/storage/' . $path;
}
//インスタンスに値をセット
$material->title = $validated['material-title'];
$material->material_detail = $validated['material-thoughts'];
$material->rating_id = $validated['material-rate'];
$material->price = $validated['material-price'];
$material->material_url = $validated['material-url'];
//教材テーブルに保管する
$material->save();
// 保存した時の主キーを取得
$materialId = $material->id;
//ここでテクノロジータグテーブルにデータを保存します
$materialTechnologieTag = new Material_technologie_tag();
$materialTechnologieTag->material_id = $materialId;
$selectedTechnologieTags = [];
for ($i = self::FIRST_SELECT_INDEX; $i <= self::LAST_SELECT_INDEX; $i++) {
$selectName = "select$i";
if ($request->$selectName) {
$selectedTechnologieTags[] = $request->$selectName;
}
}
$uniqueSelectedTechnologieTags = array_unique($selectedTechnologieTags);
foreach ($uniqueSelectedTechnologieTags as $uniqueSelectedTechnologieTag) {
$materialTechnologieTag = new Material_technologie_tag();
$materialTechnologieTag->material_id = $materialId;
$materialTechnologieTag->technologie_id = $uniqueSelectedTechnologieTag;
$materialTechnologieTag->save();
}
// 教材ポストテーブルに保存
$materialPost = new Material_post();
$materialPost->material_id = $materialId;
$materialPost->posted_user_id = Auth::user()->id;
$materialPost->save();
return view('materials.index');
}
フォームリクエストクラス
public function rules(): array
{
return [
'material-image' => 'required|image|mimes:jpeg,png,jpg,gif|max:2048',
'material_title' => 'required',
'material-thoughts' => 'required',
'material-rate' => 'required',
'material-price' => 'required',
'material-url' => 'required',
'select1' => 'required|integer',
];
}
コードは上記になります。ChatGPTに提案してもらいながら進めていたら再びフォームに戻ってしまう問題に遭遇しました。ChatGPTやGeminiなどに聞いても問題は解決しませんでした。
ですが、dd関数などを使用して、どこまでは問題なく動いているか確認しながら進めていき、問題にたどり着くことができました。
原因
原因はフォームリクエストクラスでした。
public function rules(): array
{
return [
'material-image' => 'required|image|mimes:jpeg,png,jpg,gif|max:2048',
'material_title' => 'required', //htmlのname属性はmaterial-titleですが、material_titleになっている
'material-thoughts' => 'required',
'material-rate' => 'required',
'material-price' => 'required',
'material-url' => 'required',
'select1' => 'required|integer',
];
}
原因の調べる手順
フォーム送信後、何をしても入力画面に戻ってしまう現象が発生しました。そこで、まずは問題の原因を特定するために調査を進めました。
1. storeメソッドが実行されているか確認
まず、storeメソッドに問題があるかを確認するため、一時的にstoreメソッドが実行されないようにしました。その結果、フォーム画面に戻ることはありませんでした。
このことから、問題はstoreメソッド以降にあると考えました。
2. リクエストコントローラーを疑う
次に、リクエストコントローラーが原因ではないかと考え、以下を試しました
リクエストコントローラーを使用せず、storeメソッド内で直接バリデーションを実行しました。
ですが、この場合でもフォーム画面に戻ってしまいました。このことから、リクエストコントローラー自体に問題があるわけではないと判断しました。
3. バリデーション後の処理を確認
バリデーション自体に問題がないと仮定し、バリデーション通過後にdd関数を追加して実行を確認しました。リクエストコントローラーあり・なしの両方で試しましたが、dd関数が実行されませんでした。
このことから、バリデーションそのものに問題があると考え、バリデーションコードを見直しました。
4. バリデーションコードの再確認
コードを見返した結果、バリデーションで期待しているキー名material_titleと、フォームから送信されるデータのキー名(htmlの名前属性)がmaterial-titleが一致していないことに気が付きました。この不一致が原因でバリデーションが失敗し、フォーム画面に戻ってしまっていたようです。
5. エラーバックの仕組みを理解
後から知ったことですが、Laravelには「エラーバック」という機能があります。バリデーションに失敗した場合、元のフォーム画面にリダイレクトされ、エラー情報がセッションに保存されます。今回のケースでは、material_titleのキー名不一致が原因でバリデーションに失敗し、このエラーバック機能が動作していました。
まとめ
バリデーションの内容についてはChatGPTを使用してコードを書きました。GPTをしようするメリットとして細かいミスがないということがあると思っていました。(特にSQLなど) ですが、今回はGPTの生成したコードがエラーの原因でした。決まったコードを出力する機械がミスすることはないでしょうが、生成AI系だとこのようなエラーが起きることもあると勉強になりました。
Discussion