【第四弾】Laravel入門資料

📚 Laravel公式ドキュメント - Eloquent ORM

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

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

モデル(Model)とは

モデルとは、データベースのテーブルを操作するためのPHPクラスです。
SQL文を書かずに、PHPのコードでデータベースを操作できます。

💡 イメージ

  • productsテーブル → Productモデル
  • usersテーブル → Userモデル
  • • モデルを使えば、SQL文なしでデータ操作できる

まずは手を動かして、モデルを作って使ってみましょう。理屈は後から理解します。

【ステップ1】モデルを作成しよう

💡 前提

マイグレーションの章で、既に以下を完了しています:
categories テーブル作成
products テーブル作成(category_idの外部キー含む)
• CategorySeeder と ProductSeeder でテストデータ投入

※ もし忘れてしまった場合は、マイグレーションの章を見返してください。

モデル作成コマンド

# Productモデルを作成
php artisan make:model Product

これで app/Models/Product.php が作られます。

⭐ 重要: $fillable を設定

app/Models/Product.php を開いて、$fillable を追加します。
これがないと create() でエラーになります。

<?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',
    ];
}

⚠️ $fillable とは?

Product::create([...]) で一括代入できるカラムを指定するものです。
セキュリティ対策として、明示的に許可したカラムだけ代入可能にします。

🔒 なぜセキュリティ上必要なのか?
悪意のあるユーザーが、予期しないカラムを勝手に変更できてしまう危険性があるためです。
例: ユーザー情報更新時に、is_admin というカラムを勝手に true にして管理者権限を取得する攻撃など。
$fillable で許可したカラムだけ受け付けることで、この攻撃を防げます。

これを設定しないと:
MassAssignmentException エラーが出ます

💡 補足: $guarded(ブラックリスト方式)もありますが、実務では$fillableを使うのが一般的です。

✅ モデルの準備完了!これで Eloquent ORM を使えます。
※ Eloquent ORM については後ほど詳しく説明します。

【ステップ2】Tinkerで実際に操作してみよう

Tinkerは、コマンドラインでPHPコードを実行できるツールです。
実際にデータベースを操作しながら、モデルの使い方を覚えましょう。

Tinker起動

php artisan tinker

終了する時は exit または Ctrl + C

【実践1】データを取得してみる

// 全件取得
>>> $products = App\Models\Product::all();
=> Illuminate\Database\Eloquent\Collection {
     all: [
       App\Models\Product {
         id: 1,
         name: "ノートPC",
         price: 120000,
       },
       App\Models\Product {
         id: 2,
         name: "ワイヤレスマウス",
         price: 3000,
       },
       // ...
     ],
   }

// 件数確認
>>> $products->count();
=> 3

// IDで1件取得
>>> $product = App\Models\Product::find(1);
=> App\Models\Product {
     id: 1,
     name: "ノートPC",
     price: 120000,
     stock: 10,
   }

// 属性にアクセス
>>> $product->name;
=> "ノートPC"

>>> $product->price;
=> 120000

💡 ポイント:
Product::all()Product::find() で取得
$product->name でカラムの値を取得
• SQL文を書いていないのに、データが取れた!

【実践2】条件を指定して取得

// 価格が10000円より高い商品
>>> $products = App\Models\Product::where('price', '>', 10000)->get();
=> Illuminate\Database\Eloquent\Collection {
     all: [
       App\Models\Product {
         id: 1,
         name: "ノートPC",
         price: 120000,
       },
     ],
   }

// 最初の1件だけ
>>> $product = App\Models\Product::where('price', '<', 5000)->first();
=> App\Models\Product {
     id: 2,
     name: "ワイヤレスマウス",
     price: 3000,
   }

// 件数だけ知りたい
>>> App\Models\Product::where('price', '>', 10000)->count();
=> 1

【実践3】データを作成してみる

// 新しい商品を作成
>>> $product = App\Models\Product::create([
...   'name' => 'Bluetoothキーボード',
...   'description' => '打ちやすいキーボード',
...   'price' => 8000,
...   'stock' => 15,
...   'is_published' => true,
...   'category_id' => 1,  // 家電
... ]);
=> App\Models\Product {
     id: 4,
     name: "Bluetoothキーボード",
     price: 8000,
     // ...
   }

// 確認
>>> App\Models\Product::find(4)->name;
=> "Bluetoothキーボード"

💡 $fillable のおかげで動く:
さっき設定した $fillable があるから、create() が動きました。
なければ MassAssignmentException エラーになります。

【実践4】データを更新してみる

// IDで取得
>>> $product = App\Models\Product::find(4);

// 価格を変更
>>> $product->price = 7500;

// 保存
>>> $product->save();
=> true

// 確認
>>> $product->price;
=> 7500

// 別の方法: update() メソッド
>>> $product->update(['stock' => 20]);
=> true

【実践5】データを削除してみる

// IDで取得
>>> $product = App\Models\Product::find(4);

// 削除
>>> $product->delete();
=> true

// 確認(nullになる)
>>> App\Models\Product::find(4);
=> null

✅ データの取得・作成・更新・削除ができました!

【ステップ3】いま何をしたか理解しよう

ORM(Object-Relational Mapping)とは

さっきやったことが、まさにORMです。

📊 ORMの仕組み

データベース ORM(PHP)
products テーブル Product モデル
1行のデータ $product オブジェクト
name カラム $product->name
SELECT * FROM products Product::all()

つまり:
SQL文を書かずに、PHPのオブジェクトとしてデータベースを操作できる仕組みです。

💡 LaravelのORM = Eloquent ORM
Laravelでは、ORMのことを Eloquent ORM(エロクアント)と呼びます。

💡 他のフレームワークにもORMはある
• Ruby on Rails → Active Record
• Django (Python) → Django ORM
• Symfony (PHP) → Doctrine
• ASP.NET → Entity Framework

ORMは、現代のWebフレームワークでは必ずと言っていいほど備わっている仕組みです。

「::」と「->」の使い分け

⚠️ 初心者がよく混乱するポイント

::-> の使い分けは、Laravel初心者が必ずつまずく箇所です。
ここでしっかり理解しておきましょう。

さっきのTinkerで、2種類の記号を使いました。

💡 シンプルな理解

:: (ダブルコロン) = 新しく作る

  • ✅ データを検索する時 → Product::where(...)
  • ✅ 新しいデータを作る時 → Product::create(...)
  • ✅ IDで探す時 → Product::find(1)

-> (アロー) = すでにあるものを操作

  • ✅ データを見る時 → $product->name
  • ✅ データを変更する時 → $product->price = 15000
  • ✅ 保存する時 → $product->save()
  • ✅ 削除する時 → $product->delete()

終端メソッド - いつSQLが実行される?

さっき where() の後に get()first() を書きましたね。
実は、get() や first() を呼ぶまでSQLは実行されません

💡 終端メソッド = SQLを実行するメソッド

// ここまではSQL未実行(クエリを組み立てているだけ)
$query = Product::where('price', '>', 10000);

// get() を呼んで初めてSQL実行される
$products = $query->get();  // ← ここで実行

// first() も終端メソッド
$product = $query->first();  // ← ここで実行

// count() も終端メソッド
$count = $query->count();  // ← ここで実行

