【VSCode】Laravelアプリ 「ショップ買い物」 手動開発

Laravel

 前回、VSCodeとClineで自動で開発してショップお買い物アプリを、手動で作成します。

1. データベースの準備(XAMPP)

XAMPP Control Panelを開き、ApacheMySQL の「Start」ボタンをそれぞれクリックして起動(ステータスが緑色に変化)させておく。

  1. http://localhost/phpmyadmin/ を開く。
  2. shop_laravel_db という名前で空のデータベースを作成する(文字コード:utf8mb4_general_ci)。

2. Laravelプロジェクトの作成と環境設定

① プロジェクトの作成

コマンドプロンプトで C:\xampp\htdocs に移動し、プロジェクトを作成。

cd C:\xampp\htdocs
C:\xampp\htdocs> composer create-project laravel/laravel shop_app

② VS Codeをコマンドプロンプトから起動

プロジェクトの作成が完了したら、生成された shop_app ディレクトリに移動し、コマンドプロンプトから直接VS Codeを起動する。

C:\xampp\htdocs> cd shop_app
C:\xampp\htdocs\shop_app> code .

💡 補足 code .(コード・ドット)を実行すると、現在カレントディレクトリに指定している shop_app フォルダが自動的にVS Codeに読み込まれた状態でエディタが立ち上がる。

③ .env ファイルの編集

VS Codeが開いたら、プロジェクトのルート直下にある .env ファイルを選択し、データベース接続情報をXAMPP環境に合わせて書き換える。

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=shop_laravel_db
DB_USERNAME=root
DB_PASSWORD=

D=

3. マイグレーションとモデルの作成

① マイグレーションファイルの作成

VSCodeのターミナルで以下のコマンドを実行し、テーブル定義ファイルを作成する。

C:\xampp\htdocs\shop_app> php artisan make:migration create_shop_tables

database/migrations/xxxx_xx_xx_xxxxxx_create_shop_tables.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
    {
        // 1. 商品テーブル
        Schema::create('products', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->integer('price');
            $table->string('image_url')->default('https://via.placeholder.com/150');
            $table->timestamps();
        });

        // 2. 注文メインテーブル
        Schema::create('orders', function (Blueprint $table) {
            $table->id();
            $table->string('customer_name');
            $table->text('address');
            $table->string('tel');
            $table->integer('total_price');
            $table->timestamps();
        });

        // 3. 注文明細テーブル
        Schema::create('order_details', function (Blueprint $table) {
            $table->id();
            $table->foreignId('order_id')->constrained()->onDelete('cascade');
            $table->foreignId('product_id')->constrained();
            $table->integer('price');
            $table->integer('quantity');
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('order_details');
        Schema::dropIfExists('orders');
        Schema::dropIfExists('products');
    }
};

② シーダーの作成(テストデータ挿入)

C:\xampp\htdocs\shop_app> php artisan make:seeder 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' => 'オーガニックコーヒー豆', 'price' => 1200, 'image_url' => 'https://via.placeholder.com/150?text=Coffee'],
            ['name' => '高級宇治抹茶セット', 'price' => 2500, 'image_url' => 'https://via.placeholder.com/150?text=Matcha'],
            ['name' => 'ハンドメイドマグカップ', 'price' => 1800, 'image_url' => 'https://via.placeholder.com/150?text=Cup'],
        ]);
    }
}

database/seeders/DatabaseSeeder.php を開き、作成したシーダーを呼ぶようにする。

public function run(): void
{
    $this->call(ProductSeeder::class);
}

③ マイグレーションとシードの実行

C:\xampp\htdocs\shop_app> php artisan migrate --seed

④ モデルの作成

C:\xampp\htdocs\shop_app> php artisan make:model Product
C:\xampp\htdocs\shop_app> php artisan make:model Order
C:\xampp\htdocs\shop_app> php artisan make:model OrderDetail

4. ルーティングの定義

routes/web.php を開き、画面遷移とアクションへのルートを定義する。

<?php

use App\Http\Controllers\ShopController;
use Illuminate\Support\Facades\Route;

Route::get('/', [ShopController::class, 'index'])->name('shop.index');
Route::post('/cart/add', [ShopController::class, 'addToCart'])->name('cart.add');
Route::get('/cart', [ShopController::class, 'cart'])->name('cart.index');
Route::post('/cart/update', [ShopController::class, 'updateCart'])->name('cart.update');
Route::post('/cart/remove', [ShopController::class, 'removeFromCart'])->name('cart.remove');
Route::get('/checkout', [ShopController::class, 'checkout'])->name('checkout');
Route::post('/success', [ShopController::class, 'success'])->name('success');

5. コントローラーの作成(ロジック実装)

C:\xampp\htdocs\shop_app> php artisan make:controller ShopController

app/Http/Controllers/ShopController.php を以下のように編集。

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Product;
use App\Models\Order;
use App\Models\OrderDetail;
use Illuminate\Support\Facades\DB;

