Laravelの商品登録機能を実装する方法|バリデーション・FormRequest・Bladeを初心者向けに解説
Laravelで商品一覧や詳細画面を作れるようになってきたら、次に挑戦したいのが商品登録機能です。
ただ、実際に商品登録を実装しようとすると、フォームを作るだけでは終わりません。 入力内容のチェック、データベースへの保存、エラーメッセージの表示など、いくつもの処理を組み合わせる必要があります。
「Controllerのstore()って何をしているの?」 「バリデーションに失敗したら、どうして入力画面に戻るの?」 「$fillableを設定しないとエラーになるのはなぜ?」
学習を進めていると、こうした疑問が出てくることもあります。 私もLaravelのコードを一つずつ追いながら、各処理の役割を理解することが大切だと感じています。
今回は、Laravelの商品登録フォームとバリデーションについて、実装の流れからコードの意味、よくあるエラーの解決方法まで解説します。 商品テーブルとカテゴリテーブルを使ったCRUD機能の学習にもつながる内容です。
Laravelの商品登録機能とは?まずは全体の流れを理解しよう
今回の目標は、商品テーブルとカテゴリテーブルを設計し、商品一覧・詳細・登録機能を実装することです。 そのなかでも、今回は商品を登録する処理に注目します。
例えば、オンラインショップの商品管理画面をイメージしてみてください。 管理者が商品名や価格、カテゴリを入力して登録ボタンを押すと、その情報がデータベースに保存されます。
しかし、商品名が空欄だったり、価格に文字を入力したりした場合、そのまま保存してしまうわけにはいきません。 入力内容をチェックして、問題がなければ保存する仕組みが必要です。
商品登録フォームからデータベース保存までの処理
Laravelの商品登録機能は、次のような流れで動きます。
- 商品登録画面を表示する
- 商品名・価格・カテゴリなどを入力する
- 登録ボタンを押してフォームを送信する
- Controllerのstore()メソッドでバリデーションを実行する
- 入力内容に問題がなければデータベースに保存する
- 商品一覧画面にリダイレクトする
一方、入力内容に問題があった場合は、次のようになります。
- バリデーションでエラーを検出する
- 商品登録画面に戻る
- エラーメッセージを表示する
- 入力済みの内容を可能な範囲で保持する
ここで押さえておきたいのが、フォームを表示する処理と、フォームから送られたデータを保存する処理は別々だという点です。
Laravelでは、主にControllerのcreate()メソッドがフォームの表示を担当し、store()メソッドが登録処理を担当します。
Laravelの商品登録に必要なテーブル設計
商品を登録する前に、保存先となるデータベースの構造を理解しておきましょう。 今回はcategoriesテーブルとproductsテーブルの2つを使用します。
categoriesテーブルのマイグレーション
categoriesテーブルには、商品のカテゴリ情報を保存します。 例えば「食品」「家電」「書籍」といった分類を管理するためのテーブルです。
Schema::create('categories', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->timestamps();
});
それぞれの処理を確認してみましょう。
id():カテゴリを識別するための主キーを作成するstring('name'):カテゴリ名を保存する文字列カラムを作成するtimestamps():作成日時と更新日時を保存するカラムを作成する
productsテーブルのマイグレーション
次に、商品情報を保存するproductsテーブルを作成します。
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->foreignId('category_id')->constrained();
$table->string('name');
$table->integer('price');
$table->text('description')->nullable();
$table->timestamps();
});
ここで重要なのが、次のコードです。
$table->foreignId('category_id')->constrained();
このコードは、カテゴリと商品を関連付けるための外部キーを定義しています。
Laravelの標準的な命名規則では、category_idはcategoriesテーブルのidを参照します。
例えば、カテゴリIDが3の商品なら、productsテーブルのcategory_idに3が保存されます。 これにより、商品がどのカテゴリに属しているのかを管理できるようになります。
商品テーブルの主なカラムは次のとおりです。
id:商品を識別するIDcategory_id:商品が属するカテゴリのIDname:商品名price:商品の価格description:商品の説明created_at:登録日時updated_at:更新日時
なお、nullable()を指定したdescriptionは、値を省略できるカラムです。
商品名や価格など、必須にしたい項目とは扱いが異なります。
また、外部キーによる整合性を保つため、商品を登録するときは、参照先となるカテゴリが存在している必要があります。
Laravelのルーティング設定|Route::resource()を使う
テーブルの構造を理解したら、次は商品登録画面と登録処理を呼び出すためのルーティングを設定します。
Laravelでは、複数のCRUDルートをまとめて定義できるRoute::resource()が便利です。
use App\Http\Controllers\ProductController;
Route::resource('products', ProductController::class);
この設定によって、商品一覧・新規登録画面・登録処理・詳細画面・編集画面・更新処理・削除処理などに対応するルートが定義されます。
商品登録に関係する2つのルート
| HTTPメソッド | URL | Controllerのメソッド | 役割 |
|---|---|---|---|
| GET | /products/create | create() | 商品登録フォームを表示する |
| POST | /products | store() | 送信された商品データを登録する |
GETは画面の表示などに使用し、POSTはフォームから送信されたデータの登録などに使用します。
つまり、ブラウザで商品登録画面を開いたときはcreate()が呼び出され、登録ボタンを押したときはstore()が呼び出される、という仕組みです。
なお、実際に定義されたルートは、ターミナルで次のコマンドを実行すると確認できます。
php artisan route:list
ルーティングがうまく動かないときは、URLだけで判断せず、登録されているルートとHTTPメソッドを確認する習慣をつけておくと便利です。
LaravelのProductControllerで商品登録を実装する
ここからは、商品登録機能の中心となるProductControllerを見ていきます。 フォーム表示とデータ保存を担当する2つのメソッドを実装しましょう。
create()メソッドで商品登録フォームを表示する
public function create()
{
$categories = Category::all();
return view('products.create', compact('categories'));
}
この処理では、まずCategoryモデルを通じてカテゴリ情報を取得しています。
なぜカテゴリ情報が必要なのでしょうか。 商品登録画面でカテゴリを選択できるようにするためです。
例えば、商品登録フォームに「食品」「家電」「書籍」という選択肢を表示したい場合、データベースからカテゴリ情報を取得してBladeに渡します。
compact('categories')は、変数categoriesをビューに渡すためのPHPの関数です。
ビュー側では、$categoriesとして利用できます。
なお、このコードを動かすには、Controllerの先頭でCategoryモデルをインポートしておく必要があります。
use App\Models\Category;
store()メソッドで商品データを保存する
続いて、フォームから送信されたデータを処理するstore()メソッドです。 今回の実装では、バリデーション・保存・リダイレクトという3段階に分けて考えます。
use App\Http\Controllers\Controller;
use App\Models\Product;
use Illuminate\Http\Request;
public function store(Request $request)
{
// 1. 入力内容をチェックする
$validated = $request->validate([
'name' => 'required|string|max:255',
'price' => 'required|integer|min:0',
'category_id' => 'required|exists:categories,id',
'description' => 'nullable|string|max:1000',
]);
// 2. 商品データを保存する
Product::create($validated);
// 3. 商品一覧画面へ移動する
return redirect()
->route('products.index')
->with('success', '商品を登録しました');
}
このコードで重要なのは、入力されたデータをいきなり保存していないことです。 先にバリデーションを実行し、条件を満たしたデータだけを保存しています。
バリデーションとは?入力データをチェックする仕組み
バリデーションとは、フォームから送信されたデータが、決められた条件を満たしているか確認する処理です。
例えば、商品登録フォームで次のような入力があったとします。
- 商品名を入力せずに登録ボタンを押す
- 価格にマイナスの数値を入力する
- 存在しないカテゴリIDを送信する
- 商品説明に長すぎる文章を入力する
こうした入力をチェックせずに受け入れると、データの整合性が崩れたり、想定外の値が保存されたりする可能性があります。 バリデーションは、こうした問題を防ぐための重要な仕組みです。
Laravelのバリデーションルール一覧
| ルール | 意味 | 今回の使用例 |
|---|---|---|
| required | 必須項目にする | 商品名・価格 |
| string | 文字列であることを確認する | 商品名 |
| integer | 整数であることを確認する | 価格 |
| max:255 | 文字列などの最大値を指定する | 商品名は最大255文字 |
| min:0 | 数値などの最小値を指定する | 価格は0以上 |
| exists:categories,id | 指定した値が対象テーブルに存在するか確認する | カテゴリIDの確認 |
| nullable | 値がnullまたは空の場合を許容する | 商品説明は任意 |
ここで、特に注目したいのがexists:categories,idです。
フォームの選択肢にカテゴリを表示していても、送信された値が必ず正しいとは限りません。 ブラウザから送信されるデータは、利用者が変更できる可能性があります。
そのため、カテゴリIDがcategoriesテーブルのidに実際に存在するかどうかを、サーバー側でも確認する必要があります。
また、integer|min:0は、価格が整数で0以上であることを確認するルールです。
ただし、0円の商品を認めない仕様なら、最小値を1に変更するなど、アプリケーションの要件に合わせて設定します。
validate()が失敗するとどうなる?
Laravelの$request->validate()には、バリデーションに失敗した場合の処理が用意されています。
入力内容が条件を満たしていなければ、通常のWebフォームでは、Laravelが前の画面にリダイレクトし、エラー情報と入力値をセッション経由で利用できるようにします。
そのため、単純なフォームであれば、エラー時のリダイレクト処理を自分で一から書く必要はありません。
一方、バリデーションに成功した場合は、チェック済みのデータが$validatedに格納されます。
そのデータを使って商品を保存する、という流れです。
LaravelのModelに$fillableを設定する理由
バリデーションを実装したら、データベースへの保存処理も確認しましょう。
ここで初心者がつまずきやすいのが、Modelの$fillableです。
Productモデルの実装例
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Product extends Model
{
protected $fillable = [
'category_id',
'name',
'price',
'description',
];
public function category(): BelongsTo
{
return $this->belongsTo(Category::class);
}
}
Product::create($validated)は、受け取った配列のデータを使って商品を作成・保存する処理です。
このように複数の属性をまとめて代入する仕組みを、Mass Assignment(マスアサインメント)と呼びます。
Laravelでは、意図しない項目まで一括登録されることを防ぐため、まとめて代入できる属性を制御します。
そのために使用するのが$fillableです。
今回の例では、category_id・name・price・descriptionの4つを登録可能な属性として指定しています。
この設定が適切でないと、環境や設定によってはMassAssignmentExceptionが発生したり、意図した属性が代入されなかったりすることがあります。
ただし、$fillableに指定すれば何でも安全になるわけではありません。
フォームから送られた値をそのまま信用するのではなく、バリデーションで必要な項目と値をチェックしたうえで保存することが大切です。
belongsTo()で商品とカテゴリを関連付ける
Productモデルには、次のリレーションも定義しています。
public function category(): BelongsTo
{
return $this->belongsTo(Category::class);
}
これは、商品が1つのカテゴリに属することを表すEloquentのリレーションです。 商品テーブルのcategory_idを使って、関連するカテゴリを取得できるようになります。
例えば、商品詳細画面でカテゴリ名を表示したい場合は、商品モデルから関連するカテゴリを参照できます。
{{ $product->category->name }}
ただし、関連するカテゴリが存在しない場合の扱いには注意が必要です。 今回のようにカテゴリの存在をバリデーションで確認し、外部キー制約も適切に設定することで、データの整合性を保ちやすくなります。
Laravel Bladeで商品登録フォームを作成する
Controllerの処理ができたら、次は利用者が実際に操作する商品登録フォームを作成します。 Laravelでは、Bladeテンプレートを使ってHTMLにPHPのデータや処理を組み込めます。
今回は、商品名・価格・カテゴリ・商品説明を入力できるフォームを作成します。
商品登録フォームのBladeコード
<form action="{{ route('products.store') }}" method="POST">
@csrf
<div>
<label for="name">商品名</label>
<input
type="text"
id="name"
name="name"
value="{{ old('name') }}"
>
@error('name')
<p class="text-red-500">{{ $message }}</p>
@enderror
</div>
<div>
<label for="price">価格</label>
<input
type="number"
id="price"
name="price"
value="{{ old('price') }}"
>
@error('price')
<p class="text-red-500">{{ $message }}</p>
@enderror
</div>
<div>
<label for="category_id">カテゴリ</label>
<select id="category_id" name="category_id">
<option value="">選択してください</option>
@foreach ($categories as $category)
<option
value="{{ $category->id }}"
@selected(old('category_id') == $category->id)
>
{{ $category->name }}
</option>
@endforeach
</select>
@error('category_id')
<p class="text-red-500">{{ $message }}</p>
@enderror
</div>
<div>
<label for="description">商品説明</label>
<textarea
id="description"
name="description"
>{{ old('description') }}</textarea>
@error('description')
<p class="text-red-500">{{ $message }}</p>
@enderror
</div>
<button type="submit">登録</button>
</form>
コードは少し長く見えますが、重要なポイントを分けて理解すれば、それほど難しくありません。
@csrfはなぜ必要?419エラーとの関係
まず、フォームの中にある@csrfを確認してみましょう。
@csrf
これは、LaravelがCSRF(クロスサイトリクエストフォージェリ)対策に使用するトークンをフォームに埋め込むためのBladeディレクティブです。
CSRFは、利用者が意図していないリクエストを送信させられる攻撃の一種です。 Laravelでは、通常のWebフォームから送信されるPOSTリクエストなどに対してCSRFトークンを検証します。
そのため、必要なトークンが送信されない場合、419 Page Expiredなどのエラーが発生することがあります。
フォームを作成するときは、POSTなどの対象リクエストに対してCSRF対策が正しく設定されているか確認しましょう。
old()で入力した値を保持する
次に注目したいのが、old('name')です。
<input type="text" name="name" value="{{ old('name') }}">
例えば、商品名に「ワイヤレスイヤホン」、価格に「3000」と入力して登録ボタンを押したとします。 ところが、カテゴリを選択していなかったため、バリデーションエラーになりました。
このとき、入力した商品名や価格がすべて消えてしまったら、もう一度入力し直さなければなりません。 項目が多いフォームでは、かなり面倒ですよね。
old()を使うと、バリデーションに失敗してフォームへ戻ったときに、直前に送信した値を再表示できます。
利用者が最初から入力し直す手間を減らせるわけです。
カテゴリの選択状態も、@selected()とold('category_id')を組み合わせて復元できます。
@errorでバリデーションエラーを表示する
例えば、商品名が未入力だった場合にエラーメッセージを表示したいときは、次のように記述します。
@error('name')
<p>{{ $message }}</p>
@enderror
@error('name')は、name項目にエラーがある場合に、その中のHTMLを表示するためのBladeディレクティブです。
$messageには、対応するエラーメッセージが入ります。
価格なら@error('price')、カテゴリなら@error('category_id')というように、フォームのname属性に対応する項目名を指定します。
エラーが表示されないときは、バリデーションルールの項目名とBlade側の指定が一致しているか確認しましょう。
formのactionとmethodの意味
フォームの先頭にあるコードも重要です。
<form action="{{ route('products.store') }}" method="POST">
actionには、フォームの送信先を指定します。
今回はroute('products.store')を使用し、商品登録処理に対応するルートを指定しています。
method="POST"は、フォームデータをPOSTリクエストとして送信するための指定です。
URLを直接文字列で書くのではなく、名前付きルートを使用しておくと、ルーティングの変更にも対応しやすくなります。
LaravelのFormRequestでバリデーションを分離する方法
商品登録の基本的な仕組みが理解できたら、次はFormRequestを使った書き方にも挑戦してみましょう。
最初はController内にバリデーションを書く方法で十分ですが、入力項目が増えると、登録処理と入力チェックのコードが長くなってしまいます。
そこで便利なのがFormRequestです。 バリデーションのルールや認可処理などを専用のクラスに分けることで、Controllerを整理できます。
artisanコマンドでFormRequestを作成する
ターミナルで次のコマンドを実行します。
php artisan make:request StoreProductRequest
実行すると、通常はapp/Http/RequestsディレクトリにStoreProductRequestクラスが作成されます。
StoreProductRequestにバリデーションルールを書く
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreProductRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'name' => 'required|string|max:255',
'price' => 'required|integer|min:0',
'category_id' => 'required|exists:categories,id',
'description' => 'nullable|string|max:1000',
];
}
public function messages(): array
{
return [
'name.required' => '商品名は必須です。',
'price.required' => '価格を入力してください。',
'price.integer' => '価格は整数で入力してください。',
'category_id.exists' => '選択したカテゴリが存在しません。',
];
}
}
rules()にはバリデーションルールを、messages()には独自のエラーメッセージを記述します。
これにより、例えば商品名が空欄の場合に「商品名は必須です。」というメッセージを表示できます。
なお、authorize()は、そのリクエストを実行する権限があるかどうかを判定するためのメソッドです。
今回は認可処理を別途設けていない学習用の例としてtrueを返していますが、実際の管理画面などでは、利用者の権限に応じた認可処理を検討する必要があります。
ControllerでFormRequestを利用する
FormRequestを作成したら、Controller側では次のように利用できます。
use App\Http\Requests\StoreProductRequest;
use App\Models\Product;
public function store(StoreProductRequest $request)
{
Product::create($request->validated());
return redirect()
->route('products.index')
->with('success', '商品を登録しました');
}
ここでは、通常のRequestではなくStoreProductRequestを引数に指定しています。
LaravelはControllerのメソッドが実行される前にFormRequestのバリデーションを行います。
条件を満たしていれば、validated()でチェック済みのデータを取得できます。
つまり、Controllerでは保存処理に集中できるようになります。 バリデーションが増えてきたときや、同じルールを整理したいときに便利な方法です。
なお、通常のRequestを使う方法とFormRequestを使う方法は、どちらか一方を選べば問題ありません。 最初から複雑に考えすぎず、基本の動きを理解してからFormRequestに進むとスムーズです。
Laravelの商品登録でよくあるエラーと解決方法
コードを書いていると、思ったとおりに動かない場面も出てきます。 そんなときは、エラーメッセージだけでなく、どの処理で問題が起きているのかを順番に確認することが大切です。
1. 419 Page Expiredが表示される
POSTリクエストを送信したときに419エラーが表示される場合は、CSRFトークンが正しく送信されているか確認しましょう。
- Bladeのフォーム内に
@csrfがあるか確認する - フォームが正しいルートに送信されているか確認する
- セッションやCookieの状態を確認する
CSRFトークンが原因とは限らないため、上記を確認しても解決しない場合は、セッションの有効期限やブラウザ側の状態なども調べてみましょう。
2. MassAssignmentExceptionが発生する
Product::create()を実行したときにこのエラーが発生したら、Productモデルの$fillableを確認します。
- 保存するカラムが
$fillableに指定されているか - 配列のキーとデータベースのカラム名が一致しているか
- 意図しない属性まで登録対象にしていないか
エラーを回避することだけを目的に、すべての属性を無条件で代入可能にするのは避けましょう。 保存してよい属性を明示し、バリデーションと組み合わせて管理することが大切です。
3. バリデーションエラーが表示されない
バリデーションが動いているのにメッセージが表示されない場合は、Blade側の記述を確認します。
例えば、Controllerではnameという項目をチェックしているのに、Bladeで@error('product_name')と書いていたら、名前が一致していません。
バリデーションルール、フォームのname属性、@errorの項目名が一致しているか確認しましょう。
4. カテゴリの選択肢が表示されない
カテゴリの選択肢が表示されない場合は、create()メソッドでカテゴリ情報を取得しているか確認します。
- Categoryモデルを正しくインポートしているか
- categoriesテーブルにデータが登録されているか
compact('categories')でビューに渡しているか- Blade側で
@foreach ($categories as $category)を正しく記述しているか
カテゴリテーブルが空の場合、コードが正しくても選択肢は表示されません。 そのため、テーブルの構造だけでなく、実際のデータも確認してみましょう。
5. 商品登録後に一覧画面へ移動できない
登録処理が終わっても一覧画面に移動できない場合は、ルート名を確認します。
return redirect()->route('products.index');
Route::resource('products', ProductController::class)を設定していれば、通常はproducts.indexという名前付きルートが利用できます。
ただし、独自にルートを変更している場合は、実際のルート名が異なることもあります。
php artisan route:listを実行して、ルート名を確認しましょう。
Laravelの商品登録機能を実装したら確認したいテスト項目
商品登録のコードを書き終えたら、登録ボタンを押して一度成功することを確認するだけでは不十分です。 正しい入力と間違った入力の両方を試して、想定どおりに動作するか確認しましょう。
商品登録フォームの動作確認チェックリスト
- 商品登録画面が正常に表示される
- カテゴリの選択肢が表示される
- 商品名と価格を入力して登録できる
- 登録した商品がデータベースに保存される
- 登録後に商品一覧画面へ移動できる
- 商品名が空欄の場合にエラーが表示される
- 価格が整数ではない場合にエラーが表示される
- 価格が負の数の場合にエラーが表示される
- 存在しないカテゴリIDが拒否される
- エラー時に入力済みの値が保持される
- 商品説明を省略しても登録できる
特に、バリデーションで失敗した場合にデータが保存されていないか、という点は重要です。 正常なケースだけでなく、異常なケースも確認することで、より信頼できる商品登録機能に近づきます。
まとめ|Laravelの商品登録はフォーム・バリデーション・保存処理をつなげて理解しよう
今回は、Laravelの商品登録フォームとバリデーションについて解説しました。
商品登録機能は、単にデータベースへ情報を書き込むだけの処理ではありません。 フォームの表示、入力内容のチェック、モデルを通じた保存、エラー時の対応まで、一連の流れを組み合わせて作ります。
最後に、今回の重要ポイントを整理しておきましょう。
- create():商品登録フォームを表示する
- store():フォームから送信されたデータを処理する
- validate():入力内容がルールを満たしているか確認する
- exists:カテゴリIDが実際に存在するか確認する
- $fillable:一括代入を許可する属性を指定する
- @csrf:フォームにCSRFトークンを埋め込む
- old():バリデーションエラー後に入力値を再表示する
- @error:項目ごとのエラーメッセージを表示する
- FormRequest:バリデーションのルールを専用クラスに分離する
最初はコードの量が多く感じるかもしれません。 そんなときは、すべてを一度に覚えようとせず、フォーム表示・入力チェック・データ保存の3つに分けて考えてみてください。
それぞれの役割がわかると、LaravelのControllerやModel、Bladeがどのようにつながっているのかも見えてきます。
商品登録機能を理解できたら、次は商品詳細画面でカテゴリ名を表示したり、商品編集機能でバリデーションを再利用したりするのもおすすめです。
一つずつ実装し、動作を確かめながら進めていきましょう。 実際に手を動かしてエラーを解決する経験が、Laravelを使った開発の理解につながっていきます。







0 件のコメント:
コメントを投稿