主な終端メソッド:
get() - 複数件取得
first() - 最初の1件
find(id) - IDで1件
count() - 件数
exists() - 存在チェック

💡 終端メソッドについて詳しくは、後のセクション「いつSQLが実行される?」で解説します。

実務でよく使うメソッド一覧

ここまでで基本的な操作ができるようになりました。
実務でよく使うメソッドを重要度順に紹介します。

📋 重要度別メソッド一覧

重要度 メソッド 説明
★★★ all() 全件取得 Product::all()
★★★ find() IDで1件取得 Product::find(1)
★★★ where() 条件指定 where('price', '>', 1000)
★★★ get() 検索結果を取得 where(...)->get()
★★★ first() 最初の1件 where(...)->first()
★★★ create() 新規作成 Product::create([...])
★★★ save() 保存 $product->save()
★★★ update() 更新 $product->update([...])
★★★ delete() 削除 $product->delete()
★★☆ orderBy() 並び替え orderBy('price', 'desc')
★★☆ count() 件数取得 Product::count()
★★☆ with() リレーション取得 Product::with('category')
★☆☆ pluck() 特定カラムのみ Product::pluck('name')
★☆☆ sum() 合計 Product::sum('price')

💡 重要度の基準:
★★★ = 必須(CRUD操作で毎日使う)
★★☆ = よく使う(実務で頻繁に使う)
★☆☆ = たまに使う(特定の場面で使う)

いつSQLが実行される?終端メソッドを理解しよう

Eloquent ORMでは、メソッドチェーンで繋いでいる間はSQL文は発行されません
最後に終端メソッドを呼んで初めて、データベースにSQLが送られます。

💡 終端メソッド = SQLを実行するメソッド

// ここまではSQL未発行(クエリを組み立てているだけ)
$query = Product::where('price', '>', 10000)
    ->where('is_published', true)
    ->orderBy('created_at', 'desc');

// この時点ではまだデータベースに問い合わせていない

// get() を呼んで初めてSQL実行される ← 終端メソッド
$products = $query->get();  // SELECT * FROM products WHERE ...

// first() も終端メソッド
$product = $query->first();  // SELECT * FROM products WHERE ... LIMIT 1

💡 つまり:
• メソッドチェーン中 = クエリを組み立てているだけ
• 終端メソッド呼び出し = その瞬間にSQL実行
終端メソッドを呼ぶまでデータは取得されない

主な終端メソッド一覧

よく使う終端メソッドを覚えましょう。

📋 終端メソッド(SQLを実行する)

メソッド 何を返す?
get() 複数件のコレクション Product::where(...)->get()
first() 最初の1件 Product::where(...)->first()
find(id) 指定IDの1件 Product::find(1)
count() 件数(数値) Product::where(...)->count()
pluck('name') 特定カラムの配列 Product::pluck('name')
sum('price') 合計値 Product::sum('price')
max('price') 最大値 Product::max('price')
min('price') 最小値 Product::min('price')
avg('price') 平均値 Product::avg('price')
exists() 存在チェック(true/false) Product::where(...)->exists()

終端メソッドでないもの(チェーンできる)

これらはSQL未実行。さらにメソッドを繋げられます。

🔗 チェーン可能なメソッド(SQL未実行)

メソッド 役割
where() 条件追加 where('price', '>', 1000)
orderBy() 並び替え orderBy('created_at', 'desc')
limit() 件数制限 limit(10)
select() 取得カラム指定 select('name', 'price')
with() リレーション指定 with('category')

💡 これらはいくらでも繋げられる:

// 全部繋げてもSQL未実行
$query = Product::where('price', '>', 10000)
    ->where('is_published', true)
    ->orderBy('created_at', 'desc')
    ->limit(10)
    ->select('name', 'price')
    ->with('category');

// ここまでSQL未実行

// get() で初めてSQL実行
$products = $query->get();  // ← ここで実行される

【Tinkerで確認】実際に試してみよう

Tinkerで、いつSQLが実行されるか確認できます。

🧪 Tinkerで実験

php artisan tinker
// クエリログを有効化
>>> DB::enableQueryLog();

// where() や orderBy() を書く(SQL未実行)
>>> $query = App\Models\Product::where('price', '>', 10000)->orderBy('price', 'desc');

// まだログは空
>>> DB::getQueryLog();
=> []  // 空!まだSQL実行されていない

// get() で実行
>>> $products = $query->get();

// ログを確認
>>> DB::getQueryLog();
=> [
     [
       "query" => "select * from `products` where `price` > ? order by `price` desc",
       "bindings" => [10000],
       "time" => 1.23,
     ],
   ]  // ← get() を呼んだ時にSQLが実行された!

💡 確認できたこと:
where(), orderBy() = SQL未実行
get() = その瞬間にSQL実行
終端メソッドを呼ぶまで、データベースには何も問い合わせていない

【Tinkerで確認】pluck() と バインド値

他の終端メソッドも試してみましょう。

🧪 pluck() を試す

// 特定のカラムだけ取得
>>> App\Models\Product::pluck('name');
=> Illuminate\Support\Collection {
     all: [
       "ノートPC",
       "ワイヤレスマウス",
       "オーガニックコーヒー",
       "Laravel入門書",
     ],
   }

// IDをキーにして名前を取得
>>> App\Models\Product::pluck('name', 'id');
=> Illuminate\Support\Collection {
     all: [
       1 => "ノートPC",
       2 => "ワイヤレスマウス",
       3 => "オーガニックコーヒー",
       4 => "Laravel入門書",
     ],
   }

💡 pluck() とは?

特定のカラムの値だけを配列で取得する終端メソッドです。
全部のデータは不要で、名前のリストだけ欲しい時などに便利です。

使い分け:
pluck('name') → 名前だけの配列
pluck('name', 'id') → IDをキーにした名前の配列(セレクトボックスなどで使う)

🧪 バインド値を確認する

// クエリを作成(SQL未実行)
>>> $query = App\Models\Product::where('price', '>', 10000);

// SQLの構造を確認(?がある)
>>> $query->toSql();
=> "select * from `products` where `price` > ?"

// バインド値を確認(?に入る実際の値)
>>> $query->getBindings();
=> [
     10000,
   ]

// 問題なければ実行
>>> $products = $query->get();

💡 バインド値とは?

SQL文の ? の部分に入る実際の値のことです。
上の例では、where('price', '>', 10000)10000 がバインド値です。

なぜ?にするのか:
SQLインジェクション対策のためです。
値を直接SQL文に埋め込まず、安全に渡すための仕組みです。

デバッグ時の使い方:
toSql() → SQL文の構造を確認
getBindings() → 実際の値を確認
→ 両方見ることで、実行されるSQLの全体像が分かります

なぜこの仕組みが便利なのか?

⚙️ 遅延実行のメリット

【メリット1】条件を動的に追加できる

$query = Product::query();

if ($request->has('min_price')) {
    $query->where('price', '>=', $request->min_price);
}

if ($request->has('category_id')) {
    $query->where('category_id', $request->category_id);
}

// 最後に一度だけSQL実行
$products = $query->get();

【メリット2】同じクエリで違う終端メソッドを使える

$query = Product::where('is_published', true);

// 同じクエリで件数を取得
$count = $query->count();  // SQL実行1回目

