【第四弾】Laravel入門資料
- Windows環境構築
- Mac環境構築
- Laravelとは
- ルーティング
- ビュー
- コントローラ
- マイグレーション
- モデル
- ここまでの知識の実践
- 検索・フィルタ
- ソフトデリート
- ページネーション
- 画像アップロード
- 認証ライブラリ
- 認証実装
- ミドルウェア・ロール
- 総合実践
📚 Laravel公式ドキュメント - Requests
https://laravel.com/docs/12.x/requests
💡 この講座では、上記の公式ドキュメントを基に解説していきます。
公式ドキュメントは初学者には内容が難しいため、エッセンスを優しく噛み砕いて解説していきます。
検索・フィルタリング - ユーザーが欲しいデータを見つける
商品が増えてくると、ユーザーが欲しい商品を見つけにくくなります。
検索機能とフィルタリング機能を実装して、データを絞り込む方法を学びます。
🎯 この章で学ぶこと
- ✅ キーワード検索 -
where()とlikeの使い方 - ✅ カテゴリーフィルタリング - プルダウンで絞り込み
- ✅ 複数条件の組み合わせ -
when()メソッド - ✅ 検索フォームの作成とGETリクエストの処理
- ✅ 検索条件の保持 -
request()->input()
💡 前提条件
- ✅
productsテーブルとcategoriesテーブルが作成済み - ✅ Product モデルと Category モデルが作成済み
- ✅ リレーション(belongsTo)が設定済み
※ 「ここまでの知識の実践」で作成したテーブルとモデルをそのまま使用します
🗄️ テストデータの準備 - ページネーション動作確認用
検索・フィルタ・ページネーション機能を確認するには、十分なテストデータが必要です。
paginate(10)を使う場合、ページネーションを表示するには11件以上のデータが必要になります。
推奨: 13件のテストデータを作成(公開12件 + 非公開1件)
📝 ProductSeeder.php の編集
database/seeders/ProductSeeder.php
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use App\Models\Product;
class ProductSeeder extends Seeder
{
public function run(): void
{
$products = [
['name' => 'ノートPC', 'category_id' => 1, 'price' => 89800, 'stock' => 15, 'is_published' => true, 'description' => '高性能ノートパソコン'],
['name' => 'デスクトップPC', 'category_id' => 1, 'price' => 120000, 'stock' => 8, 'is_published' => true, 'description' => 'ゲーミングPC'],
['name' => 'マウス', 'category_id' => 1, 'price' => 2980, 'stock' => 50, 'is_published' => true, 'description' => 'ワイヤレスマウス'],
['name' => 'キーボード', 'category_id' => 1, 'price' => 8900, 'stock' => 30, 'is_published' => true, 'description' => 'メカニカルキーボード'],
['name' => 'モニター', 'category_id' => 1, 'price' => 34800, 'stock' => 12, 'is_published' => true, 'description' => '27インチ4Kモニター'],
['name' => 'Tシャツ', 'category_id' => 2, 'price' => 2900, 'stock' => 100, 'is_published' => true, 'description' => 'コットン100%'],
['name' => 'ジーンズ', 'category_id' => 2, 'price' => 5980, 'stock' => 45, 'is_published' => true, 'description' => 'デニムパンツ'],
['name' => 'スニーカー', 'category_id' => 2, 'price' => 7800, 'stock' => 20, 'is_published' => true, 'description' => 'ランニングシューズ'],
['name' => '大学ノート', 'category_id' => 3, 'price' => 180, 'stock' => 200, 'is_published' => true, 'description' => 'B5サイズ'],
['name' => 'ボールペン', 'category_id' => 3, 'price' => 120, 'stock' => 300, 'is_published' => true, 'description' => '黒・赤・青'],
['name' => 'クリアファイル', 'category_id' => 3, 'price' => 100, 'stock' => 150, 'is_published' => true, 'description' => 'A4サイズ'],
['name' => 'マグカップ', 'category_id' => 4, 'price' => 1200, 'stock' => 80, 'is_published' => true, 'description' => '陶器製'],
['name' => 'スマートウォッチ', 'category_id' => 1, 'price' => 45000, 'stock' => 0, 'is_published' => false, 'description' => '在庫切れ・非公開テスト用'],
];
foreach ($products as $product) {
Product::create($product);
}
}
}
💡 このデータの特徴
- 13件のデータ → ページネーション表示確認可能(2ページ目が表示される)
- 「ノート」キーワード → 「ノートPC」「大学ノート」の2件がヒット
- 複数カテゴリー → フィルタリング動作確認可能
- 1件非公開(
is_published = false) → 検索結果に含まれない動作確認 - 在庫0の商品 → 在庫フィルター実装時に利用可能
🚀 シーダー実行コマンド
# データベースをリセットして再シード
php artisan migrate:fresh --seed
# または ProductSeeder のみ実行
php artisan db:seed --class=ProductSeeder
⚠️ 注意
migrate:freshは全データが削除されます。
本番環境では絶対に実行しないでください!
検索・フィルタリング機能の例
実装する機能
- キーワード検索: 商品名で部分一致検索
- カテゴリーフィルター: プルダウンでカテゴリーを選択
- 検索条件の保持: 検索後もフォームに入力内容を保持
- 検索とページネーションの組み合わせ
GETリクエストとクエリパラメータ
検索機能ではGETリクエストを使い、検索条件をクエリパラメータ(URLパラメータ)で渡します。
💡 クエリパラメータとは?
URLの?以降に付けるパラメータのことです。
例: /search?keyword=ノート&category_id=1
- •
?の後にパラメータを記述 - •
キー=値の形式 - • 複数のパラメータは
&で繋ぐ
✅ GETリクエストを使うメリット
- • URLに検索条件が表示される → ブックマーク可能
- • URLをシェアできる(「この検索結果を見て!」)
- • ブラウザの戻るボタンが使える
- • ページネーションと組み合わせやすい
クエリパラメータの取得方法
// コントローラーでの取得方法
$keyword = $request->input('keyword');
$categoryId = $request->input('category_id');
// デフォルト値を指定することも可能
$keyword = $request->input('keyword', ''); // デフォルトは空文字
LIKE検索を理解する
部分一致検索を実装する前に、LIKE検索について詳しく学びましょう。
Tinkerで試してみよう
php artisan tinker
1. 部分一致検索(前後に%)
// 「ノート」を含む商品を検索
Product::where('name', 'like', '%ノート%')->get();
// 実行されるSQL: SELECT * FROM products WHERE name LIKE '%ノート%'
// ヒットする例: 「ノートPC」「大学ノート」「ノート型パソコン」
2. 前方一致検索(後ろに%)
// 「ノート」で始まる商品を検索
Product::where('name', 'like', 'ノート%')->get();
// ヒットする例: 「ノートPC」「ノート型パソコン」
// ヒットしない例: 「大学ノート」(ノートが先頭にない)
3. 後方一致検索(前に%)
// 「ノート」で終わる商品を検索
Product::where('name', 'like', '%ノート')->get();
// ヒットする例: 「大学ノート」「方眼ノート」
// ヒットしない例: 「ノートPC」(ノートが末尾にない)
4. 完全一致検索(%なし)
// 「ノート」と完全一致する商品を検索
Product::where('name', 'like', 'ノート')->get();
// これは where('name', '=', 'ノート') と同じ
// ヒットする例: 「ノート」のみ
⚠️ LIKE検索のパフォーマンス注意
前方に%がある場合(%キーワード%や%キーワード)は、
インデックスが使われず、全件スキャンになるため遅くなります。
大量データの場合は、全文検索エンジン(Elasticsearch等)の利用を検討しましょう。
基本的なキーワード検索の実装
それでは、学んだLIKE検索を使って商品検索機能を実装します。
ステップ1: ルーティングの設定
routes/web.php
use App\Http\Controllers\SearchController;
Route::get('/search', [SearchController::class, 'index']);
ステップ2: コントローラーの作成
検索機能を持つコントローラーを作成します。
php artisan make:controller SearchController
app/Http/Controllers/SearchController.php を以下のように編集します。
<?php
namespace App\Http\Controllers;
use App\Models\Product;
use Illuminate\Http\Request;
class SearchController extends Controller
{
public function index(Request $request)
{
// リクエストからキーワードを取得
$keyword = $request->input('keyword');
// 検索クエリを構築
$query = Product::with('category');
// キーワードがある場合のみ検索条件を追加
if ($keyword) {
$query->where('name', 'like', "%{$keyword}%");
}
// ページネーション付きで取得
$products = $query->paginate(10);
return view('search.index', compact('products', 'keyword'));
}
}
💡 LIKE検索とは?
where('name', 'like', "%{$keyword}%") は部分一致検索を行います。
% はワイルドカード(任意の文字列)を表します。
- •
"%キーワード%"→ 前後に任意の文字(部分一致) - •
"キーワード%"→ 前方一致 - •
"%キーワード"→ 後方一致
ステップ3: ビューファイルの作成
resources/views/search/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>
<script src="https://cdn.tailwindcss.com"></script>
<style>
body { font-family: sans-serif; padding: 20px; }
.search-form { margin-bottom: 20px; }
.search-form input { padding: 8px; width: 300px; }
.search-form button { padding: 8px 16px; background: #4CAF50; color: white; border: none; cursor: pointer; }
table { width: 100%; border-collapse: collapse; margin: 20px 0; }
th, td { border: 1px solid #ddd; padding: 12px; text-left; }
th { background-color: #4CAF50; color: white; }
</style>
</head>
<body>
<h1>商品検索</h1>
{{-- 検索フォーム --}}
<form method="GET" action="/search" class="search-form">
<input type="text" name="keyword" placeholder="商品名で検索" value="{{ $keyword }}">
<button type="submit">検索</button>
</form>
<p>検索結果: {{ $products->total() }}件</p>
<table>
<thead>
<tr>
<th>ID</th>
<th>商品名</th>
<th>カテゴリー</th>
<th>価格</th>
<th>在庫</th>
</tr>
</thead>
<tbody>
@forelse($products as $product)
<tr>
<td>{{ $product->id }}</td>
<td>{{ $product->name }}</td>
<td>{{ $product->category->name }}</td>
<td>¥{{ number_format($product->price) }}</td>
<td>{{ $product->stock }}</td>
</tr>
@empty
<tr>
<td colspan="5" style="text-align: center;">該当する商品が見つかりませんでした</td>
</tr>
@endforelse
</tbody>
</table>
{{-- ページネーションリンク(検索条件を保持) --}}
{{ $products->appends(['keyword' => $keyword])->links() }}
</body>
</html>
💡 重要なポイント
- •
<script src="https://cdn.tailwindcss.com"></script>- Tailwind CSS CDN(ページネーションのスタイルに必須) - •
value="{{ $keyword }}"- 検索後も入力値を保持 - •
@forelse ... @empty- データがない場合の表示 - •
$products->total()- 検索結果の総件数を表示 - •
appends(['keyword' => $keyword])- ページネーションに検索条件を追加
⚠️ Tailwind CDNが必要な理由
Laravel 12のページネーション(links())は、デフォルトでTailwind CSSのクラスを使用しています。
もしTailwind CSSを読み込まないと:
- ページネーションのボタン(
< >)のスタイルが崩れる - レイアウトが正しく表示されない
本番環境では、CDNではなくViteでビルドしたTailwind CSSを使うことを推奨します。
value属性で検索値を保持する仕組み
value="{{ $keyword }}" がないと、検索後にフォームが空になってしまいます。
🔍 実際の動作例
ユーザーが「ノート」と入力して検索ボタンをクリックした時:
- ブラウザが
/search?keyword=ノートにGETリクエスト - コントローラーが
$keyword = $request->input('keyword');で「ノート」を取得 - 検索実行後、ビューに
$keyword(値は「ノート」)を渡す - Bladeテンプレートで
value="{{ $keyword }}"がvalue="ノート"に変換される - 検索フォームに「ノート」が表示されたままになる ✅
❌ value属性がない場合
<input type="text" name="keyword" placeholder="商品名で検索">
→ 検索後、フォームが空になってしまう
→ 「何で検索したんだっけ?」となる(ユーザー体験が悪い)
✅ value属性がある場合
<input type="text" name="keyword" placeholder="商品名で検索" value="{{ $keyword }}">
→ 検索後、フォームに「ノート」が残る
→ 検索条件を確認・修正しやすい(ユーザー体験が良い)
💡 初回アクセス時の動作
初めて/searchにアクセスした時は、$keywordがnullなので、
value=""(空文字)になり、フォームは空で表示されます。
ステップ4: 確認
ブラウザで /search にアクセスして確認します。
検索フォームでキーワードを入力して検索してみましょう。
✅ 基本的なキーワード検索機能が完成しました!
カテゴリーフィルタリングの追加
キーワード検索に加えて、カテゴリーでフィルタリングできるようにします。
コントローラーの更新
app/Http/Controllers/SearchController.php
<?php
namespace App\Http\Controllers;
use App\Models\Product;
use App\Models\Category;
use Illuminate\Http\Request;
class SearchController extends Controller
{
public function index(Request $request)
{
// リクエストから検索条件を取得
$keyword = $request->input('keyword');
$categoryId = $request->input('category_id');
// 検索クエリを構築
$query = Product::with('category');
// キーワード検索
if ($keyword) {
$query->where('name', 'like', "%{$keyword}%");
}
// カテゴリーフィルター
if ($categoryId) {
$query->where('category_id', $categoryId);
}
// ページネーション付きで取得
$products = $query->paginate(10);
// カテゴリー一覧を取得(プルダウン用)
$categories = Category::all();
return view('search.index', compact('products', 'keyword', 'categoryId', 'categories'));
}
}
ビューの更新
検索フォームにカテゴリープルダウンを追加します。
{{-- 検索フォーム --}}
<form method="GET" action="/search" class="search-form">
<input type="text" name="keyword" placeholder="商品名で検索" value="{{ $keyword }}">
<select name="category_id">
<option value="">すべてのカテゴリー</option>
@foreach($categories as $category)
<option value="{{ $category->id }}" {{ $categoryId == $category->id ? 'selected' : '' }}>
{{ $category->name }}
</option>
@endforeach
</select>
<button type="submit">検索</button>
</form>
{{-- ページネーションリンク(検索条件を保持) --}}
{{ $products->appends(['keyword' => $keyword, 'category_id' => $categoryId])->links() }}
ページネーション表示のカスタマイズ
Laravel 12のlinks()は、デフォルトで英語の翻訳とTailwind CSSのスタイルを使用します。
日本語表示を自然にするには、翻訳ファイルとビューファイルのカスタマイズが必要です。
📝 ページネーションの表示問題
links()を使うと、以下のような問題が発生することがあります:
- 「表示中 1 に 10 の 13 結果」という不自然な日本語
- ボタン(
< >)のスタイルが崩れる
解決策1: ページネーションビューのカスタマイズ(推奨)
ページネーションビューを公開して、日本語の文章構造に合わせて編集します。
# ページネーションビューを公開
php artisan vendor:publish --tag=laravel-pagination
公開先: resources/views/vendor/pagination/tailwind.blade.php
編集箇所: 26行目付近の表示テキスト部分
{{-- 変更前 --}}
<p class="text-sm text-gray-700 leading-5 dark:text-gray-400">
{!! __('Showing') !!}
@if ($paginator->firstItem())
<span class="font-medium">{{ $paginator->firstItem() }}</span>
{!! __('to') !!}
<span class="font-medium">{{ $paginator->lastItem() }}</span>
@else
{{ $paginator->count() }}
@endif
{!! __('of') !!}
<span class="font-medium">{{ $paginator->total() }}</span>
{!! __('results') !!}
</p>
{{-- 変更後(自然な日本語) --}}
<p class="text-sm text-gray-700 leading-5 dark:text-gray-400">
全 <span class="font-medium">{{ $paginator->total() }}</span> 件中
@if ($paginator->firstItem())
<span class="font-medium">{{ $paginator->firstItem() }}</span> 〜
<span class="font-medium">{{ $paginator->lastItem() }}</span> 件を表示
@else
{{ $paginator->count() }} 件を表示
@endif
</p>
✅ カスタマイズ後の表示
「全 13 件中 1 〜 10 件を表示」のような自然な日本語になります。
解決策2: 翻訳ファイルの編集(簡易的)
lang/ja.jsonを編集して翻訳を調整します(完全には自然な日本語にならない)。
{
"Showing": "表示中",
"to": "〜",
"of": "件中",
"results": "件"
}
→ 「表示中 1 〜 10 件中 13 件」となり、やや改善されますが完璧ではありません。
links()の仕組み - どうやってHTMLが生成されるのか
{{ $products->links() }} と書くだけでページネーションが表示される裏側の仕組みを理解しましょう。
🔄 links()の内部動作フロー
- Bladeで呼び出し:
{{ $products->links() }} - どのビューを使うか判定: デフォルトは
tailwind.blade.php - ビューファイルを探す優先順位:
resources/views/vendor/pagination/tailwind.blade.php← まずここを探すvendor/laravel/framework/.../pagination/.../tailwind.blade.php← なければフレームワーク内のデフォルトを使う
- $paginator オブジェクトを渡してレンダリング: ページ情報を含むオブジェクトがビューに渡される
- HTML出力: ボタンやページ番号のHTMLが生成される
💡 $paginator オブジェクトで使えるメソッド
ビューファイル内で以下のメソッドが使えます:
$paginator->total() // 総件数: 13
$paginator->firstItem() // 現在ページの最初の番号: 1
$paginator->lastItem() // 現在ページの最後の番号: 10
$paginator->currentPage() // 現在のページ番号: 1
$paginator->hasPages() // ページが複数あるか: true/false
$paginator->hasMorePages() // 次のページがあるか: true/false
他のページネーションビューを使う
Tailwind以外のスタイルも選択できます。
// デフォルト(Tailwind CSS)
{{ $products->links() }}
// Bootstrap 5
{{ $products->links('pagination::bootstrap-5') }}
// シンプル版(前へ・次へのみ)
{{ $products->links('pagination::simple-tailwind') }}
✅ なぜ vendor:publish が必要なのか
デフォルトのビューはvendor/ディレクトリ内にあり、直接編集するとComposerアップデート時に上書きされます。
php artisan vendor:publish --tag=laravel-paginationでコピーすることで、
resources/views/vendor/pagination/に配置され、安全にカスタマイズできます。
appends()でページネーションに検索条件を追加
ページネーションのリンクをクリックした時、検索条件が消えてしまう問題を解決します。
⚠️ appends()がない場合の問題
「ノート」で検索して、2ページ目に移動した時:
- 1ページ目:
/search?keyword=ノート→ 「ノート」の検索結果が表示される ✅ - 2ページ目のリンクをクリック:
/search?page=2になる - 2ページ目:
keywordパラメータが消えている → 全商品が表示される ❌ - 検索条件が失われる → ユーザー体験が悪い 😢
❌ appends()なしの場合
{{ $products->links() }}
→ ページネーションのURLが /search?page=2 になる
→ 検索条件(keyword, category_id)が消える
✅ appends()ありの場合
{{ $products->appends(['keyword' => $keyword, 'category_id' => $categoryId])->links() }}
→ ページネーションのURLが /search?keyword=ノート&category_id=1&page=2 になる
→ 検索条件が保持される ✅
💡 appends()の仕組み
appends()は、ページネーションリンクに追加のクエリパラメータを付与します。
// 生成されるページネーションリンク
1ページ目: /search?keyword=ノート&category_id=1&page=1
2ページ目: /search?keyword=ノート&category_id=1&page=2
3ページ目: /search?keyword=ノート&category_id=1&page=3
💡 複数条件の保持
appends() に配列で複数の条件を渡すことで、
ページネーション時にすべての検索条件を保持できます。
📝 補足: ページネーションの詳細
ページネーションの詳しい仕組みは、次の「ページネーション」タブで学習します。
ここでは「appends()を使うと検索条件が保持される」と理解しておけばOKです!
✅ カテゴリーフィルタリング機能が追加できました!
when()メソッドでコードをスッキリさせる
if 文で条件分岐する代わりに、when() メソッドを使うとコードがスッキリします。
💡 when()とは?
Laravelのwhen()は、クエリビルダーに条件を動的に追加するためのメソッドです。
検索条件の有無によってクエリを変えたい時に、if文を書かずにスッキリと記述できます。
⚠️ SQLのCASE WHENとは別物
SQLにもWHENがありますが、用途が全く異なります:
SQLのCASE WHEN
→ 値の条件分岐
SELECT
CASE
WHEN price < 1000 THEN '安い'
WHEN price < 5000 THEN '普通'
ELSE '高い'
END
FROM products
価格帯の判定など
Laravelのwhen()
→ クエリの条件分岐
Product::when($keyword,
function ($q, $keyword) {
return $q->where(
'name', 'like',
"%{$keyword}%"
);
}
)->get()
検索条件の動的追加
Tinkerでwhen()を理解する
php artisan tinker
1. 基本的な使い方
// 条件がtrueの時だけクエリを追加
$keyword = 'ノート';
Product::when($keyword, function ($query, $keyword) {
return $query->where('name', 'like', "%{$keyword}%");
})->get();
// $keywordが存在する → where条件が追加される
// 実行されるSQL: SELECT * FROM products WHERE name LIKE '%ノート%'
2. 条件がfalseの場合
// 条件がnullや空文字の場合、when内は実行されない
$keyword = null; // または ''
Product::when($keyword, function ($query, $keyword) {
return $query->where('name', 'like', "%{$keyword}%");
})->get();
// $keywordがnull → when内は実行されない
// 実行されるSQL: SELECT * FROM products (条件なし)
3. 複数のwhenを繋げる
$keyword = 'ノート';
$categoryId = 1;
Product::when($keyword, function ($query, $keyword) {
return $query->where('name', 'like', "%{$keyword}%");
})
->when($categoryId, function ($query, $categoryId) {
return $query->where('category_id', $categoryId);
})
->get();
// 実行されるSQL:
// SELECT * FROM products
// WHERE name LIKE '%ノート%' AND category_id = 1
4. if文との比較
// ❌ if文を使った場合(冗長)
$query = Product::query();
if ($keyword) {
$query->where('name', 'like', "%{$keyword}%");
}
if ($categoryId) {
$query->where('category_id', $categoryId);
}
$products = $query->get();
// ✅ when()を使った場合(スッキリ)
$products = Product::when($keyword, function ($query, $keyword) {
return $query->where('name', 'like', "%{$keyword}%");
})
->when($categoryId, function ($query, $categoryId) {
return $query->where('category_id', $categoryId);
})
->get();
💡 when()の仕組み
when(条件, コールバック) は以下のように動作します:
- • 第1引数が truthy(
true、非0、非null、非空文字)→ コールバック実行 - • 第1引数が falsy(
false、0、null、'')→ 何もしない - • 第2引数のコールバックには、
$queryと条件の値が渡される
✅ when()のメリット
- •
if文を書かなくていい → コード量が減る - • メソッドチェーンで書ける → 読みやすい
- • 条件が多くなってもスッキリ → メンテナンスしやすい
- • Laravelらしい書き方 → チームで統一しやすい
コントローラーでの実装例
public function index(Request $request)
{
$keyword = $request->input('keyword');
$categoryId = $request->input('category_id');
// when()メソッドで条件付きクエリを構築
$products = Product::with('category')
->when($keyword, function ($query, $keyword) {
return $query->where('name', 'like', "%{$keyword}%");
})
->when($categoryId, function ($query, $categoryId) {
return $query->where('category_id', $categoryId);
})
->paginate(10);
$categories = Category::all();
return view('search.index', compact('products', 'keyword', 'categoryId', 'categories'));
}
✅ より洗練されたコードになりました!
まとめ
検索・フィルタリング機能の実装を学びました!
学んだこと
- ✅
where('name', 'like', "%{$keyword}%")でキーワード検索 - ✅
where('category_id', $categoryId)でカテゴリーフィルター - ✅
when()メソッドで条件付きクエリをスッキリ記述 - ✅
appends()でページネーション時に検索条件を保持 - ✅
@forelse ... @emptyで検索結果0件の表示
次のステップ
検索・フィルタリング機能を習得しました。
次は「ソフトデリート」タブで、データを論理削除する方法を学習しましょう!
✅ 検索・フィルタリング学習完了です!