class ShopController extends Controller
{
    // 商品一覧
    public function index()
    {
        $products = Product::all();
        return view('shop.index', compact('products'));
    }

    // カート追加
    public function addToCart(Request $request)
    {
        $productId = $request->input('product_id');
        $quantity = $request->input('quantity', 1);

        $cart = session()->get('cart', []);

        if (isset($cart[$productId])) {
            $cart[$productId] += $quantity;
        } else {
            $cart[$productId] = $quantity;
        }

        session()->put('cart', $cart);
        return redirect()->route('cart.index');
    }

    // カート画面
    public function cart()
    {
        $cart = session()->get('cart', []);
        $cartItems = [];
        $totalPrice = 0;

        if (!empty($cart)) {
            $products = Product::whereIn('id', array_keys($cart))->get();
            foreach ($products as $p) {
                $qty = $cart[$p->id];
                $subtotal = $p->price * $qty;
                $totalPrice += $subtotal;

                $cartItems[] = [
                    'id' => $p->id,
                    'name' => $p->name,
                    'price' => $p->price,
                    'image_url' => $p->image_url,
                    'quantity' => $qty,
                    'subtotal' => $subtotal
                ];
            }
        }

        return view('shop.cart', compact('cartItems', 'totalPrice'));
    }

    // カート数量更新
    public function updateCart(Request $request)
    {
        $quantities = $request->input('quantities', []);
        $cart = session()->get('cart', []);

        foreach ($quantities as $id => $qty) {
            if ($qty <= 0) {
                unset($cart[$id]);
            } else {
                $cart[$id] = (int)$qty;
            }
        }

        session()->put('cart', $cart);
        return redirect()->route('cart.index');
    }

    // カートから削除
    public function removeFromCart(Request $request)
    {
        $id = $request->input('product_id');
        $cart = session()->get('cart', []);

        if (isset($cart[$id])) {
            unset($cart[$id]);
        }

        session()->put('cart', $cart);
        return redirect()->route('cart.index');
    }

    // お届け先入力画面
    public function checkout()
    {
        if (empty(session()->get('cart'))) {
            return redirect()->route('shop.index');
        }
        return view('shop.checkout');
    }

    // 注文確定(トランザクション処理)
    public function success(Request $request)
    {
        $cart = session()->get('cart', []);
        if (empty($cart)) {
            return redirect()->route('shop.index');
        }

        $request->validate([
            'customer_name' => 'required|string|max:100',
            'address' => 'required|string',
            'tel' => 'required|string',
        ]);

        DB::beginTransaction();
        try {
            $products = Product::whereIn('id', array_keys($cart))->get();
            $totalPrice = 0;
            $orderDetailsData = [];

            foreach ($products as $p) {
                $qty = $cart[$p->id];
                $totalPrice += $p->price * $qty;

                $orderDetailsData[] = [
                    'product_id' => $p->id,
                    'price' => $p->price,
                    'quantity' => $qty,
                ];
            }

            // 1. ordersテーブル保存
            $order = new Order();
            $order->customer_name = $request->input('customer_name');
            $order->address = $request->input('address');
            $order->tel = $request->input('tel');
            $order->total_price = $totalPrice;
            $order->save();

            // 2. order_detailsテーブル保存
            foreach ($orderDetailsData as $data) {
                $detail = new OrderDetail();
                $detail->order_id = $order->id;
                $detail->product_id = $data['product_id'];
                $detail->price = $data['price'];
                $detail->quantity = $data['quantity'];
                $detail->save();
            }

            DB::commit();
            session()->forget('cart'); // カート空にする

            return view('shop.success');

        } catch (\Exception $e) {
            DB::rollBack();
            return back()->withErrors(['error' => '注文処理に失敗しました: ' . $e->getMessage()]);
        }
    }
}

6. ビュー(Bladeテンプレート)の作成

resources/views/shop ディレクトリを作成し、その中に以下の4つのファイルを配置する。