// 同じクエリでデータ取得
$products = $query->get();  // SQL実行2回目

// 同じクエリで最初の1件
$first = $query->first();  // SQL実行3回目

【メリット3】デバッグしやすい

$query = Product::where('price', '>', 10000);

// SQLを確認できる(まだ実行されていない)
echo $query->toSql();  // select * from `products` where `price` > ?

// バインド値も確認
dd($query->getBindings());  // [10000]

// 問題なければ実行
$products = $query->get();

💡 まとめ:
• 必要になるまでSQLを発行しないので効率的
• 条件を柔軟に組み立てられる
• デバッグがしやすい
終端メソッドを呼んで初めて実行される

💡 覚え方: get, first, count, pluck などが終端メソッド。これを呼ぶまでデータは取得されない!

なぜEloquent ORMを使うのか? - SQL → クエリビルダー → ORMの進化

Laravelでデータベース操作をする方法は、実は3つあります。
それぞれに問題があり、最終的にEloquent ORMが生まれました。

【方法1】生SQL - 最も危険な方法

書き方

use Illuminate\Support\Facades\DB;

// 全件取得
$products = DB::select('SELECT * FROM products');

// 条件付き取得
$products = DB::select('SELECT * FROM products WHERE price > ?', [10000]);

// ❌ 危険な書き方(SQLインジェクションの危険性)
$products = DB::select("SELECT * FROM products WHERE name = '$name'");

❌ 最大の問題: SQLインジェクションのリスク

SQL文を文字列で組み立てると、SQLインジェクション攻撃を受ける可能性があります。
悪意のあるユーザーが、データベースを不正に操作したり、データを盗んだりできてしまいます。

【方法2】クエリビルダー - まだリスクが残る

書き方

use Illuminate\Support\Facades\DB;

// ✅ 安全な書き方
$products = DB::table('products')->where('price', '>', 10000)->get();

// ❌ 危険な書き方(whereRaw を使うとSQLインジェクションのリスク)
$products = DB::table('products')
    ->whereRaw("name = '$name'")  // 危険!
    ->get();

⚠️ 問題1: whereRaw() でSQLインジェクションのリスクが残る

基本的には安全ですが、whereRaw()selectRaw() などの「Raw」メソッドを使うと、
生SQL文を書くことになり、SQLインジェクションの危険性が残ります

⚠️ 問題2: リレーションが使えない

商品のカテゴリー名を取得したい場合、クエリビルダーだとJOINを手動で書く必要があります。

// クエリビルダー: JOINを手動で書く必要がある
$products = DB::table('products')
    ->join('categories', 'products.category_id', '=', 'categories.id')
    ->select('products.*', 'categories.name as category_name')
    ->get();

// 結果は配列(オブジェクトではない)
foreach ($products as $product) {
    echo $product->category_name;  // categoryオブジェクトではなく文字列
}

🧪 Tinkerで実際に試してみよう

php artisan tinker
// クエリビルダーでJOIN
>>> $products = DB::table('products')
...     ->join('categories', 'products.category_id', '=', 'categories.id')
...     ->select('products.*', 'categories.name as category_name')
...     ->limit(2)
...     ->get();

=> Illuminate\Support\Collection {
     all: [
       {
         +"id": 1,
         +"name": "ノートPC",
         +"description": "高性能なノートパソコン",
         +"price": 120000,
         +"stock": 10,
         +"is_published": 1,
         +"category_id": 1,
         +"created_at": "2025-10-26 23:30:29",
         +"updated_at": "2025-10-26 23:30:29",
         +"category_name": "家電",  // ← 文字列として追加されただけ
       },
       {
         +"id": 2,
         +"name": "ワイヤレスマウス",
         +"description": "静音設計のマウス",
         +"price": 3000,
         +"stock": 50,
         +"is_published": 1,
         +"category_id": 1,
         +"created_at": "2025-10-26 23:30:29",
         +"updated_at": "2025-10-26 23:30:29",
         +"category_name": "家電",
       },
     ],
   }

// カテゴリー名を取得する場合
>>> $products[0]->category_name;
=> "家電"  // ただの文字列

// カテゴリーオブジェクトは存在しない
>>> $products[0]->category;
=> null  // ❌ リレーションではないので取れない

💡 問題点:
• データが平坦(フラット)になってしまう
• category_name は文字列として追加されただけ
• カテゴリーの他の情報(id、created_atなど)は取得できない
JOINのSQL文を毎回書く必要がある

Eloquent ORMなら非常にシンプル:

// Eloquent: リレーションで簡単に取得
$products = Product::with('category')->get();

foreach ($products as $product) {
    echo $product->category->name;  // リレーションで自動取得
}

🧪 Eloquentの結果(リレーション使用)

>>> $products = App\Models\Product::with('category')->limit(2)->get();

=> Illuminate\Database\Eloquent\Collection {
     all: [
       App\Models\Product {
         id: 1,
         name: "ノートPC",
         description: "高性能なノートパソコン",
         price: 120000,
         stock: 10,
         is_published: 1,
         category_id: 1,
         created_at: "2025-10-26 23:30:29",
         updated_at: "2025-10-26 23:30:29",
         category: App\Models\Category {  // ← オブジェクトとして取得!
           id: 1,
           name: "家電",
           created_at: "2025-10-26 23:30:29",
           updated_at: "2025-10-26 23:30:29",
         },
       },
       App\Models\Product {
         id: 2,
         name: "ワイヤレスマウス",
         description: "静音設計のマウス",
         price: 3000,
         stock: 50,
         is_published: 1,
         category_id: 1,
         created_at: "2025-10-26 23:30:29",
         updated_at: "2025-10-26 23:30:29",
         category: App\Models\Category {
           id: 1,
           name: "家電",
           created_at: "2025-10-26 23:30:29",
           updated_at: "2025-10-26 23:30:29",
         },
       },
     ],
   }

// カテゴリー名を取得
>>> $products[0]->category->name;
=> "家電"

// カテゴリーの他の情報も取得可能
>>> $products[0]->category->id;
=> 1

>>> $products[0]->category->created_at;
=> Illuminate\Support\Carbon @1730020229 {
     date: 2025-10-26 23:30:29.0 UTC (+00:00),
   }

💡 Eloquentの利点:
• カテゴリーがオブジェクトとして取得される
• カテゴリーの全ての情報にアクセス可能
$product->category->name のように直感的に書ける
JOINのSQLを書く必要がない

⚠️ 問題3: モデルの機能が使えない

クエリビルダーの返り値はただの配列なので、モデルに定義した便利な機能が使えません。

// 例: 価格を税込みで表示したい場合

// クエリビルダー: 毎回計算が必要
$products = DB::table('products')->get();
foreach ($products as $product) {
    echo $product->price * 1.1;  // 毎回計算を書く必要がある
}

// Eloquent: モデルにカスタムメソッドを定義すれば再利用できる
// Product.php
public function getPriceWithTax() {
    return $this->price * 1.1;
}

// 使う側
$products = Product::all();
foreach ($products as $product) {
    echo $product->getPriceWithTax();  // スッキリ!明示的!
}

⚠️ 問題4: コードの可読性が低い

テーブル名を文字列で書く必要があるため、タイポに気づきにくいです。

