✅ 事前準備
- XAMPP(Apache & MySQL が起動できること)
- Composer(インストール済み)
- VS Code(推奨)
確認コマンド(PowerShell/コマンドプロンプト):
php -v
composer -V
🧭 完成イメージ
- 新規投稿フォーム(対象名、タイトル、本文、星評価1〜5)
- 一覧表示(「★★★★★」表記、インライン編集、削除)
- 1つの Blade で完結するミニ CRUD
- 画像機能なし → 超シンプル
1) データベース作成(phpMyAdmin)
-
ブラウザ:
http://localhost/phpmyadmin/ -
「データベース」→「新規作成」
-
データベース名:
review_system -
照合順序:
utf8mb4_general_ci
-
データベース名:
2) Laravel プロジェクト新規作成
cd C:\xampp\htdocs
composer create-project laravel/laravel review-system
cd review-system
php artisan --version
3) .env(DB接続)を修正 → 反映
ファイル:review-system/.env
APP_NAME="ReviewSystem"
APP_ENV=local
APP_DEBUG=true
APP_URL=http://localhost
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=review_system
DB_USERNAME=root
DB_PASSWORD=
反映コマンド:
php artisan key:generate
php artisan config:clear
4) マイグレーション作成(reviews テーブル)
php artisan make:migration create_reviews_table --create=reviews
ファイル:database/migrations/xxxx_xx_xx_xxxxxx_create_reviews_table.php
👉 全文コピペ
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::create('reviews', function (Blueprint $table) {
$table->id();
$table->string('item_name'); // 対象名(お店・商品・サービス等)
$table->string('title'); // タイトル
$table->text('body')->nullable(); // 本文(任意)
$table->unsignedTinyInteger('rating'); // 星(1〜5)
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('reviews');
}
};
実行:
php artisan migrate
5) モデル作成(Review)
php artisan make:model Review
ファイル:app/Models/Review.php
👉 全文コピペ
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Review extends Model
{
protected $fillable = ['item_name', 'title', 'body', 'rating'];
}
6) コントローラ作成(ReviewController)
php artisan make:controller ReviewController
ファイル:app/Http/Controllers/ReviewController.php
👉 全文コピペ
<?php
namespace App\Http\Controllers;
use App\Models\Review;
use Illuminate\Http\Request;
class ReviewController extends Controller
{
public function index()
{
$reviews = Review::latest()->get();
return view('reviews.index', compact('reviews'));
}
public function store(Request $request)
{
$data = $request->validate([
'item_name' => 'required|string|max:100',
'title' => 'required|string|max:100',
'body' => 'nullable|string|max:1000',
'rating' => 'required|integer|min:1|max:5',
]);
Review::create($data);
return redirect()->route('reviews.index')->with('status', '口コミを追加しました!');
}
public function update(Request $request, Review $review)
{
$data = $request->validate([
'item_name' => 'required|string|max:100',
'title' => 'required|string|max:100',
'body' => 'nullable|string|max:1000',
'rating' => 'required|integer|min:1|max:5',
]);
$review->update($data);
return redirect()->route('reviews.index')->with('status', '口コミを更新しました!');
}
public function destroy(Review $review)
{
$review->delete();
return redirect()->route('reviews.index')->with('status', '口コミを削除しました!');
}
}
7) ルート設定
ファイル:routes/web.php
👉 全文コピペ
<?php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\ReviewController;
Route::get('/', fn() => redirect()->route('reviews.index'));
Route::get('/reviews', [ReviewController::class, 'index'])->name('reviews.index');
Route::post('/reviews', [ReviewController::class, 'store'])->name('reviews.store');
Route::put('/reviews/{review}', [ReviewController::class, 'update'])->name('reviews.update');
Route::delete('/reviews/{review}', [ReviewController::class, 'destroy'])->name('reviews.destroy');
8) ビュー作成(手動)— ライトテーマの見やすいデザイン
⚠️ Laravel 標準に「
make:view」はありません。手動でファイルを作ります。
フォルダ作成:
mkdir resources/views/reviews
ファイル:resources/views/reviews/index.blade.php
👉 全文コピペ
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>ReviewSystem(口コミアプリ)</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
/* ===== ライトテーマ ===== */
:root{
--bg:#f5f7fb;
--card:#ffffff;
--text:#1e293b;
--muted:#6b7280;
--primary:#2563eb;
--primary-600:#1d4ed8;
--danger:#dc2626;
--border:#e5e7eb;
--shadow:0 8px 20px rgba(0,0,0,.06);
--radius:14px;
}
*{box-sizing:border-box}
body{
margin:0; padding:24px;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Noto Sans JP", sans-serif;
background:var(--bg); color:var(--text); line-height:1.6;
}
.container{ max-width: 960px; margin:0 auto; }
.hero{ display:flex; gap:16px; align-items:center; justify-content:space-between; margin-bottom:20px; }
.title{ margin:0; font-weight:800; font-size: clamp(22px, 3.6vw, 30px); }
.pill{ padding:6px 10px; border:1px solid var(--border); border-radius:999px; font-size:12px; color:var(--muted); }
.card{ background:var(--card); border:1px solid var(--border); border-radius:var(--radius); box-shadow:var(--shadow); padding:18px; margin-top:16px; }
.grid{ display:grid; gap:12px; grid-template-columns: 1fr; }
@media (min-width:720px){ .grid.cols-2{ grid-template-columns: 1fr 1fr; } }
label{ font-size:12px; color:var(--muted); display:block; margin-bottom:4px; }
input[type="text"], textarea{
width:100%; padding:10px 12px; border:1px solid var(--border); border-radius:10px; background:#fff;
}
textarea{ min-height:100px; resize: vertical; }
.actions{ display:flex; gap:10px; flex-wrap:wrap; }
.btn{ border:none; cursor:pointer; border-radius:10px; padding:10px 14px; font-weight:600; }
.btn.primary{ background:var(--primary); color:#fff; }
.btn.primary:hover{ background:var(--primary-600); }
.btn.outline{ background:#fff; color:var(--text); border:1px solid var(--border); }
.btn.danger{ background:var(--danger); color:#fff; }
.rating{ display:inline-flex; flex-direction: row-reverse; gap:6px; }
.rating input{ display:none; }
.rating label{ font-size:26px; cursor:pointer; color:#cbd5e1; }
.rating input:checked ~ label,
.rating label:hover,
.rating label:hover ~ label{ color:#f59e0b; }
.stars{ color:#f59e0b; letter-spacing:2px; font-size:18px; font-weight:700; }
.review{ border-top:1px dashed var(--border); padding-top:14px; margin-top:14px; }
.review:first-child{ border-top:none; }
.meta{ font-size:12px; color:var(--muted); margin-bottom:6px; }
.alert{ border:1px solid var(--border); background:#eff6ff; color:var(--text); border-radius:12px; padding:10px 14px; margin:10px 0; }
.alert.error{ background:#fee2e2; border-color:#fecaca; }
</style>
</head>
<body>
<div class="container">
<div class="hero">
<h1 class="title">🌟 ReviewSystem(口コミアプリ)</h1>
<span class="pill">Laravel / CRUD / ★評価</span>
</div>
{{-- Flash --}}
@if(session('status'))
<div class="alert">{{ session('status') }}</div>
@endif
{{-- Validation --}}
@if ($errors->any())
<div class="alert error">
入力に誤りがあります。
<ul>
@foreach ($errors->all() as $e)
<li>・{{ $e }}</li>
@endforeach
</ul>
</div>
@endif
<!-- ===== 新規投稿フォーム ===== -->
<div class="card">
<h2>🆕 新規口コミを投稿</h2>
<form method="post" action="{{ route('reviews.store') }}">
@csrf
<div class="grid cols-2">
<div>
<label>対象名</label>
<input type="text" name="item_name" value="{{ old('item_name') }}" required placeholder="例:カフェABC">
</div>
<div>
<label>タイトル</label>
<input type="text" name="title" value="{{ old('title') }}" required placeholder="短い見出し">
</div>
</div>
<div class="grid">
<div>
<label>本文(任意)</label>
<textarea name="body" placeholder="感想やポイント">{{ old('body') }}</textarea>
</div>
</div>
<div class="rating" aria-label="星評価">
@for($i=5;$i>=1;$i--)
<input type="radio" id="create-star-{{ $i }}" name="rating" value="{{ $i }}" {{ old('rating') == $i ? 'checked' : '' }}>
<label for="create-star-{{ $i }}">★</label>
@endfor
</div>
<div class="actions" style="margin-top:12px;">
<button class="btn primary">投稿する</button>
<button type="reset" class="btn outline">リセット</button>
</div>
</form>
</div>
<!-- ===== 一覧表示 ===== -->
<div class="card">
<h2>📚 口コミ一覧</h2>
@if($reviews->isEmpty())
<p class="muted">まだ口コミがありません。</p>
@else
@foreach($reviews as $r)
<div class="review">
<div class="meta">#{{ $r->id }} ・ {{ $r->created_at->format('Y-m-d H:i') }}</div>
<div><strong>対象:</strong>{{ $r->item_name }}</div>
<div class="stars">{{ str_repeat('★',$r->rating) }}{{ str_repeat('☆',5-$r->rating) }}</div>
<form method="post" action="{{ route('reviews.update',$r) }}" style="margin-top:10px;">
@csrf @method('PUT')
<input type="text" name="item_name" value="{{ $r->item_name }}" required>
<input type="text" name="title" value="{{ $r->title }}" required>
<textarea name="body">{{ $r->body }}</textarea>
<div class="rating" style="margin-top:8px;">
@for($i=5;$i>=1;$i--)
<input type="radio" id="edit-{{ $r->id }}-star-{{ $i }}" name="rating" value="{{ $i }}" {{ $r->rating==$i?'checked':'' }}>
<label for="edit-{{ $r->id }}-star-{{ $i }}">★</label>
@endfor
</div>
<div class="actions" style="margin-top:12px;">
<button class="btn primary">更新</button>
</div>
</form>
<form method="post" action="{{ route('reviews.destroy',$r) }}" onsubmit="return confirm('削除しますか?');" style="margin-top:8px;">
@csrf @method('DELETE')
<button class="btn danger">削除</button>
</form>
</div>
@endforeach
@endif
</div>
</div>
</body>
</html>
9) 起動
php artisan serve
- ブラウザ:
http://127.0.0.1:8000 - 「対象名+タイトル+本文+星評価(★★★★★)」で 投稿 → 一覧 → 更新 → 削除 を確認
🧹 困ったときのキャッシュクリア(まとめて)
反映されない/画面が崩れる/.env を変えたのに変化しない…そんな時はコレ
php artisan optimize:clear
(個別:config:clear / route:clear / view:clear / cache:clear)
🧩 よくあるエラーと対処
-
419 Page Expired(CSRF)
→ すべての<form>に@csrfが入っているか。php artisan key:generate後にconfig:clear -
Base table already exists
→ 開発中はphp artisan migrate:fresh(既存データは消えます) -
ページが真っ白
→.envをAPP_DEBUG=trueにしてstorage/logs/laravel.logを確認 -
ビューが作れない
→ Laravel 標準にphp artisan make:viewはありません(手動で作成してください)
🎯 まとめ
- 最小の CRUD 構成で「Laravel がどう動くか」を確実に体験
- Blade 1画面・画像なし・Node不要 → 学習コストを最小化
- まずはこの構成で成功体験 → 慣れてきたら「検索」「ページネーション」「カテゴリ」など拡張!