① index.blade.php(商品一覧)

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>商品一覧</title>
    <style>
        body { font-family: sans-serif; background: #f4f4f4; margin: 20px; }
        .grid { display: flex; gap: 20px; }
        .card { background: #fff; padding: 15px; border-radius: 5px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); width: 200px; text-align: center; }
        .price { color: #bf0000; font-weight: bold; margin: 10px 0; }
        .btn { background: #007bff; color: #fff; border: none; padding: 8px 15px; border-radius: 3px; cursor: pointer; }
    </style>
</head>
<body>
    <h1>商品一覧</h1>
    <p><a href="{{ route('cart.index') }}">カートを見る</a></p>
    <div class="grid">
        @foreach ($products as $p)
            <div class="card">
                <img src="{{ $p->image_url }}" alt="" style="max-width:100%;">
                <h3>{{ $p->name }}</h3>
                <p class="price">¥{{ number_format($p->price) }}</p>
                <form action="{{ route('cart.add') }}" method="POST">
                    @csrf
                    <input type="hidden" name="product_id" value="{{ $p->id }}">
                    <input type="number" name="quantity" value="1" min="1" style="width: 50px; margin-bottom: 10px;"><br>
                    <button type="submit" class="btn">カートに入れる</button>
                </form>
            </div>
        @endforeach
    </div>
</body>
</html>

② cart.blade.php(カート画面)

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>買い物カート</title>
    <style>
        body { font-family: sans-serif; background: #f4f4f4; margin: 20px; }
        table { width: 100%; border-collapse: collapse; background: #fff; }
        th, td { padding: 10px; border: 1px solid #ddd; text-align: center; }
        th { background: #eee; }
        .total { text-align: right; font-size: 1.2rem; font-weight: bold; margin: 20px 0; }
    </style>
</head>
<body>
    <h1>買い物カート</h1>
    @if (empty($cartItems))
        <p>カートは空です。<a href="{{ route('shop.index') }}">お買い物を続ける</a></p>
    @else
        <form action="{{ route('cart.update') }}" method="POST">
            @csrf
            <table>
                <thead>
                    <tr>
                        <th>商品名</th><th>単価</th><th>数量</th><th>小計</th><th>操作</th>
                    </tr>
                </thead>
                <tbody>
                    @foreach ($cartItems as $item)
                        <tr>
                            <td>{{ $item['name'] }}</td>
                            <td>¥{{ number_format($item['price']) }}</td>
                            <td>
                                <input type="number" name="quantities[{{ $item['id'] }}]" value="{{ $item['quantity'] }}" min="1" style="width:60px;">
                            </td>
                            <td>¥{{ number_format($item['subtotal']) }}</td>
                            <td>
                                <button type="submit" formaction="{{ route('cart.remove') }}" name="product_id" value="{{ $item['id'] }}" style="color:red;">削除</button>
                            </td>
                        </tr>
                    @endforeach
                </tbody>
            </table>
            <div class="total">合計金額: ¥{{ number_format($totalPrice) }}</div>
            <button type="submit">数量を更新</button>
            <a href="{{ route('shop.index') }}">お買い物を続ける</a> | 
            <a href="{{ route('checkout') }}" style="font-weight:bold; color:green;">購入手続きへ</a>
        </form>
    @endif
</body>
</html>

③ checkout.blade.php(お届け先入力)

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>お届け先入力</title>
    <style>
        body { font-family: sans-serif; background: #f4f4f4; margin: 20px; }
        .form-group { margin-bottom: 15px; }
        label { display: block; font-weight: bold; margin-bottom: 5px; }
        input[type="text"] { width: 300px; padding: 8px; }
    </style>
</head>
<body>
    <h1>お届け先情報入力</h1>
    <form action="{{ route('success') }}" method="POST">
        @csrf
        <div class="form-group">
            <label>お名前</label>
            <input type="text" name="customer_name" required>
        </div>
        <div class="form-group">
            <label>ご住所</label>
            <input type="text" name="address" required style="width:400px;">
        </div>
        <div class="form-group">
            <label>電話番号</label>
            <input type="text" name="tel" required>
        </div>
        <button type="submit" style="background: green; color: white; padding: 10px 20px; border: none; cursor: pointer;">注文を確定する</button>
    </form>
    <p><a href="{{ route('cart.index') }}">カートに戻る</a></p>
</body>
</html>

④ success.blade.php(注文完了)

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>注文完了</title>
    <style>body { font-family: sans-serif; text-align: center; margin-top: 50px; }</style>
</head>
<body>
    <h1>ご注文ありがとうございました!</h1>
    <p>Laravelバックエンドでの注文処理(トランザクション)が正常に完了しました。</p>
    <p><a href="{{ route('shop.index') }}">トップページへ戻る</a></p>
</body>
</html>

7. 動作確認

① ブラウザからアクセス

ブラウザを開き、以下のURLへアクセスする。 👉 http://localhost/shop_app/public/

💡 注意ポイント Laravelの公開ディレクトリはプロジェクト直下の public フォルダであるため、URLの末尾に /public/ が必要。

② 挙動の検証

  1. 商品一覧画面: products テーブルから取得したデータがBlade(index.blade.php)を介して正常に表示されるか確認。
  2. カート追加: 数量を指定して「カートに入れる」を押した際、セッションに保持され、cart.blade.phphttp://localhost/shop_app/public/cart)へ正しく遷移するか検証。
  3. 数量変更・削除: カート画面で数量の更新、および削除ボタンが正常に機能(ルート定義通りのPOST処理が実行)するか確認。
  4. 注文処理: お届け先入力から「注文を確定する」を実行し、DBのトランザクションが成功して完了画面(success.blade.php)が表示されるか確認。
  5. DB確認: phpMyAdminを開き、orders および order_details にレコードが正しく格納され、カートのセッションがクリアされているか最終確認して完了。

コメント