// クエリビルダー: テーブル名が文字列(タイポしても気づかない)
DB::table('producst')->get();  // タイポ!でもエラーにならず実行時に失敗

// Eloquent: クラス名なのでIDEが補完してくれる
Product::all();  // タイポしたら即座にエラーになる

💡 まとめ: クエリビルダーを避ける理由
• whereRaw() でSQLインジェクションのリスク
• リレーションを使うにはJOINを手動で書く必要がある
• モデルの便利な機能(アクセサ、スコープなど)が使えない
• 返り値が配列なので型安全ではない
• テーブル名が文字列なのでタイポに気づきにくい

【方法3】Eloquent ORM - 最も安全な方法

書き方

use App\Models\Product;

// 全件取得
$products = Product::all();

// 条件付き取得
$products = Product::where('price', '>', 10000)->get();

// ユーザー入力を使っても安全
$products = Product::where('name', $request->name)->get();  // ✅ 自動でエスケープされる

✅ 最大のメリット: SQLインジェクションのリスクがない

Eloquent ORMは、すべての入力値を自動的にエスケープします。
開発者が意識しなくても、SQLインジェクション対策が完璧に行われます。
whereRaw() のような危険なメソッドを使う必要がありません。

その他のメリット

  • created_at / updated_at が自動
  • リレーション(関連データ)が簡単 - $product->category で取得
  • • IDE の補完が効く
  • • より直感的で読みやすい

3つの方法の比較

項目 生SQL クエリビルダー Eloquent ORM
SQLインジェクション 危険 whereRaw()で危険 安全
書き方 DB::select('SELECT...') DB::table('products') Product::where(...)
タイムスタンプ 手動 手動 自動
リレーション JOIN文を書く join()メソッド $product->category

💡 結論: できるだけ Eloquent ORM を使おう

Laravelでは、基本的にEloquent ORMを使って書くのが推奨されます。
最大の理由はセキュリティです。SQLインジェクションのリスクを開発者が意識せずに回避できます。

生SQLやクエリビルダーを使う場合:
• 複雑な集計やサブクエリで、Eloquentでは書きづらい時
• 大量データの一括更新/削除(Eloquentより高速)
⚠️ ただし、whereRaw() などを使う時は、SQLインジェクション対策を忘れずに!

リレーションシップの実践

ここからは、実際にリレーションシップを定義して、Tinkerで動作を確認していきます。
Product(商品)を中心に、Category(カテゴリー)ProductDetail(商品詳細)Tag(タグ)との関係を学びます。

⚠️ 先ほどのwith()エラーについて

「なぜEloquent ORM」のセクションでProduct::with('category')を試した際、エラーが出ました。
理由は以下の2つです:

1️⃣ Categoryモデルがまだ存在しない
2️⃣ Productモデルにcategory()リレーションが定義されていない

これから順番に解決していきます!

【準備】Categoryモデルを作成

categoriesテーブルは既に存在しているため、モデルだけを作成します。

php artisan make:model Category

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;

    // まだ設定していないため、Categoryにもfillableを設定
    protected $fillable = ['name'];
}

💡 カテゴリーデータについて

カテゴリーデータは既にSeederで作成済みです。
CategorySeederで「家電」「食品」「衣類」「書籍」の4つが登録されています。

📚 リレーションメソッドの種類

Laravelでは、テーブル間の関係を表現するために、いくつかのリレーションメソッドが用意されています。

メソッド 関係性
hasMany() 1対多(1つが複数を持つ) 1つのカテゴリーは複数の商品を持つ
belongsTo() 多対1(多くが1つに属する) 複数の商品は1つのカテゴリーに属する
hasOne() 1対1(1つが1つを持つ) 1つの商品は1つの詳細情報を持つ
belongsToMany() 多対多(多くが多くと関連) 複数の商品は複数のタグを持ち、1つのタグは複数の商品に紐づく

💡 Product::class とは?

Product::classは、完全修飾クラス名(名前空間 + クラス名)を文字列で返すPHPの機能です。

Product::class
// ↓ 実際にはこの文字列を返す
"App\Models\Product"

これはapp/Models/Product.phpファイルにあるProductクラスを指しています。

メリット:
• タイポ防止(文字列で書くよりも安全)
• IDEがクラスの存在をチェックしてくれる
• クラス名変更時に自動で修正されやすい

1対多(One to Many)リレーションを定義

Product(商品) ← Category(カテゴリー)
1つのカテゴリーは複数の商品を持つ関係です。

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',  // 外部キー
    ];

    // この商品が属するカテゴリー(1つ)
    public function category()
    {
        return $this->belongsTo(Category::class);
    }
}

💡 リレーションメソッド名は自由に付けられる!

実は、category()products()というメソッド名は、何でも好きな名前にできます
ただし、わかりやすい名前を付けることが重要です。

// ✅ 推奨: わかりやすい名前
public function category()  // 単数形(1つのカテゴリー)
{
    return $this->belongsTo(Category::class);
}

public function products()  // 複数形(複数の商品)
{
    return $this->hasMany(Product::class);
}

// ❌ 動くけど推奨しない: 意味不明な名前
public function test123()  // これでも動く
{
    return $this->belongsTo(Category::class);
}

// ✅ こんな名前でもOK(意味があれば)
public function belongsToCategory()  // より明示的
{
    return $this->belongsTo(Category::class);
}

public function allProducts()  // すべての商品
{
    return $this->hasMany(Product::class);
}

命名のベストプラクティス:
• belongsTo(子→親)は単数形: category, user
• hasMany(親→子)は複数形: products, comments
• hasOne(1対1)は単数形: profile, detail
• 英語で意味が通じる名前にする

⚠️ メソッド名を変えると、使い方(呼び出し方)も変わる!

リレーションメソッド名は自由ですが、付けた名前でアクセスする必要があります

// ケース1: category() というメソッド名の場合
public function category()
{
    return $this->belongsTo(Category::class);
}

// 使い方
$product = Product::first();
$product->category;  // ✅ category() で定義したので category でアクセス
$product->test123;   // ❌ エラー: そんなリレーションは存在しない
// ケース2: test123() というメソッド名に変えた場合
public function test123()  // メソッド名を変更
{
    return $this->belongsTo(Category::class);
}

// 使い方
$product = Product::first();
$product->category;  // ❌ エラー: category() メソッドは存在しない
$product->test123;   // ✅ test123() で定義したので test123 でアクセス

// Eager Loadingも同じ
Product::with('category')->get();  // ❌ エラー
Product::with('test123')->get();   // ✅ これなら動く
// ケース3: belongsToCategory() という名前の場合
public function belongsToCategory()
{
    return $this->belongsTo(Category::class);
}

// 使い方
$product = Product::first();
$product->category;            // ❌ エラー
$product->belongsToCategory;   // ✅ このメソッド名でアクセス

// Eager Loading
Product::with('belongsToCategory')->get();  // ✅

💡 重要なポイント:
• メソッド名 = アクセスする時のプロパティ名
category() で定義 → $product->category でアクセス
test123() で定義 → $product->test123 でアクセス
with('category') の文字列も、メソッド名と一致させる必要がある

だからこそ、わかりやすい名前を付けることが大切!

Tinkerで動作確認

🧪 Tinkerを起動して試してみよう

