0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

📸:XAMPP+Composerで作る「口コミレビュー」アプリ

0
Last updated at Posted at 2025-09-03

✅ 事前準備

  • 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(既存データは消えます)

  • ページが真っ白
    .envAPP_DEBUG=true にして storage/logs/laravel.log を確認

  • ビューが作れない
    → Laravel 標準に php artisan make:view はありません(手動で作成してください)


🎯 まとめ

  • 最小の CRUD 構成で「Laravel がどう動くか」を確実に体験
  • Blade 1画面・画像なし・Node不要 → 学習コストを最小化
  • まずはこの構成で成功体験 → 慣れてきたら「検索」「ページネーション」「カテゴリ」など拡張!

0
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?