【第四弾】Laravel入門資料
- Windows環境構築
- Mac環境構築
- Laravelとは
- ルーティング
- ビュー
- コントローラ
- マイグレーション
- モデル
- ここまでの知識の実践
- 検索・フィルタ
- ソフトデリート
- ページネーション
- 画像アップロード
- 認証ライブラリ
- 認証実装
- ミドルウェア・ロール
- 総合実践
ここまでの知識の実践 - 商品管理アプリを作ろう
これまで学んだ知識を総動員して、実際に動く商品管理アプリケーションを作ります。
🎯 この実践で作るもの
- ✅ 商品の一覧表示
- ✅ 商品の詳細表示
- ✅ 商品の新規登録
- ✅ 商品の編集
- ✅ 商品の削除
- ✅ カテゴリーとのリレーション
- ✅ バリデーション
- ✅ フラッシュメッセージ
使用する技術スタック
- バックエンド: Laravel (ルーティング、コントローラ、モデル)
- データベース: MySQL
- フロントエンド: Blade テンプレート
- CSS: Tailwind CSS
Tailwind CSSとは
Tailwind CSSは、ユーティリティファーストのCSSフレームワークです。
通常のCSSのようにクラス名を考える必要がなく、bg-blue-500やtext-centerのような
小さなユーティリティクラスを組み合わせてデザインを構築します。
🎨 Tailwind CSSの特徴
- ✅ CDN経由で簡単に導入可能: HTMLファイルに1行追加するだけで使える
- ✅ 直感的なクラス名:
text-red-500、p-4、rounded-lgなど、見ればわかるクラス名 - ✅ レスポンシブデザインが簡単:
md:text-lgのようにプレフィックスで対応 - ✅ カスタマイズが容易: 設定ファイルで独自の色やサイズを定義可能
このアプリでのTailwind CSSの使い方
今回のアプリでは、各Bladeテンプレートの<head>タグ内で
CDN版のTailwind CSSを読み込んでいます:
<script src="https://cdn.tailwindcss.com"></script>
この1行を追加するだけで、Tailwind CSSの全てのユーティリティクラスがHTMLで使えるようになります。
💡 Tailwind CSSクラスの例
bg-gray-100- 灰色の背景text-3xl- 大きな文字サイズfont-bold- 太字px-4 py-8- 左右に16px、上下に32pxのパディングrounded-lg- 大きめの角丸hover:bg-blue-600- ホバー時に背景色を変更flex justify-between items-center- フレックスボックスで要素を両端配置、垂直中央揃え
Tailwind CSSを使った実際のHTML例
以下は、Tailwind CSSのクラスを使って、ボタンやカードをスタイリングする例です:
例1: ボタンのスタイリング
<!-- 青いボタン -->
<button class="bg-blue-500 text-white px-6 py-2 rounded-lg hover:bg-blue-600">
登録する
</button>
<!-- 赤いボタン -->
<button class="bg-red-500 text-white px-6 py-2 rounded-lg hover:bg-red-600">
削除する
</button>
<!-- グレーのボタン -->
<button class="bg-gray-500 text-white px-6 py-2 rounded-lg hover:bg-gray-600">
キャンセル
</button>
🎯 クラスの説明
bg-blue-500- 背景色を青にtext-white- 文字色を白にpx-6- 左右のパディングを24px (6 × 4px)py-2- 上下のパディングを8px (2 × 4px)rounded-lg- 角を丸くhover:bg-blue-600- ホバー時に背景色を濃い青に変更
例2: カードのレイアウト
<div class="bg-white p-6 rounded-lg shadow-lg">
<h2 class="text-2xl font-bold text-gray-800 mb-4">商品名</h2>
<p class="text-gray-600 mb-4">商品の説明文がここに入ります。</p>
<div class="flex justify-between items-center">
<span class="text-3xl font-bold text-blue-600">¥1,200</span>
<button class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">
購入する
</button>
</div>
</div>
🎯 クラスの説明
bg-white- 背景色を白にp-6- 全方向のパディングを24pxshadow-lg- 大きな影を追加text-2xl- 文字サイズを24pxにmb-4- 下マージンを16pxflex- フレックスボックスを有効化justify-between- 要素を両端に配置items-center- 要素を垂直方向に中央揃え
例3: フォームのスタイリング
<form class="space-y-4">
<div>
<label class="block text-gray-700 font-bold mb-2">商品名</label>
<input
type="text"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="商品名を入力"
>
</div>
<div>
<label class="block text-gray-700 font-bold mb-2">説明</label>
<textarea
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
rows="4"
placeholder="商品の説明を入力"
></textarea>
</div>
<button class="w-full bg-blue-500 text-white px-6 py-3 rounded-lg hover:bg-blue-600">
登録する
</button>
</form>
🎯 クラスの説明
space-y-4- 子要素間に16pxの縦方向の間隔を追加block- ブロック要素として表示w-full- 幅を100%にborder- ボーダーを追加border-gray-300- ボーダーの色をグレーにfocus:outline-none- フォーカス時のアウトラインを非表示focus:ring-2- フォーカス時に2pxのリングを表示focus:ring-blue-500- リングの色を青に
公式ドキュメント: https://tailwindcss.com/
【準備】モデルとマイグレーションの作成
まずは、モデルとマイグレーションを作成します。
💡 前提条件
- ✅ MySQLがインストール済み
- ✅
.envでデータベース接続設定済み - ✅ データベースGUIツール導入済み
- • Mac: Sequel Ace
- • Windows: A5M2
ステップ1: マイグレーションとモデルの作成
# カテゴリーモデル + マイグレーション
php artisan make:model Category -m
# 商品モデル + マイグレーション
php artisan make:model Product -m
ステップ2: マイグレーションファイルの編集
カテゴリーテーブル
database/migrations/xxxx_create_categories_table.php
public function up(): void
{
Schema::create('categories', function (Blueprint $table) {
$table->id();
$table->string('name')->comment('カテゴリー名');
$table->timestamps();
});
}
商品テーブル
database/migrations/xxxx_create_products_table.php
public function up(): void
{
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->string('name')->comment('商品名');
$table->text('description')->nullable()->comment('説明');
$table->integer('price')->unsigned()->comment('価格');
$table->integer('stock')->default(0)->comment('在庫数');
$table->boolean('is_published')->default(true)->comment('公開状態');
$table->foreignId('category_id')->constrained()->onDelete('cascade');
$table->timestamps();
});
}
ステップ3: モデルの設定
Categoryモデル
app/Models/Category.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Category extends Model
{
use HasFactory;
protected $fillable = ['name'];
// このカテゴリーに属する商品
public function products()
{
return $this->hasMany(Product::class);
}
}
Productモデル
app/Models/Product.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
use HasFactory;
protected $fillable = [
'name',
'description',
'price',
'stock',
'is_published',
'category_id',
];
// この商品が属するカテゴリー
public function category()
{
return $this->belongsTo(Category::class);
}
}
ステップ4: シーダーの作成
php artisan make:seeder CategorySeeder
php artisan make:seeder ProductSeeder
CategorySeeder
database/seeders/CategorySeeder.php
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
class CategorySeeder extends Seeder
{
public function run(): void
{
DB::table('categories')->insert([
['name' => '家電', 'created_at' => now(), 'updated_at' => now()],
['name' => '食品', 'created_at' => now(), 'updated_at' => now()],
['name' => '衣類', 'created_at' => now(), 'updated_at' => now()],
['name' => '書籍', 'created_at' => now(), 'updated_at' => now()],
]);
}
}
ProductSeeder
database/seeders/ProductSeeder.php
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
class ProductSeeder extends Seeder
{
public function run(): void
{
DB::table('products')->insert([
[
'name' => 'ノートPC',
'description' => '高性能なノートパソコン',
'price' => 120000,
'stock' => 10,
'is_published' => true,
'category_id' => 1,
'created_at' => now(),
'updated_at' => now(),
],
[
'name' => 'ワイヤレスマウス',
'description' => '快適な操作感',
'price' => 3000,
'stock' => 50,
'is_published' => true,
'category_id' => 1,
'created_at' => now(),
'updated_at' => now(),
],
[
'name' => 'オーガニックコーヒー',
'description' => '香り高いコーヒー豆',
'price' => 1500,
'stock' => 30,
'is_published' => true,
'category_id' => 2,
'created_at' => now(),
'updated_at' => now(),
],
]);
}
}
DatabaseSeederに登録
database/seeders/DatabaseSeeder.php
public function run(): void
{
$this->call([
CategorySeeder::class,
ProductSeeder::class,
]);
}
ステップ5: マイグレーション実行
# テーブル作成 + データ投入
php artisan migrate:fresh --seed
ステップ6: データベースで確認
GUIツールでテーブルとデータが作成されたか確認しましょう。
確認するテーブル:
- •
categoriesテーブル - 4件のデータ - •
productsテーブル - 3件のデータ
✅ データベースの準備が完了しました!
【実践1】商品一覧ページの作成 (Read)
商品の一覧を表示するページを作成します。
ステップ1: コントローラーの作成
php artisan make:controller ProductController --resource
💡 --resource オプションとは?
--resource オプションを付けると、リソースコントローラが作成されます。
自動生成されるメソッド:
index()- 一覧表示create()- 登録フォーム表示store()- 登録処理show()- 詳細表示edit()- 編集フォーム表示update()- 更新処理destroy()- 削除処理
これらのメソッドはCRUD操作の標準的なパターンに対応しており、
わざわざ手動で作成する手間が省けます。
ステップ2: ルーティングの設定
routes/web.php
use App\Http\Controllers\ProductController;
Route::resource('products', ProductController::class);
💡 Route::resource() とは?
Route::resource() は、1行でCRUD操作に必要な7つのルートを自動生成します。
通常であれば Route::get() や Route::post() を7回書く必要がありますが、
Route::resource() を使うと、わずか1行で全てのルートが設定されます。
自動生成されるルート一覧:
| メソッド | URI | アクション |
|---|---|---|
| GET | /products | index |
| GET | /products/create | create |
| POST | /products | store |
| GET | /products/{id} | show |
| GET | /products/{id}/edit | edit |
| PUT/PATCH | /products/{id} | update |
| DELETE | /products/{id} | destroy |
ステップ3: indexメソッドの実装
app/Http/Controllers/ProductController.php
<?php
namespace App\Http\Controllers;
use App\Models\Product;
use Illuminate\Http\Request;
class ProductController extends Controller
{
// 商品一覧
public function index()
{
// 公開されている商品を新しい順に取得(カテゴリー情報も一緒に)
$products = Product::with('category')
->where('is_published', true)
->latest()
->get();
return view('products.index', compact('products'));
}
}
ステップ4: ビューの作成
resources/views/products/index.blade.php を作成
# ディレクトリ作成
mkdir resources/views/products
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>商品一覧</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-100">
<div class="container mx-auto px-4 py-8">
<div class="flex justify-between items-center mb-6">
<h1 class="text-3xl font-bold text-gray-800">商品一覧</h1>
<a href="{{ route('products.create') }}"
class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded">
新規登録
</a>
</div>
@if (session('success'))
<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-4">
{{ session('success') }}
</div>
@endif
<div class="bg-white shadow-md rounded overflow-hidden">
<table class="min-w-full">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">ID</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">商品名</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">カテゴリー</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">価格</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">在庫</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">操作</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
@foreach ($products as $product)
<tr>
<td class="px-6 py-4">{{ $product->id }}</td>
<td class="px-6 py-4">{{ $product->name }}</td>
<td class="px-6 py-4">{{ $product->category->name }}</td>
<td class="px-6 py-4">¥{{ number_format($product->price) }}</td>
<td class="px-6 py-4">{{ $product->stock }}</td>
<td class="px-6 py-4 space-x-2">
<a href="{{ route('products.show', $product->id) }}"
class="text-blue-600 hover:underline">詳細</a>
<a href="{{ route('products.edit', $product->id) }}"
class="text-green-600 hover:underline">編集</a>
<form action="{{ route('products.destroy', $product->id) }}"
method="POST" class="inline">
@csrf
@method('DELETE')
<button type="submit"
class="text-red-600 hover:underline"
onclick="return confirm('本当に削除しますか?')">
削除
</button>
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</body>
</html>
💡 number_format() とは?
number_format() は、数値を3桁区切りでフォーマットするPHP関数です。
例: number_format(120000) → "120,000"
価格表示など、大きな数値を見やすくする時に使います。
ステップ5: 確認
ブラウザで /products にアクセスして確認します。
✅ 商品一覧ページが完成しました!
【実践2】商品詳細ページの作成 (Read)
商品の詳細情報を表示するページを作成します。
showメソッドの実装
app/Http/Controllers/ProductController.php
// 商品詳細
public function show($id)
{
// カテゴリー情報も一緒に取得
$product = Product::with('category')->findOrFail($id);
return view('products.show', compact('product'));
}
💡 findOrFail() とは?
find($id) は、IDが存在しない場合に null を返します。
findOrFail($id) は、IDが存在しない場合に 自動的に404エラーを返します。
メリット: わざわざ「nullチェック → 404返す」というコードを書く必要がありません。
詳細ページや編集ページなど、特定のIDが必須の場合に非常に便利です。
ビューの作成
resources/views/products/show.blade.php
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>商品詳細</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-100">
<div class="container mx-auto px-4 py-8">
<div class="mb-6">
<a href="{{ route('products.index') }}"
class="text-blue-600 hover:underline">← 一覧に戻る</a>
</div>
<div class="bg-white shadow-md rounded-lg p-6">
<h1 class="text-3xl font-bold text-gray-800 mb-4">{{ $product->name }}</h1>
<div class="space-y-3">
<div class="flex border-b pb-2">
<span class="font-semibold w-32">ID:</span>
<span>{{ $product->id }}</span>
</div>
<div class="flex border-b pb-2">
<span class="font-semibold w-32">カテゴリー:</span>
<span>{{ $product->category->name }}</span>
</div>
<div class="flex border-b pb-2">
<span class="font-semibold w-32">価格:</span>
<span class="text-xl text-red-600">¥{{ number_format($product->price) }}</span>
</div>
<div class="flex border-b pb-2">
<span class="font-semibold w-32">在庫:</span>
<span>{{ $product->stock }} 個</span>
</div>
<div class="flex border-b pb-2">
<span class="font-semibold w-32">公開状態:</span>
<span>
@if ($product->is_published)
<span class="bg-green-100 text-green-800 px-2 py-1 rounded text-sm">公開中</span>
@else
<span class="bg-gray-100 text-gray-800 px-2 py-1 rounded text-sm">非公開</span>
@endif
</span>
</div>
<div class="border-b pb-2">
<span class="font-semibold">説明:</span>
<p class="mt-2 text-gray-700">{{ $product->description ?? '説明はありません' }}</p>
</div>
<div class="flex border-b pb-2">
<span class="font-semibold w-32">登録日時:</span>
<span>{{ $product->created_at->format('Y年m月d日 H:i') }}</span>
</div>
<div class="flex border-b pb-2">
<span class="font-semibold w-32">更新日時:</span>
<span>{{ $product->updated_at->format('Y年m月d日 H:i') }}</span>
</div>
</div>
<div class="mt-6 space-x-3">
<a href="{{ route('products.edit', $product->id) }}"
class="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded">
編集
</a>
<form action="{{ route('products.destroy', $product->id) }}"
method="POST" class="inline">
@csrf
@method('DELETE')
<button type="submit"
class="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded"
onclick="return confirm('本当に削除しますか?')">
削除
</button>
</form>
</div>
</div>
</div>
</body>
</html>
✅ 商品詳細ページが完成しました!
【実践3】商品登録ページの作成 (Create)
新しい商品を登録するページを作成します。
ステップ1: createメソッドの実装
app/Http/Controllers/ProductController.php
// 商品登録フォーム表示
public function create()
{
// カテゴリー一覧を取得
$categories = Category::all();
return view('products.create', compact('categories'));
}
ステップ2: storeメソッドの実装
// 商品登録処理
public function store(Request $request)
{
// バリデーション
$validated = $request->validate([
'name' => 'required|max:255',
'description' => 'nullable',
'price' => 'required|integer|min:0',
'stock' => 'required|integer|min:0',
'is_published' => 'boolean',
'category_id' => 'required|exists:categories,id',
], [
'name.required' => '商品名は必須です',
'name.max' => '商品名は255文字以内で入力してください',
'price.required' => '価格は必須です',
'price.integer' => '価格は整数で入力してください',
'price.min' => '価格は0円以上で入力してください',
'stock.required' => '在庫数は必須です',
'stock.integer' => '在庫数は整数で入力してください',
'stock.min' => '在庫数は0以上で入力してください',
'category_id.required' => 'カテゴリーは必須です',
'category_id.exists' => '選択されたカテゴリーは存在しません',
]);
// 公開状態のデフォルト値
$validated['is_published'] = $request->has('is_published');
// 商品を作成
Product::create($validated);
// リダイレクト
return redirect()
->route('products.index')
->with('success', '商品を登録しました');
}
💡 with('success', 'メッセージ') とは?
with() は、フラッシュメッセージ(1回だけ表示されるメッセージ)をセッションに保存します。
リダイレクト先のビューで session('success') または @if (session('success')) を使って、
「商品を登録しました」などの成功メッセージを表示できます。
ページをリロードすると自動的に消えるので、ユーザー体験が向上します。
ステップ3: ビューの作成
resources/views/products/create.blade.php
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>商品登録</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-100">
<div class="container mx-auto px-4 py-8">
<div class="mb-6">
<a href="{{ route('products.index') }}"
class="text-blue-600 hover:underline">← 一覧に戻る</a>
</div>
<div class="bg-white shadow-md rounded-lg p-6 max-w-2xl">
<h1 class="text-3xl font-bold text-gray-800 mb-6">商品登録</h1>
<form action="{{ route('products.store') }}" method="POST">
@csrf
<div class="bg-blue-50 border border-blue-200 p-3 rounded mb-4">
<p class="text-xs text-gray-700 mb-1"><strong>💡 @csrf とは?</strong></p>
<p class="text-xs text-gray-600">
<code>@csrf</code> は <strong>CSRF(クロスサイトリクエストフォージェリ)攻撃</strong>から守るためのトークンです。<br>
Laravelでは、POST/PUT/DELETE リクエストを送信する全てのフォームに<strong>必須</strong>です。<br>
このトークンがないと、フォーム送信時に<strong>419エラー</strong>が発生します。
</p>
</div>
<!-- 商品名 -->
<div class="mb-4">
<label for="name" class="block text-sm font-medium text-gray-700 mb-2">
商品名 <span class="text-red-500">*</span>
</label>
<input type="text" name="name" id="name"
value="{{ old('name') }}"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
required>
<div class="bg-green-50 border border-green-200 p-2 rounded mt-2 text-xs">
<strong>💡 old('name') とは?</strong><br>
バリデーションエラーが発生した時、<strong>入力した値を保持</strong>してくれます。<br>
これがないと、エラー時に全ての入力内容が消えてしまいます。
</div>
@if($errors->has('name'))
<p class="text-red-500 text-sm mt-1">{{ $errors->first('name') }}</p>
@endif
<div class="bg-yellow-50 border border-yellow-200 p-2 rounded mt-2 text-xs">
<strong>💡 $errors->has('name') とは?</strong><br>
バリデーションエラーが発生した時のみ、エラーメッセージを表示します。<br>
<code>$errors->first('name')</code> には、コントローラで設定したエラーメッセージが入ります。
</div>
</div>
<!-- カテゴリー -->
<div class="mb-4">
<label for="category_id" class="block text-sm font-medium text-gray-700 mb-2">
カテゴリー <span class="text-red-500">*</span>
</label>
<select name="category_id" id="category_id"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
required>
<option value="">選択してください</option>
@foreach ($categories as $category)
<option value="{{ $category->id }}"
{{ old('category_id') == $category->id ? 'selected' : '' }}>
{{ $category->name }}
</option>
@endforeach
</select>
@if($errors->has('category_id'))
<p class="text-red-500 text-sm mt-1">{{ $errors->first('category_id') }}</p>
@endif
</div>
<!-- 価格 -->
<div class="mb-4">
<label for="price" class="block text-sm font-medium text-gray-700 mb-2">
価格(円) <span class="text-red-500">*</span>
</label>
<input type="number" name="price" id="price"
value="{{ old('price') }}"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
required min="0">
@if($errors->has('price'))
<p class="text-red-500 text-sm mt-1">{{ $errors->first('price') }}</p>
@endif
</div>
<!-- 在庫数 -->
<div class="mb-4">
<label for="stock" class="block text-sm font-medium text-gray-700 mb-2">
在庫数 <span class="text-red-500">*</span>
</label>
<input type="number" name="stock" id="stock"
value="{{ old('stock', 0) }}"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
required min="0">
@if($errors->has('stock'))
<p class="text-red-500 text-sm mt-1">{{ $errors->first('stock') }}</p>
@endif
</div>
<!-- 説明 -->
<div class="mb-4">
<label for="description" class="block text-sm font-medium text-gray-700 mb-2">
説明
</label>
<textarea name="description" id="description" rows="4"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">{{ old('description') }}</textarea>
@if($errors->has('description'))
<p class="text-red-500 text-sm mt-1">{{ $errors->first('description') }}</p>
@endif
</div>
<!-- 公開状態 -->
<div class="mb-6">
<label class="flex items-center">
<input type="checkbox" name="is_published" value="1"
{{ old('is_published', true) ? 'checked' : '' }}
class="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500">
<span class="ml-2 text-sm text-gray-700">公開する</span>
</label>
</div>
<div class="flex space-x-3">
<button type="submit"
class="bg-blue-500 hover:bg-blue-600 text-white px-6 py-2 rounded">
登録
</button>
<a href="{{ route('products.index') }}"
class="bg-gray-300 hover:bg-gray-400 text-gray-800 px-6 py-2 rounded inline-block">
キャンセル
</a>
</div>
</form>
</div>
</div>
</body>
</html>
💡 重要なポイント
- •
old()- バリデーションエラー時に入力値を保持 - •
$errors->has()- バリデーションエラーメッセージの表示 - •
@csrf- CSRF対策トークン(必須) - •
exists:categories,id- 存在するカテゴリーIDかチェック
✅ 商品登録ページが完成しました!
【実践4】商品編集ページの作成 (Update)
既存の商品を編集するページを作成します。
ステップ1: editメソッドの実装
app/Http/Controllers/ProductController.php
// 商品編集フォーム表示
public function edit($id)
{
$product = Product::findOrFail($id);
$categories = Category::all();
return view('products.edit', compact('product', 'categories'));
}
ステップ2: updateメソッドの実装
// 商品更新処理
public function update(Request $request, $id)
{
$product = Product::findOrFail($id);
// バリデーション
$validated = $request->validate([
'name' => 'required|max:255',
'description' => 'nullable',
'price' => 'required|integer|min:0',
'stock' => 'required|integer|min:0',
'is_published' => 'boolean',
'category_id' => 'required|exists:categories,id',
], [
'name.required' => '商品名は必須です',
'price.required' => '価格は必須です',
'stock.required' => '在庫数は必須です',
'category_id.required' => 'カテゴリーは必須です',
]);
// 公開状態の設定
$validated['is_published'] = $request->has('is_published');
// 更新
$product->update($validated);
return redirect()
->route('products.show', $product->id)
->with('success', '商品を更新しました');
}
ステップ3: ビューの作成
resources/views/products/edit.blade.php
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>商品編集</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-100">
<div class="container mx-auto px-4 py-8">
<div class="mb-6">
<a href="{{ route('products.show', $product->id) }}"
class="text-blue-600 hover:underline">← 詳細に戻る</a>
</div>
<div class="bg-white shadow-md rounded-lg p-6 max-w-2xl">
<h1 class="text-3xl font-bold text-gray-800 mb-6">商品編集</h1>
<form action="{{ route('products.update', $product->id) }}" method="POST">
@csrf
@method('PUT')
<div class="bg-purple-50 border border-purple-200 p-3 rounded mb-4">
<p class="text-xs text-gray-700 mb-1"><strong>💡 @method('PUT') / @method('DELETE') とは?</strong></p>
<p class="text-xs text-gray-600 mb-2">
HTMLの <code><form></code> タグは <strong>GET と POST しかサポートしていません</strong>。
</p>
<p class="text-xs text-gray-600">
しかしLaravelのリソースルートは PUT や DELETE を使います。<br>
<code>@method('PUT')</code> を使うと、Laravelが内部的にPUTリクエストとして扱ってくれます。<br>
これを<strong>HTTPメソッドの偽装(Method Spoofing)</strong>と言います。
</p>
</div>
<!-- 商品名 -->
<div class="mb-4">
<label for="name" class="block text-sm font-medium text-gray-700 mb-2">
商品名 <span class="text-red-500">*</span>
</label>
<input type="text" name="name" id="name"
value="{{ old('name', $product->name) }}"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
required>
@error('name')
<p class="text-red-500 text-sm mt-1">{{ $message }}</p>
@enderror
</div>
<!-- カテゴリー -->
<div class="mb-4">
<label for="category_id" class="block text-sm font-medium text-gray-700 mb-2">
カテゴリー <span class="text-red-500">*</span>
</label>
<select name="category_id" id="category_id"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
required>
@foreach ($categories as $category)
<option value="{{ $category->id }}"
{{ old('category_id', $product->category_id) == $category->id ? 'selected' : '' }}>
{{ $category->name }}
</option>
@endforeach
</select>
@if($errors->has('category_id'))
<p class="text-red-500 text-sm mt-1">{{ $errors->first('category_id') }}</p>
@endif
</div>
<!-- 価格 -->
<div class="mb-4">
<label for="price" class="block text-sm font-medium text-gray-700 mb-2">
価格(円) <span class="text-red-500">*</span>
</label>
<input type="number" name="price" id="price"
value="{{ old('price', $product->price) }}"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
required min="0">
@if($errors->has('price'))
<p class="text-red-500 text-sm mt-1">{{ $errors->first('price') }}</p>
@endif
</div>
<!-- 在庫数 -->
<div class="mb-4">
<label for="stock" class="block text-sm font-medium text-gray-700 mb-2">
在庫数 <span class="text-red-500">*</span>
</label>
<input type="number" name="stock" id="stock"
value="{{ old('stock', $product->stock) }}"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
required min="0">
@if($errors->has('stock'))
<p class="text-red-500 text-sm mt-1">{{ $errors->first('stock') }}</p>
@endif
</div>
<!-- 説明 -->
<div class="mb-4">
<label for="description" class="block text-sm font-medium text-gray-700 mb-2">
説明
</label>
<textarea name="description" id="description" rows="4"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">{{ old('description', $product->description) }}</textarea>
@if($errors->has('description'))
<p class="text-red-500 text-sm mt-1">{{ $errors->first('description') }}</p>
@endif
</div>
<!-- 公開状態 -->
<div class="mb-6">
<label class="flex items-center">
<input type="checkbox" name="is_published" value="1"
{{ old('is_published', $product->is_published) ? 'checked' : '' }}
class="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500">
<span class="ml-2 text-sm text-gray-700">公開する</span>
</label>
</div>
<div class="flex space-x-3">
<button type="submit"
class="bg-green-500 hover:bg-green-600 text-white px-6 py-2 rounded">
更新
</button>
<a href="{{ route('products.show', $product->id) }}"
class="bg-gray-300 hover:bg-gray-400 text-gray-800 px-6 py-2 rounded inline-block">
キャンセル
</a>
</div>
</form>
</div>
</div>
</body>
</html>
💡 編集フォームのポイント
- •
@method('PUT')- HTTPメソッドをPUTに指定 - •
old('name', $product->name)- エラー時は入力値、正常時はDB値 - • 登録と編集でほぼ同じフォーム → 後でコンポーネント化できる
✅ 商品編集ページが完成しました!
【実践5】商品削除機能の実装 (Delete)
商品を削除する機能を実装します。
destroyメソッドの実装
app/Http/Controllers/ProductController.php
// 商品削除処理
public function destroy($id)
{
$product = Product::findOrFail($id);
$product->delete();
return redirect()
->route('products.index')
->with('success', '商品を削除しました');
}
⚠️ 削除の確認
削除は取り消せない操作なので、必ず確認ダイアログを表示します。
一覧ページと詳細ページの削除ボタンには、すでに onclick="return confirm(...)" が実装されています。
完成したProductController全体
app/Http/Controllers/ProductController.php
<?php
namespace App\Http\Controllers;
use App\Models\Category;
use App\Models\Product;
use Illuminate\Http\Request;
class ProductController extends Controller
{
// 商品一覧
public function index()
{
$products = Product::with('category')
->where('is_published', true)
->latest()
->get();
return view('products.index', compact('products'));
}
// 商品登録フォーム表示
public function create()
{
$categories = Category::all();
return view('products.create', compact('categories'));
}
// 商品登録処理
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|max:255',
'description' => 'nullable',
'price' => 'required|integer|min:0',
'stock' => 'required|integer|min:0',
'is_published' => 'boolean',
'category_id' => 'required|exists:categories,id',
]);
$validated['is_published'] = $request->has('is_published');
Product::create($validated);
return redirect()
->route('products.index')
->with('success', '商品を登録しました');
}
// 商品詳細
public function show($id)
{
$product = Product::with('category')->findOrFail($id);
return view('products.show', compact('product'));
}
// 商品編集フォーム表示
public function edit($id)
{
$product = Product::findOrFail($id);
$categories = Category::all();
return view('products.edit', compact('product', 'categories'));
}
// 商品更新処理
public function update(Request $request, $id)
{
$product = Product::findOrFail($id);
$validated = $request->validate([
'name' => 'required|max:255',
'description' => 'nullable',
'price' => 'required|integer|min:0',
'stock' => 'required|integer|min:0',
'is_published' => 'boolean',
'category_id' => 'required|exists:categories,id',
]);
$validated['is_published'] = $request->has('is_published');
$product->update($validated);
return redirect()
->route('products.show', $product->id)
->with('success', '商品を更新しました');
}
// 商品削除処理
public function destroy($id)
{
$product = Product::findOrFail($id);
$product->delete();
return redirect()
->route('products.index')
->with('success', '商品を削除しました');
}
}
✅ CRUD機能が全て完成しました!
まとめ
商品管理アプリケーションが完成しました!
実装した機能
| 機能 | ルート | メソッド |
|---|---|---|
| 一覧表示 | GET /products | index() |
| 詳細表示 | GET /products/{id} | show() |
| 登録フォーム | GET /products/create | create() |
| 登録処理 | POST /products | store() |
| 編集フォーム | GET /products/{id}/edit | edit() |
| 更新処理 | PUT /products/{id} | update() |
| 削除処理 | DELETE /products/{id} | destroy() |
使用した技術
バックエンド
- • リソースコントローラ
- • Eloquent ORM
- • リレーション(belongsTo)
- • スコープ(published)
- • Eager Loading(N+1問題対策)
フロントエンド
- • Blade テンプレート
- • バリデーション表示($errors)
- • フラッシュメッセージ
- • 入力値保持(old())
- • CSRF対策(@csrf)
ベストプラクティス
- ✅ findOrFail() を使用
存在しないIDは自動で404エラーに - ✅ バリデーションルールを日本語化
ユーザーフレンドリーなエラーメッセージ - ✅ Eager Loadingでクエリ最適化
N+1問題を防ぐ - ✅ フラッシュメッセージで操作結果を通知
UX向上 - ✅ リソースルートで統一感のあるURL設計
RESTfulなAPI設計
次のステップ
このアプリケーションをさらに改善するアイデア:
- • ページネーション(大量データ対応)
- • 検索機能(商品名、カテゴリーで絞り込み)
- • 並び替え機能(価格順、新着順など)
- • 画像アップロード機能
- • 在庫アラート機能
- • カテゴリー管理機能
- • ユーザー認証機能
🎉 お疲れ様でした!
完全に動作する商品管理アプリケーションが完成しました!
これまで学んだルーティング、コントローラ、ビュー、マイグレーション、モデルの知識を全て使って、
実際のWebアプリケーションを作成できるようになりました。
このアプリケーションを土台に、さらに機能を追加していきましょう!