php artisan tinker
// 既存のカテゴリーを取得(Seederで作成済み)
>>> $category = \App\Models\Category::first();
=> App\Models\Category {
     id: 1,
     name: "家電",
     created_at: "2025-10-27 12:00:00",
     updated_at: "2025-10-27 12:00:00",
   }

// 商品からカテゴリーを取得(これで動くようになった!)
>>> $product = \App\Models\Product::first();
>>> $product->category;
=> App\Models\Category {
     id: 1,
     name: "家電",
     created_at: "2025-10-27 12:00:00",
     updated_at: "2025-10-27 12:00:00",
   }

>>> $product->category->name;
=> "家電"

// カテゴリーから商品を取得
>>> $category->products;
=> Illuminate\Database\Eloquent\Collection {
     all: [
       App\Models\Product {
         id: 1,
         name: "ノートPC",
         // ... 以下省略
       },
       // ... 他の商品
     ],
   }

// withでEager Loading(これでエラーが出なくなった!)
>>> $products = \App\Models\Product::with('category')->get();
=> Illuminate\Database\Eloquent\Collection {
     all: [
       App\Models\Product {
         id: 1,
         name: "ノートPC",
         category: App\Models\Category {
           id: 1,
           name: "家電",
         },
       },
       // ...
     ],
   }

✅ リレーションを定義したことで:
$product->category でカテゴリー情報を取得できる
$category->products で商品一覧を取得できる
with('category') でEager Loadingが使える

Eager Loading(事前読み込み)とは? - N+1問題を解決する

Eager Loadingは、リレーションデータを事前にまとめて取得する機能です。
これを使わないと、N+1問題という深刻なパフォーマンス問題が発生します。

❌ 悪い例: Eager Loadingを使わない場合(N+1問題)

Tinkerで実際に試してみましょう。何回SQLが実行されるかに注目してください。

php artisan tinker
// まず、DBクエリのログを有効化
>>> \DB::enableQueryLog();

