【第四弾】Laravel入門資料

📚 Laravel公式ドキュメント - Controllers

https://laravel.com/docs/12.x/controllers

💡 この講座では、上記の公式ドキュメントを基に解説していきます。
公式ドキュメントは初学者には内容が難しいため、エッセンスを優しく噛み砕いて解説していきます。

コントローラ(Controller)とは

コントローラとは、アプリケーションのロジック(処理)をまとめて管理するクラスのことです。 これまではルートファイル(routes/web.php)に直接処理を書いていましたが、 アプリケーションが大きくなると管理が大変になります。

コントローラを使うことで、ルートファイルをシンプルに保ち、処理を整理して管理できます。

MVCアーキテクチャ

LaravelはMVC(Model-View-Controller)アーキテクチャを採用しています。

  • Model(モデル) - データベースとのやり取りを担当
  • View(ビュー) - 画面表示を担当
  • Controller(コントローラ) - ビジネスロジックを担当

役割を分けることで、コードの見通しが良くなり、保守性が向上します。

💡
重要:MVCは汎用的な知識です

MVCアーキテクチャはLaravel専用のものではありません。 世界中の多くのWebフレームワークで採用されている、普遍的な設計パターンです。

ここで学ぶMVCの知識は、以下のようなフレームワークでも活用できます:

  • Ruby on Rails(Ruby)- MVC発祥のフレームワーク
  • Django(Python)- MTV(Model-Template-View)として実装
  • ASP.NET MVC(C#)- Microsoftの企業向けフレームワーク
  • Spring MVC(Java)- エンタープライズ開発で広く使用
  • Express.js(Node.js)- MVCパターンを採用可能
  • CakePHP / Symfony(PHP)- 同じくPHPのMVCフレームワーク

つまり、Laravelで学んだMVCの考え方は、他の言語やフレームワークに移っても使える「一生モノのスキル」です。 単なるLaravelの使い方ではなく、Webアプリケーション設計の基礎を学んでいるのだと理解してください。

コントローラを使わない場合の問題点

まず、コントローラを使わない場合のコードを見てみましょう。

// routes/web.php

Route::get('/users', function () {
    $users = ['太郎', '花子', '次郎'];
    return view('users.index', compact('users'));
});

Route::get('/users/{id}', function ($id) {
    // ユーザー情報を取得する処理
    $user = ['id' => $id, 'name' => '太郎'];
    return view('users.show', compact('user'));
});

Route::post('/users', function () {
    // ユーザー作成処理
    // バリデーション処理
    // データベース保存処理
    return redirect('/users');
});

問題点:

  • routes/web.phpが肥大化する
  • 再利用しづらい
  • テストがしづらい

コントローラの作成

Artisanコマンドで作成

コントローラは、Artisanコマンドで簡単に作成できます。

php artisan make:controller UserController

これにより、app/Http/Controllers/UserController.php が作成されます。

作成されたファイルの中身

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class UserController extends Controller
{
    //
}

Controller クラスを継承した空のクラスが作成されます。
ここにメソッド(処理)を追加していきます。

基本的なコントローラの使い方

1. コントローラにメソッドを追加

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class UserController extends Controller
{
    // ユーザー一覧を表示
    public function index()
    {
        $users = ['太郎', '花子', '次郎'];
        return view('users.index', compact('users'));
    }

    // 個別ユーザーを表示
    public function show($id)
    {
        $user = ['id' => $id, 'name' => '太郎'];
        return view('users.show', compact('user'));
    }
}

2. ルートからコントローラを呼び出す

// routes/web.php

use App\Http\Controllers\UserController;

Route::get('/users', [UserController::class, 'index']);
Route::get('/users/{id}', [UserController::class, 'show']);

解説:

  • [コントローラクラス::class, 'メソッド名'] の形式で指定
  • ファイル先頭で use でコントローラをインポート
  • ルートファイルがすっきりする
📝 補足:::class とは?

::class は、クラスの完全修飾名を文字列で取得するPHP の記法です。

UserController::class
// 結果: 'App\Http\Controllers\UserController'

なぜ使うのか?

  • ✅ タイプミスを防げる
    文字列 'UserContoller' だと間違いに気づけないが、
    UserContoller::class ならクラスが存在しないとエラーが出る
  • ✅ IDEの補完が効く
    VSCodeやPhpStormで自動補完やジャンプ機能が使える
  • ✅ リファクタリングに強い
    クラス名を変更した時、IDEが自動で追従してくれる

💡 Laravel 8以降は ::class 記法が推奨されています。

✅ ルートファイルは「交通整理」だけを担当し、実際の処理はコントローラに任せます。

リソースコントローラ

CRUDとは?

CRUDは、データベース操作の基本的な4つの機能を表す頭文字です:

CRUD 意味 SQL HTTPメソッド
Create 作成 INSERT POST 新しいユーザーを登録
Read 読取 SELECT GET ユーザー一覧・詳細を表示
Update 更新 UPDATE PUT/PATCH ユーザー情報を編集
Delete 削除 DELETE DELETE ユーザーを削除

💡 CRUDの重要性

ほとんどのWebアプリケーションは、このCRUD操作の組み合わせで構成されています。
例:ブログ → 記事の作成・表示・編集・削除
例:ECサイト → 商品の登録・一覧表示・更新・削除

LaravelではCRUD操作を行うコントローラを、 リソースコントローラとして一括で作成できます。

リソースコントローラを作成

php artisan make:controller UserController --resource

自動的に7つのメソッドが作成されます:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class UserController extends Controller
{
    // 一覧表示
    public function index()
    {
        //
    }

    // 新規作成フォーム表示
    public function create()
    {
        //
    }

    // データ保存
    public function store(Request $request)
    {
        //
    }

    // 個別データ表示
    public function show(string $id)
    {
        //
    }

    // 編集フォーム表示
    public function edit(string $id)
    {
        //
    }

    // データ更新
    public function update(Request $request, string $id)
    {
        //
    }

    // データ削除
    public function destroy(string $id)
    {
        //
    }
}

リソースルートの定義

7つのルートを1行で定義できます。

// routes/web.php

use App\Http\Controllers\UserController;

Route::resource('users', UserController::class);

この1行で、以下のルートが自動的に定義されます:

HTTPメソッド URI アクション 用途
GET /users index 一覧表示
GET /users/create create 新規作成フォーム
POST /users store データ保存
GET /users/{id} show 個別表示
GET /users/{id}/edit edit 編集フォーム
PUT/PATCH /users/{id} update データ更新
DELETE /users/{id} destroy データ削除
📝 補足:PUT と PATCH の違い

HTTPメソッドとして、PUTPATCHは本来異なる意味を持ちます。

PUT

リソース全体を置き換える(すべてのフィールドを送信)

PATCH

リソースの一部を更新する(変更するフィールドだけ送信)

⚠️ Laravelでは区別しない

実務では、PUTとPATCHを厳密に区別せず、両方とも同じupdateメソッドで処理します。
Route::resource() も自動的に両方のルートを同じメソッドに割り当てます。

// どちらも update メソッドを呼び出す
Route::put('/users/{id}', [UserController::class, 'update']);
Route::patch('/users/{id}', [UserController::class, 'update']);

// または両方を一度に定義
Route::match(['put', 'patch'], '/users/{id}', [UserController::class, 'update']);

💡 Bladeテンプレートでは @method('PUT') または @method('PATCH') のどちらでもOKです。

💡 php artisan route:list で定義されたルートを確認できます。

📚 HTMLフォームでPUT/DELETEメソッドを使う方法

HTMLの<form>タグは、GETとPOSTしか対応していません
Laravelでは @method ディレクティブを使ってPUT/PATCH/DELETEを実現します。

更新フォーム (PUT/PATCH)

<form action="/products/{{ $id }}" method="POST">
    @csrf
    @method('PUT')

    <input type="text" name="name" value="{{ $product->name }}">
    <button type="submit">更新</button>
</form>

削除フォーム (DELETE)

<form action="/products/{{ $id }}" method="POST">
    @csrf
    @method('DELETE')

    <button type="submit">削除</button>
</form>

💡 @method('PUT') を書くと、Laravelが内部的に <input type="hidden" name="_method" value="PUT"> を生成します。
実際の使い方は、次の講座(データベース編)で編集・削除機能を作る時に詳しく学びます。

コントローラの整理

サブディレクトリで整理

コントローラが増えてきたら、サブディレクトリで整理できます。

app/Http/Controllers/
├── Admin/
│   ├── UserController.php
│   └── PostController.php
├── Api/
│   └── UserController.php
└── UserController.php
# サブディレクトリ付きで作成
php artisan make:controller Admin/UserController
// routes/web.php

use App\Http\Controllers\Admin\UserController;

Route::get('/admin/users', [UserController::class, 'index']);

【実践1】商品一覧と詳細ページを作る

ここからは実際に手を動かして、コントローラを使ったページを作っていきましょう。
商品一覧と詳細ページを作成し、ページ遷移を実装します。

Step 1: コントローラを作成

ターミナルで以下のコマンドを実行します。

php artisan make:controller ProductController

app/Http/Controllers/ProductController.php が作成されます。

Step 2: コントローラにメソッドを追加

作成された ProductController にメソッドを追加します。

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class ProductController extends Controller
{
    // 商品一覧を表示
    public function index()
    {
        $products = [
            ['id' => 1, 'name' => 'ノートPC', 'price' => 120000],
            ['id' => 2, 'name' => 'マウス', 'price' => 3000],
            ['id' => 3, 'name' => 'キーボード', 'price' => 8000],
        ];

        return view('products.index', compact('products'));
    }

    // 商品詳細を表示
    public function show($id)
    {
        $products = [
            1 => ['id' => 1, 'name' => 'ノートPC', 'price' => 120000, 'description' => '高性能なノートパソコンです'],
            2 => ['id' => 2, 'name' => 'マウス', 'price' => 3000, 'description' => 'ワイヤレスマウスです'],
            3 => ['id' => 3, 'name' => 'キーボード', 'price' => 8000, 'description' => 'メカニカルキーボードです'],
        ];

        // 指定されたIDの商品を取得(存在しない場合はnull)
        $product = $products[$id] ?? null;

        // 商品が見つからない場合は404エラー
        if (!$product) {
            abort(404, '商品が見つかりません');
        }

        return view('products.show', compact('product'));
    }
}

💡 今回は配列でデータを用意していますが、次の講座でデータベースから取得する方法を学びます。

📚 abort() によるエラーレスポンス

abort(404) は、指定したHTTPステータスコードでエラーページを表示する関数です。

// よく使うステータスコード
abort(404);  // Not Found - ページが見つからない
abort(403);  // Forbidden - アクセス権限がない
abort(500);  // Internal Server Error - サーバーエラー

// エラーメッセージを付ける
abort(404, '商品が見つかりません');

// 条件付きでabort
abort_if(!$user, 403, 'アクセス権限がありません');
abort_unless($user->isAdmin(), 403);

💡 実務では、データが見つからない時やアクセス権限チェックでよく使います。

Step 3: ルートを定義

routes/web.php にルートを追加します。

// routes/web.php

use App\Http\Controllers\ProductController;

Route::get('/products', [ProductController::class, 'index']);
Route::get('/products/{id}', [ProductController::class, 'show']);

Step 4: ビューを作成(一覧ページ)

resources/views/products ディレクトリを作成し、
resources/views/products/index.blade.php を作成します。

<!-- resources/views/products/index.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>
    <style>
        body {
            font-family: sans-serif;
            max-width: 800px;
            margin: 50px auto;
            padding: 20px;
        }
        h1 {
            color: #333;
        }
        ul {
            list-style: none;
            padding: 0;
        }
        li {
            background: #f5f5f5;
            margin: 10px 0;
            padding: 15px;
            border-radius: 5px;
        }
        a {
            color: #3183ff;
            text-decoration: none;
            font-size: 18px;
        }
        a:hover {
            text-decoration: underline;
        }
        .price {
            color: #666;
            font-size: 16px;
        }
    </style>
</head>
<body>
    <h1>商品一覧</h1>
    <ul>
        @foreach($products as $product)
            <li>
                <a href="/products/{{ $product['id'] }}">
                    {{ $product['name'] }}
                </a>
                <span class="price"> - {{ number_format($product['price']) }}円</span>
            </li>
        @endforeach
    </ul>
</body>
</html>

Step 5: ビューを作成(詳細ページ)

resources/views/products/show.blade.php を作成します。

<!-- 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>{{ $product['name'] }}</title>
    <style>
        body {
            font-family: sans-serif;
            max-width: 800px;
            margin: 50px auto;
            padding: 20px;
        }
        h1 {
            color: #333;
        }
        .info {
            background: #f5f5f5;
            padding: 20px;
            border-radius: 5px;
            margin: 20px 0;
        }
        .price {
            font-size: 24px;
            color: #3183ff;
            font-weight: bold;
        }
        .description {
            margin: 20px 0;
            line-height: 1.6;
        }
        .back-link {
            display: inline-block;
            margin-top: 20px;
            color: #3183ff;
            text-decoration: none;
        }
        .back-link:hover {
            text-decoration: underline;
        }
    </style>
</head>
<body>
    <h1>{{ $product['name'] }}</h1>

    <div class="info">
        <div class="price">{{ number_format($product['price']) }}円</div>
        <div class="description">{{ $product['description'] }}</div>
    </div>

    <a href="/products" class="back-link">← 一覧に戻る</a>
</body>
</html>

Step 6: 動作確認

ブラウザで以下のURLにアクセスして確認しましょう。

  • http://localhost/products → 商品一覧が表示される
  • 商品名をクリック → 詳細ページに遷移
  • 「一覧に戻る」リンクをクリック → 一覧ページに戻る
  • http://localhost/products/999 → 存在しないIDなので404エラー

✅ これでコントローラを使った基本的なページ遷移が実装できました!

【実践2】フォームからデータを受け取る

次は、フォームからデータを送信して、コントローラで受け取る処理を実装します。
商品登録フォームを作成しましょう。

Step 1: コントローラにメソッドを追加

ProductController に create() と store() メソッドを追加します。

// app/Http/Controllers/ProductController.php

// 商品登録フォームを表示
public function create()
{
    return view('products.create');
}

// フォームから送信されたデータを処理
public function store(Request $request)
{
    // 送信されたデータを取得
    $name = $request->input('name');
    $price = $request->input('price');
    $description = $request->input('description');

    // 今回は画面に表示するだけ(次の講座でDBに保存します)
    return "商品「{$name}」(価格: " . number_format($price) . "円) を受け取りました!
説明: {$description}"; }

Step 2: ルートを追加

routes/web.php にルートを追加します。

// routes/web.php

Route::get('/products/create', [ProductController::class, 'create']);
Route::post('/products', [ProductController::class, 'store']);

⚠️ 注意: /products/create/products/{id} よりも前に定義してください。
順序を間違えると、「create」がIDとして認識されてしまいます。

Step 3: 登録フォームを作成

resources/views/products/create.blade.php を作成します。

<!-- 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>
    <style>
        body {
            font-family: sans-serif;
            max-width: 600px;
            margin: 50px auto;
            padding: 20px;
        }
        h1 {
            color: #333;
        }
        form {
            background: #f5f5f5;
            padding: 30px;
            border-radius: 8px;
        }
        .form-group {
            margin-bottom: 20px;
        }
        label {
            display: block;
            margin-bottom: 5px;
            font-weight: bold;
            color: #333;
        }
        input[type="text"],
        input[type="number"],
        textarea {
            width: 100%;
            padding: 10px;
            border: 1px solid #ddd;
            border-radius: 4px;
            font-size: 16px;
            box-sizing: border-box;
        }
        textarea {
            resize: vertical;
            min-height: 100px;
        }
        button {
            background: #3183ff;
            color: white;
            padding: 12px 30px;
            border: none;
            border-radius: 4px;
            font-size: 16px;
            cursor: pointer;
        }
        button:hover {
            background: #1a6cf5;
        }
        .back-link {
            display: inline-block;
            margin-top: 20px;
            color: #3183ff;
            text-decoration: none;
        }
        .back-link:hover {
            text-decoration: underline;
        }
    </style>
</head>
<body>
    <h1>商品登録</h1>

    <form action="/products" method="POST">
        @csrf

        <div class="form-group">
            <label for="name">商品名:</label>
            <input type="text" id="name" name="name" required>
        </div>

        <div class="form-group">
            <label for="price">価格:</label>
            <input type="number" id="price" name="price" required>
        </div>

        <div class="form-group">
            <label for="description">説明:</label>
            <textarea id="description" name="description" required></textarea>
        </div>

        <button type="submit">登録</button>
    </form>

    <a href="/products" class="back-link">← 一覧に戻る</a>
</body>
</html>

💡 @csrf は、CSRF(クロスサイトリクエストフォージェリ)攻撃を防ぐためのトークンです。
フォームには必ず記述しましょう。これがないとエラーになります。

Step 4: 一覧ページに登録リンクを追加

resources/views/products/index.blade.php に登録ボタンを追加します。

<h1>商品一覧</h1>

<!-- この行を追加 -->
<p><a href="/products/create" style="background: #3183ff; color: white; padding: 10px 20px; text-decoration: none; border-radius: 4px; display: inline-block; margin-bottom: 20px;">新規登録</a></p>

<ul>
    @foreach($products as $product)
        ...
    @endforeach
</ul>

Step 5: 動作確認

  1. http://localhost/products にアクセス
  2. 「新規登録」ボタンをクリック
  3. フォームに以下を入力:
    • 商品名: スマートフォン
    • 価格: 80000
    • 説明: 最新モデルのスマートフォンです
  4. 「登録」ボタンをクリック
  5. 送信した内容が画面に表示される

✅ フォームからデータを送信し、コントローラで受け取ることができました!

🔍 補足: Requestオブジェクトの便利なメソッド

// 全てのデータを取得
$allData = $request->all();

// 特定のデータのみ取得
$data = $request->only(['name', 'price']);

// 特定のデータ以外を取得
$data = $request->except(['_token']);

// デフォルト値を指定
$category = $request->input('category', '未分類');

Request(リクエスト)とは?

コントローラで何度も登場している Request $request について理解を深めましょう。

Request = HTTPリクエストの情報が詰まった箱

ユーザーがフォームを送信したり、URLにアクセスしたりすると、
ブラウザからサーバーに「HTTPリクエスト」が送られます。
Request は、その情報をまとめて扱うためのオブジェクトです。

public function store(Request $request)
{
    // フォームから送信されたデータを取得
    $name = $request->input('name');
    $price = $request->input('price');

    // すべてのデータを取得
    $all = $request->all();
}

🤔 なぜ Request と書くだけでデータが取れるの?

答え:Laravelの「依存性注入」という仕組み

コントローラのメソッドに Request $request と書くと、
Laravelが自動的にRequestオブジェクトを作って渡してくれます。

簡単に言うと:

  1. 1. ユーザーがフォーム送信 → サーバーにリクエスト到達
  2. 2. Laravelが「このメソッドにはRequestが必要だな」と判断
  3. 3. Requestオブジェクトを自動作成して渡す
  4. 4. 開発者は受け取って使うだけ!

💡 DIは現代のフレームワークの標準機能

依存性注入(Dependency Injection / DI) は、Laravel以外のフレームワークでも広く使われている重要な設計パターンです。

  • Spring Framework (Java) - DIコンテナが中心機能
  • ASP.NET Core (C#) - 標準でDIをサポート
  • Symfony (PHP) - サービスコンテナを採用
  • NestJS (TypeScript) - Angularライクなディペンデンシーインジェクション
  • Django (Python) - ミドルウェアやビューで活用

📚 Laravelで学んだDIの概念は、他のフレームワークでも役立ちます!

📦 Request の中身を確認してみよう

dd() を使うと、Requestの中身を確認できます。

public function store(Request $request)
{
    // フォームデータを確認
    dd($request->all());

    // 出力例:
    // [
    //   "name" => "ノートPC"
    //   "price" => "80000"
    //   "description" => "高性能なノートパソコン"
    // ]
}

💡 dd($request->all()) を追加してフォーム送信すると、
どんなデータが送られてきたか確認できます。デバッグに便利!

🔑 HTMLのname属性との関係

$request->input('name') で取得する値は、
HTMLフォームの name属性 に対応しています。

HTML(ビュー)

<input
  type="text"
  name="product_name"
  value="ノートPC"
>

PHP(コントローラ)

$request->input('product_name');
// "ノートPC" が取得できる

⚠️ name属性と input() の引数は完全一致が必要です!

【実践3】バリデーションでエラーチェック

現在のフォームには入力チェックがないため、不正なデータも受け付けてしまいます。
バリデーション機能を追加して、正しいデータのみ処理できるようにしましょう。

Step 1: storeメソッドにバリデーションを追加

ProductController の store() メソッドを修正します。

// app/Http/Controllers/ProductController.php

public function store(Request $request)
{
    // バリデーション(入力チェック)
    $validated = $request->validate([
        'name' => 'required|max:100',
        'price' => 'required|integer|min:0|max:10000000',
        'description' => 'required|max:500',
    ]);

    // バリデーションが成功した場合のみここに到達
    return "商品「{$validated['name']}」(価格: " . number_format($validated['price']) . "円) を受け取りました!
説明: {$validated['description']}"; }

📚 主なバリデーションルール

  • required - 必須項目
  • max:n - 最大文字数(文字列)または最大値(数値)
  • min:n - 最小値
  • integer - 整数のみ
  • email - メール形式
  • unique:テーブル名 - 一意性チェック(DB使用時)
  • confirmed - パスワード確認フィールドと一致

🤔 バリデーション失敗時に何が起きる?

エラー時の自動処理
public function store(Request $request)
{
    $validated = $request->validate([
        'name' => 'required|max:100',
    ]);

    // ⬆️ バリデーション失敗すると、この下のコードは実行されない!
    // Laravelが自動的にリダイレクト処理を行う

    return "成功: {$validated['name']}";
}

Laravelの自動処理フロー

  1. 1. バリデーションを実行
    各フィールドがルールに合っているかチェック
  2. 2-A. 成功した場合
    検証済みデータを配列で返す → 処理続行
  3. 2-B. 失敗した場合(ここが重要!)
    Laravelが自動的に以下を実行:
    • ✅ 元のページ(フォーム)にリダイレクト
    • ✅ エラーメッセージをセッションに保存
    • ✅ 入力値(old値)をセッションに保存
    • ✅ HTTPステータス 422(Unprocessable Entity)を返す
ビューで自動的に使える変数

バリデーション実行後、ビューで以下の変数が自動的に使えます:

// resources/views/products/create.blade.php

// ❶ $errors - エラー情報(Laravelが自動的に渡す)
@if($errors->any())
    @foreach($errors->all() as $error)
        <li>{{ $error }}</li>
    @endforeach
@endif

// ❷ old() - 前回の入力値(セッションから取得)
<input name="name" value="{{ old('name') }}">

// ❸ @error - 特定フィールドのエラーチェック(Bladeディレクティブ)
@error('name')
    <span>{{ $message }}</span>
@enderror

📌 つまり

バリデーション失敗時、開発者は何もコードを書かなくても:
1. 自動的に元のページにリダイレクト
2. エラーメッセージが $errors で利用可能
3. 入力値が old() で復元可能

だから、たった数行でバリデーションが完結する!

Step 2: ビューにエラー表示を追加

resources/views/products/create.blade.php を修正します。

<h1>商品登録</h1>

<!-- エラーメッセージの全体表示(追加) -->
@if($errors->any())
    <div style="background: #ffe0e0; border: 1px solid #ff0000; padding: 15px; border-radius: 4px; margin-bottom: 20px;">
        <strong style="color: #cc0000;">入力エラーがあります:</strong>
        <ul style="margin: 10px 0 0 20px; color: #cc0000;">
            @foreach($errors->all() as $error)
                <li>{{ $error }}</li>
            @endforeach
        </ul>
    </div>
@endif

<form action="/products" method="POST">
    @csrf

    <div class="form-group">
        <label for="name">商品名: <span style="color: red;">*</span></label>
        <input type="text" id="name" name="name" value="{{ old('name') }}" required>
        <!-- 個別エラーメッセージ(追加) -->
        @error('name')
            <span style="color: red; font-size: 14px;">{{ $message }}</span>
        @enderror
    </div>

    <div class="form-group">
        <label for="price">価格: <span style="color: red;">*</span></label>
        <input type="number" id="price" name="price" value="{{ old('price') }}" required>
        @error('price')
            <span style="color: red; font-size: 14px;">{{ $message }}</span>
        @enderror
    </div>

    <div class="form-group">
        <label for="description">説明: <span style="color: red;">*</span></label>
        <textarea id="description" name="description" required>{{ old('description') }}</textarea>
        @error('description')
            <span style="color: red; font-size: 14px;">{{ $message }}</span>
        @enderror
    </div>

    <button type="submit">登録</button>
</form>

💡 old('name') は、エラー時に以前入力した値を保持する関数です。
ユーザーが再入力する手間を省けます。

Step 3: エラーメッセージを日本語化

デフォルトではエラーメッセージが英語で表示されます。
日本語化するには、Laravel-lang パッケージを使うのが一般的です。

💼 実務での日本語化:3つの方法

1️⃣ Laravel-lang パッケージ(最も一般的⭐おすすめ)

コミュニティが作成した公式日本語パッケージを使います。

# パッケージをインストール
composer require laravel-lang/common

# 日本語ファイルを追加
php artisan lang:add ja

# config/app.php でロケールを変更
'locale' => 'ja',

✅ バリデーション、認証、パスワードリセットなど、すべてのメッセージが自動的に日本語化されます

2️⃣ 言語ファイルを手動作成

自分で日本語ファイルを作成します。

# 言語ファイルを公開
php artisan lang:publish

# lang/ja/validation.php を作成して翻訳を記述

💡 完全なコントロールが可能ですが、初期設定が手間です

3️⃣ カスタムメッセージ(特定の項目だけ変更)

バリデーションごとに個別のメッセージを指定します。

$request->validate([
    'email' => 'required|email',
    'age' => 'required|integer|min:18',
], [
    'age.min' => '18歳以上の方のみご利用いただけます', // 特別なメッセージ
    // email は lang/ja/validation.php の設定を使用
]);

💡 基本は言語ファイルで日本語化し、特別なメッセージだけカスタマイズするのが実務パターン

📌 実務でのおすすめ

Laravel-lang パッケージ + 必要に応じてカスタムメッセージ
パッケージで全体を日本語化し、特別な表現が必要な箇所だけカスタムメッセージを使います。

今回の実装:Laravel-lang パッケージを使用

以下のコマンドでパッケージをインストールし、日本語化します。

# ターミナルで実行
composer require laravel-lang/common
php artisan lang:add ja

次に、config/app.php を編集してロケールを日本語に変更します。

// config/app.php

'locale' => 'ja', // 'en' から 'ja' に変更
'fallback_locale' => 'en',

✅ これで、バリデーションエラーが自動的に日本語で表示されます。
コントローラのコードを変更する必要はありません。

Step 4: 動作確認

以下のパターンでテストしてみましょう。

テストケース 期待される結果
商品名を空欄にして送信 「商品名は必須です」とエラー表示
価格に「-100」を入力 「価格は0円以上で入力してください」
価格に「abc」を入力 「価格は整数で入力してください」
説明に501文字入力 「説明は500文字以内で入力してください」
全て正しく入力 成功メッセージが表示される

✅ バリデーションで不正なデータを防ぎ、エラーをわかりやすく表示できました!

⚡ バリデーションの仕組み

  1. 1. フォームから送信されたデータをチェック
  2. 2. エラーがあれば自動的に元のページにリダイレクト
  3. 3. エラーメッセージと入力値をセッションに保存
  4. 4. ビューで @errorold() を使って表示

💡 これらは全てLaravelが自動でやってくれます!

【実践4】リダイレクトと成功メッセージ

最後に、登録が成功したら一覧ページにリダイレクトし、
成功メッセージを表示する機能を実装します。

🔄 リダイレクトとは?

リダイレクト(Redirect)は、ユーザーを別のページに自動的に移動させる仕組みです。

通常のページ遷移との違い

  • 通常: return view('products.index')
    → その場でビューを表示(URLは変わらない)
  • リダイレクト: return redirect('/products')
    → ブラウザに「別のURLに移動しろ」と指示(URLが変わる)

なぜリダイレクトが必要?

❌ ダメな例: viewで別画面を返す

public function store(Request $request)
{
    // データを保存する処理

    // 一覧ページを表示
    return view('products.index', ['products' => $products]);
}

この方法だと何が問題か?

  • 1. URLとページが一致しない
    画面は一覧ページなのに、URLは POST /products のまま
    (本来は GET /products であるべき)
  • 2. ブラウザの履歴に問題
    ブラウザの「戻る」ボタンを押したとき、POSTリクエストの履歴が残る
    → ブラウザが「フォームを再送信しますか?」と警告を出す
  • 3. リロード(F5)で再送信される!
    一覧画面でF5を押すと、ブラウザは「最後に実行したリクエスト」を再実行する。
    最後のリクエストが POST /products なので、登録処理が再度実行される!

✅ 正しい方法: リダイレクトする(PRGパターン)

public function store(Request $request)
{
    // データを保存する処理

    // 一覧ページにリダイレクト
    return redirect('/products');
}

PRGパターン(Post-Redirect-Get)とは?

  1. Post: フォームをPOSTで送信
    POST /products で登録処理を実行
  2. Redirect: リダイレクト命令を返す
    → ブラウザに「GET /products に移動しろ」と指示(HTTPステータス 302)
  3. Get: ブラウザが自動的にGETリクエストを送信
    GET /products で一覧ページを表示

これにより:

  • ✅ URLが GET /products になる(正しい状態)
  • ✅ ブラウザの履歴には GET /products が残る
  • ✅ F5でリロードしても、GET /products が実行されるだけ(登録処理は実行されない)

🔍 重要な理解: ブラウザの動作

「viewで別画面を返せば大丈夫」と思うかもしれませんが、実際には:

// この場合...
public function store(Request $request)
{
    // 登録処理
    return view('products.index'); // ← 一覧画面を表示
}

// ブラウザから見ると:
// - リクエストは POST /products のまま
// - 画面だけが一覧ページに変わっている
// - URLバーには /products と表示されているが、内部的にはPOSTリクエスト
// - F5を押すと → ブラウザは「POST /products」を再実行 → 登録処理が再度実行される!

つまり: viewで別画面を返しても、ブラウザの履歴に残るのはPOSTリクエスト
画面が変わっただけで、リクエストの種類(POST)は変わっていません。

💡 リダイレクトを使うことで、ブラウザに「新しいGETリクエストを送れ」と指示できます。
これがPRGパターンの本質です。実際には、バリデーション失敗時もLaravelが自動的にリダイレクトしています!

Step 1: storeメソッドをリダイレクトに変更

ProductController の store() メソッドを修正します。

// app/Http/Controllers/ProductController.php

public function store(Request $request)
{
    // バリデーション(入力チェック)
    // 第2引数のカスタムメッセージは、Laravel-langパッケージを使えば省略できます
    $validated = $request->validate([
        'name' => 'required|max:100',
        'price' => 'required|integer|min:0|max:10000000',
        'description' => 'required|max:500',
    ], [
        // カスタムメッセージ(学習用に明示的に記述)
        // Laravel-langを使う場合は、この配列を省略して自動翻訳されたメッセージを使えます
        'name.required' => '商品名は必須です',
        'name.max' => '商品名は100文字以内で入力してください',
        'price.required' => '価格は必須です',
        'price.integer' => '価格は整数で入力してください',
        'price.min' => '価格は0円以上で入力してください',
        'price.max' => '価格は1000万円以下で入力してください',
        'description.required' => '説明は必須です',
        'description.max' => '説明は500文字以内で入力してください',
    ]);

    // ここでは実際の保存処理は行わない(次の講座でDB保存を学びます)

    // 商品一覧ページにリダイレクトし、成功メッセージを表示
    return redirect('/products')
        ->with('success', "商品「{$validated['name']}」を登録しました!");
}

💡 with('success', 'メッセージ') で、次のページに一度だけ表示されるメッセージを送れます。
これをフラッシュメッセージと呼びます。

⚠️ 注意: view()->with() とは別のメソッドです

同じ with() という名前ですが、全く違うメソッドです。

  • view()->with('products', $products)
    → ビューにデータを渡す(そのビュー内で使える)
  • redirect()->with('success', 'メッセージ')
    → 次のページに一時的なメッセージを送る(セッションに保存、1回のみ表示)

💡 名前は同じでも、別のクラスのメソッドなので用途が違います!

Step 2: 一覧ページに成功メッセージ表示を追加

resources/views/products/index.blade.php を修正します。

<body>
    <h1>商品一覧</h1>

    <!-- 成功メッセージの表示(追加) -->
    @if(session('success'))
        <div style="background: #d4edda; border: 1px solid #28a745; color: #155724; padding: 15px; border-radius: 4px; margin-bottom: 20px;">
            ✅ {{ session('success') }}
        </div>
    @endif

    <p><a href="/products/create" style="background: #3183ff; color: white; padding: 10px 20px; text-decoration: none; border-radius: 4px; display: inline-block; margin-bottom: 20px;">新規登録</a></p>

    <ul>
        @foreach($products as $product)
            <li>
                <a href="/products/{{ $product['id'] }}">
                    {{ $product['name'] }}
                </a>
                <span class="price"> - {{ number_format($product['price']) }}円</span>
            </li>
        @endforeach
    </ul>
</body>

Step 3: 動作確認

  1. http://localhost/products にアクセス
  2. 「新規登録」ボタンをクリック
  3. フォームに正しいデータを入力して送信
  4. 商品一覧ページにリダイレクトされる
  5. 緑色の成功メッセージが表示される
  6. ページをリロードすると、メッセージが消える(フラッシュメッセージの特徴)

✅ リダイレクトとフラッシュメッセージで、ユーザーに優しいフローが作れました!

💡 フラッシュメッセージはセッションという仕組みを使っています。
セッションについては、次の【実践5】で詳しく学びます。

他のリダイレクト方法

🔄 よく使うリダイレクト

// URLでリダイレクト
return redirect('/products');

// ルート名でリダイレクト(推奨)
return redirect()->route('products.index');

// 一つ前のページに戻る
return back();

// 複数のメッセージを送る
return redirect('/products')
    ->with('success', '登録しました')
    ->with('info', '確認メールを送信しました');
 
// エラーメッセージを送る
return redirect('/products')
    ->with('error', '処理に失敗しました');

✨ 実践完了!

これで以下の機能が実装できました:

  • ✅ コントローラを使ったページ遷移
  • ✅ 一覧表示と詳細表示
  • ✅ フォームからのデータ受信
  • ✅ バリデーション(入力チェック)
  • ✅ リダイレクトとフラッシュメッセージ

💡 次の講座では、配列ではなくデータベースを使って、
実際にデータを保存・更新・削除する方法を学びます!

【実践5】セッションの仕組みを理解する

【実践4】で使ったフラッシュメッセージは、セッションという仕組みを使っています。
セッションは実務で頻繁に使う重要な概念なので、実際に動かして仕組みを理解しましょう。

🤔 セッションとは?

セッションとは、ユーザーごとの一時的な保存場所です。
ログイン状態の保持、ショッピングカート、フラッシュメッセージなどに使われます。

Step 1: セッションを使うルートを作成

まず、セッションにデータを保存するルートを作ります。

// routes/web.php

Route::get('/session-test', function () {
    // セッションにデータを保存
    session(['user_name' => '太郎']);
    session(['cart_count' => 3]);
    session(['last_page' => 'products']);

    // HTMLタグを含む文字列を返す(Laravelは自動的にHTMLとして出力する)
    return 'セッションにデータを保存しました!<br><a href="/session-show">セッションを確認</a>';
});

Route::get('/session-show', function () {
    // セッションからデータを取得
    $userName = session('user_name');
    $cartCount = session('cart_count');
    $lastPage = session('last_page');

    // 全てのセッションデータを表示
    dd(session()->all());
});

Step 2: ブラウザでアクセスして確認

  1. http://localhost/session-test にアクセス
  2. 「セッションにデータを保存しました!」と表示される
  3. 画面に表示された「セッションを確認」リンクをクリック
  4. セッションの中身が表示される(/session-show に遷移)

💡 dd() で表示されるデータに、user_name, cart_count, last_page が保存されているのが確認できます。

Step 3: ブラウザのCookieを確認

セッションIDがCookieに保存されているか確認しましょう。

  1. ブラウザで DevTools を開く (F12キー)
  2. Application タブ (Chromeの場合) をクリック
  3. 左メニューの Cookieshttp://localhost を選択
  4. laravel_session という名前のCookieが見える

📸 Cookieの内容例:

名前 laravel_session
eyJpdiI6IktVQ3ZqTU... (長い暗号化された文字列)
説明 これが「セッションID」です

⚠️ Cookieには**セッションIDだけ**が保存されています。
実際のデータ(user_name, cart_countなど)は**サーバー側**に保存されています。

Step 4: データベースでセッションを確認

Laravel 11以降では、セッションはデータベースに保存されます(デフォルト)。
実際に sessions テーブルを確認してみましょう。

🗄️ データベース確認方法:

-- SQLで確認
SELECT * FROM sessions;

-- 結果例:
id: NiWPZg8ZlznznHN7lAXy9iAs4Nt0ayDKaRTvSxGP
user_id: NULL
ip_address: 192.168.1.1
user_agent: Mozilla/5.0...
payload: YTo2OntzOjY6Il90b2tlbiI7czo0... (暗号化されたデータ)
last_activity: 1761239128

payload カラムに暗号化されたセッションデータが入っています。
この中に user_name, cart_count, last_page が保存されています。

💡 重要なポイント

  • • セッションデータはデータベースに保存される(Laravel 11以降のデフォルト)
  • • CookieにはセッションIDだけが保存される
  • • 実際のデータ(user_name等)はサーバー側のDBに保存される
  • • 開発者は保存場所を意識せず、session('key') で取得できる

Step 5: セッションの仕組みを理解する

ここまでの確認で、セッションの仕組みが理解できたと思います。

🔄 セッションの流れ

  1. 1. 初回アクセス時
    • • Laravelがランダムな「セッションID」を生成
    • • セッションIDを暗号化してCookieに保存
    • • データベースの sessions テーブルに新しいレコードを作成
  2. 2. データ保存時
    • session(['user_name' => '太郎']) を実行
    • • セッションIDに紐づいたレコードの payload カラムにデータを保存
    • • ブラウザのCookieは変わらない(セッションIDは同じ)
  3. 3. 次のリクエスト時
    • • ブラウザがCookieのセッションIDを送信
    • • LaravelがセッションIDで sessions テーブルを検索
    • payload カラムからデータを読み込んで使える

Step 6: フラッシュメッセージの仕組み

【実践4】で使ったフラッシュメッセージも、セッションを使っています。

// これは...
return redirect('/products')->with('success', '登録しました');

// 内部的にこうなっている
session()->flash('success', '登録しました');
return redirect('/products');

💡 flash() は、「次のリクエストで1回だけ取得できる」特殊なセッションデータです。
取得後は自動的に削除されます。

Step 7: セッションの保存場所(SESSION_DRIVER)

セッションはデータベース以外にも保存できます。.envSESSION_DRIVER で設定します。

保存方法 説明 用途
database データベースの sessions テーブル デフォルト(Laravel 11以降)
redis Redisサーバー 高速アクセスが必要な場合
memcached Memcachedサーバー 高速アクセスが必要な場合
file storage/framework/sessions/ 古いバージョンのデフォルト

✅ Laravel 11からの変更点

Laravel 11以降では、デフォルトが database になりました。
Laravel 10以前では file がデフォルトでしたが、現在はデータベース保存が標準です。

設定ファイル: config/session.php または .envSESSION_DRIVER

セッションの基本的な使い方

// データを保存
session(['key' => 'value']);
session()->put('key', 'value');

// データを取得
$value = session('key');
$value = session('key', 'デフォルト値');
$value = session()->get('key');

// 全てのデータを取得
$all = session()->all();

// データが存在するか確認
if (session()->has('key')) {
    // ...
}

// データを削除
session()->forget('key');
session()->flush();  // 全削除

// フラッシュデータ(1回だけ)
session()->flash('message', '成功しました');
return redirect('/products')->with('success', '登録しました');

✅ セッションの仕組みが理解できました!
ログイン機能やショッピングカートなど、実務で頻繁に使う重要な技術です。

💡 重要なポイント

  • • セッションはユーザーごとに独立している
  • • CookieにはセッションIDだけが保存される
  • • 実際のデータはサーバー側に保存される
  • • フラッシュメッセージは1回だけ表示されるセッション
  • • 本番環境ではdatabaseredisを推奨

セッションはどこで使われている?

実は、これまでの講座ですでにセッションを使っていました!
知らずに使っていた場所を確認してみましょう。

📌 1. CSRF トークン (@csrf)

【実践2】【実践3】で使った @csrf は、セッションを使っています!

<form action="/products" method="POST">
    @csrf  <!-- これがセッションを使っている! -->
    ...
</form>

仕組み:

  1. 1. フォーム表示時
    • • Laravelがランダムなトークンを生成
    • • トークンをセッションに保存
    • @csrf が hidden input として出力
  2. 2. フォーム送信時
    • • セッションのトークンと送信されたトークンを照合
    • • 一致すれば処理続行、不一致なら 419 エラー
    • • CSRF攻撃を防ぐ仕組み

確認してみる:

// コントローラで
dd(session()->all());

// 出力に "_token" => "KhGxT8ZqF..." が入っている!

💡 @csrf を書くだけで、Laravelが自動的にセッションを使ってセキュリティを守ってくれています!

📌 2. old() 関数(入力値の保持)

【実践3】で使った old('name') も、セッションを使っています!

<input type="text" name="name" value="{{ old('name') }}">
<!-- エラー時、以前入力した値が表示される -->

仕組み:

  1. 1. バリデーションエラー時
    • • 送信されたデータをセッションに保存(フラッシュデータ)
    • • エラーページにリダイレクト
  2. 2. エラーページ表示時
    • old('name') がセッションから値を取得
    • • フォームに以前の入力値が表示される
    • • 次のリクエストで自動削除される(フラッシュデータ)

確認してみる:

// バリデーションエラー時のセッション
dd(session()->all());

// "_old_input" => [
//     "name" => "入力した商品名",
//     "price" => "入力した価格",
// ]

💡 ユーザーが再入力する手間を省くために、Laravelが自動的にセッションを使ってくれています!

📌 3. エラーメッセージ (@error)

【実践3】で使った @error('name') も、セッション経由です!

@error('name')
    <span style="color: red;">{{ $message }}</span>
@enderror

バリデーションエラー時、エラーメッセージはセッションに保存され、
次のページで $errors として取得できます。

💡 これもフラッシュデータなので、1回表示されると自動的に削除されます。

✨ まとめ: セッションの重要性

セッションは、Laravelの至る所で使われている基盤技術です。

  • すでに使っている: CSRF、old()、エラーメッセージ、フラッシュメッセージ
  • 自動的に動く: Laravelが裏側でセッションを管理してくれる
  • 意識して使う: カスタム機能でセッションを活用できる

💡 セッションの仕組みを理解することで、Laravelの動作原理が深く理解できます!

まとめ

この章では、Laravelのコントローラ(Controller)について学びました。

重要なポイント

  • コントローラは app/Http/Controllers/ に配置
  • php artisan make:controller で作成
  • ルートファイルから [クラス::class, 'メソッド'] で呼び出し
  • --resource オプションでCRUD操作の7メソッドを自動生成
  • Route::resource() で7つのルートを一括定義
  • Request オブジェクトでフォームデータを取得
  • validate() で入力チェック

次のステップ

コントローラでビジネスロジックを整理できるようになりました。
次はモデル(Model)とデータベースについて学び、 実際にデータを保存・取得できるようにしていきます。

補足:クッキー(Cookie)とは?(読まなくてもOK)

※ この説明はフロントエンド寄りの内容のため、本編では省略しました。興味がある方だけお読みください。

クッキーの正体

【実践5】で「Cookie」という言葉が出てきましたが、クッキーとは何でしょうか?

🍪 クッキー(Cookie)とは

ブラウザに保存される小さなテキストデータです。
サーバーが「このブラウザは誰か」を識別するために使います。

現実世界の「会員証」に似ています:

  • 初回来店: お店が会員証を発行
    → 次回から、この証明書で本人確認
  • 次回来店: 会員証を提示
    → お店が「この人は○○さんだ」と識別

Webの世界:
サーバーが「Cookie」をブラウザに保存し、次回アクセス時に自動的に送信することで、
「このブラウザは前回来た人だ」と識別できます。

クッキーの具体的な流れ

📌 初回アクセス時

  1. あなた: http://localhost:86 にアクセス
  2. サーバー: 「初めてのお客さんだな。識別用のIDを発行しよう」
    → レスポンスヘッダーに追加: Set-Cookie: laravel_session=暗号化されたID
  3. ブラウザ: 「了解!このIDを保存しておこう」
    → ブラウザ内部に保存(F12のApplicationタブで確認可能)

📌 2回目以降のアクセス

  1. あなた: http://localhost:86 に再度アクセス
  2. ブラウザ: 「前回もらったIDがあるぞ。自動で送っておこう」
    → リクエストヘッダーに自動追加: Cookie: laravel_session=暗号化されたID
  3. サーバー: 「このIDは前回来た人だ!セッションデータを確認しよう」
    → IDでデータベースを検索 → ログイン状態やカート内容を取得

クッキーとセッションの関係

よくある誤解を整理しましょう。

❌ よくある誤解

「セッションはサーバー、クッキーはブラウザ」
→ 半分正解、半分間違い

✅ 正しい理解

セッションデータ サーバー(データベース)に保存
セッションID クッキーに保存
役割 クッキーでIDを受け渡し → サーバーがデータ取得

クッキーの主な用途

  • 1. ログイン状態の維持
    セッションIDをクッキーに保存 → ページ移動してもログイン状態を維持
  • 2. ショッピングカート
    カート情報はサーバーに保存、識別用IDはクッキーに保存
  • 3. ユーザー設定
    言語設定、テーマ(ダークモード)などを保存

クッキーがないとどうなる?

⚠️ クッキーなしの世界

  • ❌ ページ移動するたびにログアウトされる
  • ❌ ショッピングカートが使えない(商品を追加しても次のページで消える)
  • ❌ ユーザー設定が保存できない(毎回選び直し)
  • ❌ パーソナライズ(おすすめ商品など)が不可能

💡 クッキーのおかげで、便利なWebサイトが実現できています!

セキュリティ機能

Laravelは、クッキーに以下のセキュリティ設定を自動で付けてくれます:

設定 効果
HttpOnly JavaScriptから読めなくする(XSS対策)
Secure HTTPS通信でのみ送信(盗聴防止)
SameSite 他サイトからのリクエストで送信しない(CSRF対策)

💡 重要なポイント

  • • クッキーはブラウザに保存される小さなテキストデータ
  • • サーバーがブラウザを識別するための「会員証」のようなもの
  • 自動的に送受信される(開発者が手動で送る必要なし)
  • • セッションIDをクッキーに保存することで、ログイン状態を維持
  • • Laravelが自動的にセキュリティ設定してくれる

補足:セッションIDの暗号化(読まなくてもOK)

※ この説明は理解しなくても、セッションは使えます。興味がある方だけお読みください。

データベースのIDとCookieの値が違う理由

【実践5】で見たように、データベースの sessions テーブルと、
ブラウザのCookieに保存されている値は異なります

📊 実際の例

データベース(sessions テーブル):

id: NiWPZg8ZlznznHN7lAXy9iAs4Nt0ayDKaRTvSxGP

ブラウザのCookie(laravel_session):

eyJpdiI6IktVQ3ZqTU1rN0Zsb3BFcjJscTZ2cDJWazZvdkZVZVQwRkYyYkhRUEJyIiwid...

なぜ違うのか?

答えはセキュリティのためです。
Laravelは、Cookieに保存する前にセッションIDを暗号化しています。

🔒 暗号化の流れ

  1. 1. セッションID生成
    • • LaravelがランダムなセッションIDを生成
    • • 例: NiWPZg8ZlznznHN7lAXy9iAs4Nt0ayDKaRTvSxGP
  2. 2. データベースに保存
    • sessions テーブルの id カラムに保存
    • 生のIDのまま保存
  3. 3. 暗号化してCookieに送信
    • .envAPP_KEY を使って暗号化
    • • 例: eyJpdiI6IktVQ3ZqTU... に変換
    • • ブラウザに laravel_session という名前で送信
  4. 4. 次のリクエストで復号化
    • • ブラウザから暗号化された値を受信
    • APP_KEY を使って復号化
    • • 生のID(NiWPZg8Z...)を取得
    • • データベースを検索してセッションデータを取得

なぜ暗号化が必要なのか?

❌ もし暗号化しなかったら...

  • 1. セッションハイジャック攻撃
    攻撃者がCookieのセッションIDを盗んで、他人になりすますことができる
  • 2. セッションID予測攻撃
    IDのパターンを推測して、他人のセッションにアクセスできる可能性
  • 3. Cookieの改ざん
    攻撃者が自由にセッションIDを書き換えて、不正アクセスできる

✅ 暗号化することで...

  • 1. IDの中身が見えない
    暗号化された文字列からは、元のIDを推測できない
  • 2. 改ざん検知
    Cookieが改ざんされると、復号化に失敗して不正を検知できる
  • 3. APP_KEYが必要
    攻撃者は APP_KEY を知らないため、復号化できない
  • 4. セッション固定攻撃を防ぐ
    ログイン時にセッションIDを再生成することで、攻撃を無効化

APP_KEYの重要性

.env ファイルの APP_KEY は、セッションIDだけでなく、
Laravel全体の暗号化に使われる超重要な秘密鍵です。

# .env
APP_KEY=base64:Vi0fyb2hqQBrmpYNqzqgmddOqctT0B8GIAq/aNalhHA=

⚠️ 重要な注意事項

  • APP_KEY絶対に公開しない(GitHubなどにpushしない)
  • APP_KEY を変更すると、既存のセッションが無効になる
  • • 本番環境では、APP_KEY を定期的に更新することを推奨

まとめ

  • • データベースには生のセッションIDが保存される
  • • Cookieには暗号化されたセッションIDが保存される
  • • 暗号化はセキュリティのため(なりすまし、改ざん防止)
  • APP_KEY が暗号化と復号化に使われる
  • • 開発者は暗号化を意識せず、session() で簡単に使える

💡 Laravelが自動的にセキュリティを守ってくれているおかげで、安全なWebアプリが作れます!