// ❌ withを使わずに商品を全件取得
>>> $products = \App\Models\Product::all();
=> Illuminate\Database\Eloquent\Collection {#...}

// 各商品のカテゴリー名を表示
>>> foreach ($products as $product) {
...     echo $product->name . ' - ' . $product->category->name . "\n";
... }
ノートPC - 家電
ワイヤレスマウス - 家電
コーヒー豆 - 食品
Tシャツ - 衣類
プログラミング入門 - 書籍

// 実行されたSQLを確認
>>> \DB::getQueryLog();
=> [
     [
       "query" => "select * from `products`",  // ← 1回目: 商品を全件取得
     ],
     [
       "query" => "select * from `categories` where `categories`.`id` = ? limit 1",
       "bindings" => [1],  // ← 2回目: category_id=1 のカテゴリーを取得
     ],
     [
       "query" => "select * from `categories` where `categories`.`id` = ? limit 1",
       "bindings" => [1],  // ← 3回目: またcategory_id=1 を取得(無駄!)
     ],
     [
       "query" => "select * from `categories` where `categories`.`id` = ? limit 1",
       "bindings" => [2],  // ← 4回目: category_id=2 のカテゴリーを取得
     ],
     [
       "query" => "select * from `categories` where `categories`.`id` = ? limit 1",
       "bindings" => [3],  // ← 5回目: category_id=3 のカテゴリーを取得
     ],
     [
       "query" => "select * from `categories` where `categories`.`id` = ? limit 1",
       "bindings" => [4],  // ← 6回目: category_id=4 のカテゴリーを取得
     ],
   ]

// 合計: 6回のクエリが実行された!
// 1回(商品) + 5回(各商品ごとにカテゴリー) = 6回

⚠️ N+1問題とは:
• 最初に商品を5件取得(1回のクエリ)← これが「+1」
• ループで各商品のカテゴリーにアクセスする度に、1件ずつSQLが実行される(5回)← これが「N」
合計6回のクエリ(N + 1 = 5 + 1 = 6)
• 商品が100件なら100+1=101回、1000件なら1000+1=1001回のクエリになる!
データベースへの負荷が非常に大きい

✅ 良い例: Eager Loadingを使う場合

with('category')を使うと、最初にまとめて取得します。

// クエリログをリセット
>>> \DB::flushQueryLog();
>>> \DB::enableQueryLog();

// ✅ withを使って商品とカテゴリーをまとめて取得
>>> $products = \App\Models\Product::with('category')->get();
=> Illuminate\Database\Eloquent\Collection {#...}

// 各商品のカテゴリー名を表示(さっきと同じコード)
>>> foreach ($products as $product) {
...     echo $product->name . ' - ' . $product->category->name . "\n";
... }
ノートPC - 家電
ワイヤレスマウス - 家電
コーヒー豆 - 食品
Tシャツ - 衣類
プログラミング入門 - 書籍

// 実行されたSQLを確認
>>> \DB::getQueryLog();
=> [
     [
       "query" => "select * from `products`",  // ← 1回目: 商品を全件取得
     ],
     [
       "query" => "select * from `categories` where `categories`.`id` in (?, ?, ?, ?)",
       "bindings" => [1, 2, 3, 4],  // ← 2回目: カテゴリーをまとめて取得!
     ],
   ]

// 合計: たった2回のクエリで完了!
// 1回(商品) + 1回(全カテゴリー) = 2回

✅ Eager Loadingの効果:
• 商品を全件取得(1回のクエリ)
• 必要なカテゴリーをまとめて取得(1回のクエリ)
合計2回のクエリで完了!
• 商品が100件でも1000件でも、クエリは2回だけ
データベースへの負荷が激減

📊 パフォーマンス比較

商品数 Eager Loadingなし Eager Loadingあり
5件 6回 (5+1) 2回
100件 101回 (100+1) 2回
1000件 1001回 (1000+1) 2回

💡 結論:
• リレーションを使う時は、必ずwith()を使う習慣をつけよう
• Eager Loadingを使わないと、アプリが遅くなる
• 特に本番環境では、パフォーマンスに大きな差が出る

1対1(One to One)リレーション

Product(商品) - ProductDetail(商品詳細)
1つの商品は1つの詳細情報を持つ関係です。

ProductDetailモデルとマイグレーションを作成

php artisan make:model ProductDetail -m

マイグレーションファイルを編集

public function up(): void
{
    Schema::create('product_details', function (Blueprint $table) {
        $table->id();
        $table->foreignId('product_id')->constrained()->onDelete('cascade');
        $table->text('long_description')->nullable();
        $table->string('manufacturer')->nullable();
        $table->string('warranty_period')->nullable();
        $table->timestamps();
    });
}

ProductDetailモデルを編集

app/Models/ProductDetail.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class ProductDetail extends Model
{
    use HasFactory;

    protected $fillable = [
        'product_id',
        'long_description',
        'manufacturer',
        'warranty_period',
    ];

    // この詳細情報が属する商品(1つ)
    public function product()
    {
        return $this->belongsTo(Product::class);
    }
}

Productモデルにリレーションを追加

app/Models/Product.php(既存のcategory()の下に追加)

// この商品の詳細情報(1つ)
public function detail()
{
    return $this->hasOne(ProductDetail::class);
}

マイグレーションを実行

php artisan migrate

🤔 hasOne と belongsTo の使い分け

1対1リレーションでは、どちら側にデータを持つかで使い分けます。

📦 データベースの構造:

products テーブル
├─ id: 1
├─ name: ノートパソコン
└─ price: 120000

product_details テーブル
├─ id: 1
├─ product_id: 1  ← 外部キー(Productのidを参照)
├─ long_description: 高性能な...
└─ manufacturer: Dell

ルール:

  • belongsTo(): 外部キー(product_id)を持つ側が使う
  • hasOne(): 外部キーを持たない側が使う

✅ ProductDetail(外部キーを持つ側)

class ProductDetail extends Model
{
    // product_id を持っているので belongsTo()
    public function product()
    {
        return $this->belongsTo(Product::class);
    }
}

✅ Product(外部キーを持たない側)

class Product extends Model
{
    // 外部キーを持たないので hasOne()
    public function detail()
    {
        return $this->hasOne(ProductDetail::class);
    }
}

💡 覚え方:
〜_id カラムを持っている方が belongsTo()」と覚えましょう!

🤔 なぜ外部キーを持たない側がhasOne()なの?

理由はデータの参照方向にあります。

  • Product側(外部キーなし): 自分のテーブルに相手の情報がないので、相手を探しに行く必要がある → hasOne()(能動的)
  • ProductDetail側(外部キーあり): product_idを持っているので、既にどのProductに属するか知っている → belongsTo()(受動的)

つまり、「相手を探しに行く側」がhas系、「既に相手のIDを知ってる側」がbelongsToです!

🎯 Tinkerで試してみよう

実際に1対1リレーションを使って、商品の詳細情報を登録・取得してみます。

1️⃣ Tinkerを起動

php artisan tinker

2️⃣ 商品を1つ取得

>>> $product = \App\Models\Product::first();
=> App\Models\Product {#...
     id: 1,
     name: "ノートパソコン",
     price: 120000,
     category_id: 1,
   }

3️⃣ この商品に詳細情報を作成(hasOneを使う)

>>> $detail = $product->detail()->create([
...     'long_description' => '高性能なCore i7プロセッサーを搭載した15.6インチノートPC。メモリ16GB、SSD512GBで快適な動作を実現。',
...     'manufacturer' => 'Dell',
...     'warranty_period' => '1年間',
... ]);
=> App\Models\ProductDetail {#...
     product_id: 1,
     long_description: "高性能なCore i7プロセッサーを搭載した...",
     manufacturer: "Dell",
     warranty_period: "1年間",
   }

4️⃣ 商品から詳細情報を取得(hasOne - プロパティとしてアクセス)

>>> $product->detail;
=> App\Models\ProductDetail {#...
     id: 1,
     product_id: 1,
     long_description: "高性能なCore i7プロセッサーを搭載した...",
     manufacturer: "Dell",
     warranty_period: "1年間",
   }

// 詳細情報の特定のカラムにアクセス
>>> $product->detail->manufacturer;
=> "Dell"

>>> $product->detail->warranty_period;
=> "1年間"

5️⃣ 逆方向: 詳細情報から商品を取得(belongsTo)

>>> $detail = \App\Models\ProductDetail::first();
=> App\Models\ProductDetail {#...
     id: 1,
     product_id: 1,
     long_description: "高性能なCore i7プロセッサーを搭載した...",
   }

// この詳細情報が属する商品を取得
>>> $detail->product;
=> App\Models\Product {#...
     id: 1,
     name: "ノートパソコン",
     price: 120000,
   }

>>> $detail->product->name;
=> "ノートパソコン"

6️⃣ Eager Loadingで一緒に取得

// ✅ 商品と詳細情報をまとめて取得
>>> $products = \App\Models\Product::with('detail')->get();

>>> foreach ($products as $product) {
...     echo $product->name;
...     if ($product->detail) {
...         echo " - 製造元: " . $product->detail->manufacturer;
...     }
...     echo "\n";
... }
ノートパソコン - 製造元: Dell
スマートフォン
冷蔵庫
...

⚠️ 注意: 詳細情報がない場合

全ての商品に詳細情報があるわけではないので、アクセス前にnullチェックをしましょう。

// ❌ エラーになる可能性
$product->detail->manufacturer;  // detailがnullの場合エラー

// ✅ 安全
if ($product->detail) {
    echo $product->detail->manufacturer;
}

💡 1対1リレーションのまとめ

  • hasOne(): 親モデル(Product)から子モデル(ProductDetail)へのアクセス
  • belongsTo(): 子モデル(ProductDetail)から親モデル(Product)へのアクセス
  • 外部キーを持つ側がbelongsTo()を使う
  • データがない場合はnullが返るので、必ずチェックする
  • Eager Loadingでwith('detail')を使うとパフォーマンスが向上

多対多(Many to Many)リレーション

Product(商品) ↔ Tag(タグ)
1つの商品は複数のタグを持ち、1つのタグは複数の商品に紐づく関係です。

💡 多対多リレーションの特徴

多対多リレーションでは、中間テーブル(ピボットテーブル)が必要です。

❌ なぜ中間テーブルが必要か?

中間テーブルがない場合、1つのセルに複数のIDを保存することになり、以下の問題が発生します:

  • データベースの正規化違反 - 1つのセルに複数の値(例: "1,2,3")を入れることになる
  • 外部キー制約が使えない - データの整合性を保証できない
  • 検索が困難 - 特定のタグを持つ商品を効率的に探せない
  • 更新が複雑 - IDの追加・削除が煩雑になる
  • データの整合性問題 - 存在しないIDが混入する可能性がある

✅ 中間テーブルを使うメリット

  • データベースの正規化 - 各セルには1つの値のみ保存
  • 外部キー制約 - データの整合性を自動的に保証
  • 検索が簡単 - SQLで効率的に検索可能
  • 更新が簡単 - レコードの追加・削除が単純
  • 追加情報を保存可能 - 例: created_at(紐付け日時)など
products テーブル
├─ id: 1 (ノートパソコン)
├─ id: 2 (スマートフォン)

tags テーブル
├─ id: 1 (人気)
├─ id: 2 (新商品)
├─ id: 3 (セール中)

product_tag テーブル(中間テーブル)
├─ product_id: 1, tag_id: 1  ← ノートパソコンは「人気」
├─ product_id: 1, tag_id: 2  ← ノートパソコンは「新商品」
├─ product_id: 2, tag_id: 1  ← スマートフォンは「人気」
└─ product_id: 2, tag_id: 3  ← スマートフォンは「セール中」

Tagモデルとマイグレーションを作成

php artisan make:model Tag -m

マイグレーションファイルを編集(tagsテーブル)

database/migrations/xxxx_create_tags_table.php

public function up(): void
{
    Schema::create('tags', function (Blueprint $table) {
        $table->id();
        $table->string('name');
        $table->timestamps();
    });
}

中間テーブルのマイグレーションを作成

php artisan make:migration create_product_tag_table

📝 中間テーブルの命名規則:
• 2つのモデル名をアルファベット順に並べる
• 単数形で、アンダースコアで区切る
• 例: Product + Tag → product_tag(tagが後)

中間テーブルのマイグレーションを編集

database/migrations/xxxx_create_product_tag_table.php

public function up(): void
{
    Schema::create('product_tag', function (Blueprint $table) {
        $table->id();
        $table->foreignId('product_id')->constrained()->onDelete('cascade');
        $table->foreignId('tag_id')->constrained()->onDelete('cascade');
        $table->timestamps();
    });
}

Tagモデルを編集

app/Models/Tag.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Tag extends Model
{
    use HasFactory;

    protected $fillable = ['name'];

    // このタグが紐づく商品(複数)
    public function products()
    {
        return $this->belongsToMany(Product::class);
    }
}

Productモデルにリレーションを追加

app/Models/Product.php(既存のリレーションの下に追加)

// この商品に紐づくタグ(複数)
public function tags()
{
    return $this->belongsToMany(Tag::class);
}

マイグレーションを実行

php artisan migrate

🤔 なぜ両方ともbelongsToMany()なの?

多対多リレーションでは、どちら側も相手を「複数持つ」関係なので、両方ともbelongsToMany()を使います。

  • Product側: 1つの商品は複数のタグに属する → belongsToMany(Tag::class)
  • Tag側: 1つのタグは複数の商品に属する → belongsToMany(Product::class)

1対多のhasManyと違い、多対多では「主従関係」がないため、両方ともbelongsToManyになります。

🎯 Tinkerで試してみよう

実際に多対多リレーションを使って、商品にタグを紐づけてみます。

1️⃣ Tinkerを起動

php artisan tinker

2️⃣ タグを作成

>>> $tag1 = \App\Models\Tag::create(['name' => '人気']);
=> App\Models\Tag {#... id: 1, name: "人気" }

>>> $tag2 = \App\Models\Tag::create(['name' => '新商品']);
=> App\Models\Tag {#... id: 2, name: "新商品" }

>>> $tag3 = \App\Models\Tag::create(['name' => 'セール中']);
=> App\Models\Tag {#... id: 3, name: "セール中" }

3️⃣ 商品を取得

>>> $product = \App\Models\Product::first();
=> App\Models\Product {#... id: 1, name: "ノートパソコン" }

4️⃣ 商品にタグを紐づける(attach)

// 1つずつ紐づける
>>> $product->tags()->attach($tag1->id);
>>> $product->tags()->attach($tag2->id);

// または、配列でまとめて紐づける
>>> $product->tags()->attach([1, 2]);

// タグIDを直接指定してもOK
>>> $product->tags()->attach(3);

5️⃣ 商品に紐づくタグを取得

>>> $product->tags;
=> Illuminate\Database\Eloquent\Collection {#...
     all: [
       App\Models\Tag {#... id: 1, name: "人気" },
       App\Models\Tag {#... id: 2, name: "新商品" },
       App\Models\Tag {#... id: 3, name: "セール中" },
     ],
   }

// タグ名だけ取得
>>> $product->tags->pluck('name');
=> Illuminate\Support\Collection {#...
     all: ["人気", "新商品", "セール中"],
   }

6️⃣ 逆方向: タグに紐づく商品を取得

>>> $tag = \App\Models\Tag::find(1);  // 「人気」タグ
=> App\Models\Tag {#... id: 1, name: "人気" }

>>> $tag->products;
=> Illuminate\Database\Eloquent\Collection {#...
     all: [
       App\Models\Product {#... id: 1, name: "ノートパソコン" },
       App\Models\Product {#... id: 2, name: "スマートフォン" },
     ],
   }

7️⃣ タグの紐づけを解除(detach)

// 特定のタグを解除
>>> $product->tags()->detach(3);  // 「セール中」タグを解除

// 複数のタグを解除
>>> $product->tags()->detach([1, 2]);

// 全てのタグを解除
>>> $product->tags()->detach();

8️⃣ タグを同期(sync)

// 指定したタグIDだけを紐づける(それ以外は削除)
>>> $product->tags()->sync([1, 3]);
// 結果: 「人気」と「セール中」だけが紐づく

// 既存のタグを保持しつつ、新しいタグを追加したい場合
>>> $product->tags()->syncWithoutDetaching([2]);
// 結果: 「人気」「セール中」「新商品」の3つが紐づく

9️⃣ Eager Loadingで一緒に取得

// ✅ 商品とタグをまとめて取得
>>> $products = \App\Models\Product::with('tags')->get();

>>> foreach ($products as $product) {
...     echo $product->name . " のタグ: ";
...     echo $product->tags->pluck('name')->implode(', ');
...     echo "\n";
... }
ノートパソコン のタグ: 人気, 新商品, セール中
スマートフォン のタグ: 人気
冷蔵庫 のタグ:
...

⚠️ attach と sync の違い

メソッド 動作 既存のデータ
attach([1, 2]) 追加する 保持される
detach([1, 2]) 削除する 指定分が削除される
sync([1, 2]) 指定したものだけにする 全て削除して新規作成

💡 多対多リレーションのまとめ

  • 中間テーブル(ピボットテーブル)が必要
  • 両方のモデルでbelongsToMany()を使う
  • attach(): タグを追加(既存は保持)
  • detach(): タグを削除
  • sync(): 指定したタグだけにする(それ以外は削除)
  • Eager Loadingでwith('tags')を使うとパフォーマンスが向上

モデルの便利な機能

リレーション以外にも、モデルには便利な機能があります。
「なぜEloquent ORMを使うのか」で触れた「問題3: モデルの機能が使えない」の具体例を見ていきましょう。

カスタムメソッド(ビジネスロジック)

モデルに独自のメソッドを追加して、ビジネスロジックをカプセル化できます。

💡 なぜカスタムメソッドを使うのか?

  • コードの重複を防ぐ: 同じロジックを何度も書かなくて済む
  • 可読性が上がる: 意図が明確になる
  • テストしやすい: ロジックが1箇所にまとまる
  • 変更に強い: 仕様変更時の修正箇所が1つで済む

例1: 高額商品かどうかを判定

// ❌ コントローラーやビューで毎回判定(悪い例)
if ($product->price >= 100000) {
    echo "高額商品";
}

// ✅ モデルにメソッドを定義(良い例)
class Product extends Model
{
    public function isExpensive()
    {
        return $this->price >= 100000;
    }
}

// 使用
if ($product->isExpensive()) {
    echo "高額商品";
}

例2: 在庫があるかチェック

class Product extends Model
{
    public function isInStock()
    {
        return $this->stock > 0;
    }

    public function hasLowStock()
    {
        return $this->stock > 0 && $this->stock <= 5;
    }
}

// 使用
if ($product->isInStock()) {
    echo "購入可能";
}

if ($product->hasLowStock()) {
    echo "残りわずか!";
}

例3: フォーマットされた価格を取得

class Product extends Model
{
    // 定数で税率を管理(マジックナンバーを避ける)
    const TAX_RATE_STANDARD = 0.1;   // 標準税率10%
    const TAX_RATE_REDUCED = 0.08;   // 軽減税率8%

    public function getFormattedPrice()
    {
        return '¥' . number_format($this->price);
    }

    public function getPriceWithTax($taxRate = null)
    {
        // nullの場合は標準税率を使用
        $rate = $taxRate ?? self::TAX_RATE_STANDARD;
        $priceWithTax = $this->price * (1 + $rate);
        return '¥' . number_format($priceWithTax);
    }
}

// 使用
echo $product->getFormattedPrice();                           // ¥120,000
echo $product->getPriceWithTax();                             // ¥132,000(標準税率)
echo $product->getPriceWithTax(Product::TAX_RATE_REDUCED);    // ¥129,600(軽減税率)

💡 定数を使う理由

  • マジックナンバーを避ける: 0.1が何を意味するか明確
  • 変更に強い: 税率変更時は定数1箇所だけ修正すればOK
  • タイポ防止: 0.1と0.01を間違えるリスクがなくなる
  • IDE補完: Product::TAX_RATE_と打つと候補が出る

例4: 割引価格を計算

class Product extends Model
{
    public function getDiscountedPrice($discountPercent)
    {
        return $this->price * (1 - $discountPercent / 100);
    }

    public function isOnSale()
    {
        // tagsリレーションを使って「セール中」タグがあるかチェック
        return $this->tags->contains('name', 'セール中');
    }
}

// 使用
if ($product->isOnSale()) {
    $salePrice = $product->getDiscountedPrice(20);  // 20%オフ
    echo "セール価格: ¥" . number_format($salePrice);
}

🎯 Tinkerで試してみよう

実際にカスタムメソッドを追加して、Tinkerで動作を確認してみます。

1️⃣ Productモデルにメソッドを追加

app/Models/Product.php

class Product extends Model
{
    // 既存のリレーション...

    // 定数
    const TAX_RATE_STANDARD = 0.1;   // 標準税率10%
    const TAX_RATE_REDUCED = 0.08;   // 軽減税率8%

    // カスタムメソッド
    public function isExpensive()
    {
        return $this->price >= 100000;
    }

    public function isInStock()
    {
        return $this->stock > 0;
    }

    public function getFormattedPrice()
    {
        return '¥' . number_format($this->price);
    }

    public function getPriceWithTax($taxRate = null)
    {
        $rate = $taxRate ?? self::TAX_RATE_STANDARD;
        $priceWithTax = $this->price * (1 + $rate);
        return '¥' . number_format($priceWithTax);
    }
}

2️⃣ Tinkerで試す

php artisan tinker
>>> $product = \App\Models\Product::first();
=> App\Models\Product {#...
     id: 1,
     name: "ノートパソコン",
     price: 120000,
     stock: 5,
   }

// 高額商品かチェック
>>> $product->isExpensive();
=> true

// 在庫チェック
>>> $product->isInStock();
=> true

// フォーマットされた価格
>>> $product->getFormattedPrice();
=> "¥120,000"

// 税込価格(デフォルトで標準税率)
>>> $product->getPriceWithTax();
=> "¥132,000"

// 軽減税率を指定
>>> $product->getPriceWithTax(Product::TAX_RATE_REDUCED);
=> "¥129,600"

// 定数の確認
>>> Product::TAX_RATE_STANDARD;
=> 0.1

>>> Product::TAX_RATE_REDUCED;
=> 0.08

💡 カスタムメソッドのメリット

  • 明示的: メソッド名で何をするかが明確
  • 再利用可能: コントローラー、ビュー、どこからでも使える
  • テスト可能: ユニットテストが書きやすい
  • 変更に強い: 「高額商品」の基準が変わっても、モデルの1箇所を修正するだけ
  • リレーションと組み合わせ可能: $product->tagsなどと連携できる

その他のモデル機能(参考)

以下の機能も便利ですが、まずはカスタムメソッドをマスターしましょう。
詳細は公式ドキュメントを参照してください。

キャスト(Casts)

データベースの値を自動的に型変換します。API開発では必須です。

protected $casts = [
    'price' => 'integer',        // 文字列 → 数値
    'is_active' => 'boolean',    // "1" → true, "0" → false
    'options' => 'array',        // JSON文字列 → 配列
    'published_at' => 'datetime', // 文字列 → Carbonオブジェクト
];

効果: React等のフロントエンドに渡す時、"1"ではなくtrueとして渡せる

アクセサ(Accessor) ※古い書き方(Laravel 8以前)

取得時に値を自動加工します。プロパティのように使えます。

// getXxxAttribute() という命名規則
public function getFullNameAttribute()
{
    return $this->first_name . ' ' . $this->last_name;
}

// 使用
$user->full_name;  // "太郎 山田"(自動的にメソッドが呼ばれる)

注意:
• 暗黙的な変換なので、デバッグしにくい
• Laravel 9以降は新しい書き方(Attributeクラス)があるが、カスタムメソッドの方が明示的で推奨
• 既存コードを読めるように知識として知っておく程度でOK

ミューテータ(Mutator) ※古い書き方(Laravel 8以前)

保存時に値を自動加工します。

// setXxxAttribute() という命名規則
public function setNameAttribute($value)
{
    $this->attributes['name'] = strtoupper($value);  // 大文字に変換
}

// 使用
$product->name = 'laptop';  // 保存時に自動的に "LAPTOP" になる

注意:
• アクセサ同様、暗黙的な変換で予期しない動作の原因になりやすい
• 新しい書き方もあるが、基本的にはカスタムメソッドで明示的に処理する方が良い
• 既存コードを読めるように知識として知っておく程度でOK

まとめ

この章では、Laravelのモデル(Model)とEloquent ORMについて学びました。

重要なポイント

  • モデルは app/Models/ に配置
  • php artisan make:model で作成
  • Eloquent ORMで安全にデータベース操作(SQLインジェクション対策が自動)
  • CRUD操作: create(), find(), update(), delete()
  • $fillable で一括代入を制御(Mass Assignment対策)
  • リレーションで関連データを簡単に取得: $product->category
  • 多対多リレーションでは中間テーブル(ピボットテーブル)が必要
  • attach(), detach(), sync() で中間テーブルを操作

よく使うEloquentメソッド

メソッド 用途
all() 全件取得
find($id) IDで1件取得
where('column', 'value')->get() 条件で検索
create([...]) 新規作成
update([...]) 更新
delete() 削除
with('relation') Eager Loading(N+1問題対策)

リレーションの種類

リレーション メソッド
1対多 hasMany() / belongsTo() 1つのカテゴリーに複数の商品
1対1 hasOne() / belongsTo() 1つの商品に1つの詳細情報
多対多 belongsToMany() 複数の商品に複数のタグ

多対多リレーション操作メソッド

メソッド 説明
attach($id) 中間テーブルに関連を追加
detach($id) 中間テーブルから関連を削除
sync([...]) 中間テーブルを指定した状態に同期
toggle($id) 関連をトグル(あれば削除、なければ追加)

ベストプラクティス

  • $fillable を必ず設定
    Mass Assignment対策は必須
  • Eloquent ORMを基本とする
    SQLインジェクション対策が自動で行われる
  • Eager Loadingを活用
    N+1問題を防ぐ(with()を使う)
  • リレーションを適切に定義
    テーブル間の関連を明確にする
  • カスタムメソッドでロジックをカプセル化
    コードの重複を防ぎ、可読性を向上
  • ⚠️ 多対多の中間テーブルは命名規則に従う
    アルファベット順、単数形、アンダースコア区切り

次のステップ

モデルの基本とEloquent ORMの使い方を学びました。
次はDB実践で、実際にCRUDアプリケーションを作成します。

🎉 お疲れ様でした!

Eloquent ORMは、Laravelの最も強力な機能の1つです。
実際にアプリケーションを作りながら、使い方を体で覚えていきましょう!