1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【Laravel 5.8】検索機能実装

Posted at

本記事では、laravelを用いた検索機能の実装の流れ、苦労した点などまとめました。
初心者向けの内容も含まれていますので、必要な部分のみ閲覧ください。
間違っている箇所があれば、ご指摘頂けると幸いです。

#目次
1.はじめに
2.マイグレーション
3.model
4.コントローラ
5.view
6.ルーティング

#1-はじめに
 バージョン

各種バージョンは以下の通りです。
・PHP : 7.2.34
・Laravel : 5.8.38
・Vagrant : 2.2.14
・VirtualBox : 6.1.18
・docker : 20.10.5

 目的

擬似ECサイトの商品検索画面を作成。

 最終目標画面

スクリーンショット 2021-05-08 14.41.47.png

#2-マイグレーション

 前提

以下のER図を元に作成。(必要箇所のみ抜粋)
スクリーンショット 2021-05-08 14.35.50.png

 マイグレーション作成

まずは、以下のコマンドでカテゴリーのマイグレーションを作成。

php artisan make:migration create_m_categories_table

上記コマンド実行したら、
/database/migrations/20xx_xx_xx_xxxxxx_create_m_categories_table.php
が作成されます。
マイグレーションファイルにER図を見ながら追記。

class CreateMCategoriesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('m_categories', function (Blueprint $table) {
            $table->increments('id');
            $table->string('category_name', 32);
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('m_categories');
    }
}

同様に商品のマイグレーションも作成。
下のような感じ。
/database/migrations/20xx_xx_xx_xxxxxx_create_m_products_table.php

class CreateMProductsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('m_products', function (Blueprint $table) {
            $table->increments('id');
            $table->string('product_name', 64);
            $table->unsignedInteger('category_id');
            $table->integer('price');

            $table->foreign('category_id')->references('id')->on('m_categories')->onDelete('cascade');
        });

    }

※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※
マイグレーションファイルの順番に関して、親テーブルの作成時間が先でないといけない!

具体的には、マイグレーションファイルの前半部分の日付が親テーブルの方が子テーブルより早い必要があります。

理由を簡単に説明すると、子テーブルの外部キー(今回はcategory_id)は、親テーブル(今回はid)から持ってくるので、親テーブルが先にマイグレートされる必要有。
そして、マイグレートには順番があり、マイグレーションファイルの日付が若い順にマイグレートされるため、親テーブルのマイグレーションファイルの日付が若い必要有。
下のような感じ。日付はファイル名を変更で簡単に修正できます。

<カテゴリー(親)>
2021_05_01_101010_create_m_categories_table.php

<商品(子)>
2021_05_01_101020_create_m_products_table.php
※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※※

#3-model
 モデル作成

まずは、以下のコマンドでカテゴリーモデルを作成。
App/Models/Category.php

php artisan make:model Models/Category

Models/Category と書くことでApp/Modelsフォルダにモデルファイル Category.phpが作られます。

モデルはModelsフォルダに集約することをおすすめします。
以下でもモデルファイルは作成できるがAppフォルダに作成され、モデルファイルが多くなってくると見づらくなってしまうので、、、

//Appフォルダ直下にモデルファイルが作られます。
php artisan make:model Category

同様に、商品モデルを作成。
App/Models/Product.php

 リレーション

今回はカテゴリーテーブルが親テーブル、商品テーブルが子テーブルの関係で、1対多の関係になります。
1つのカテゴリーに対して、複数の商品がある関係をモデルに記載していきます。

まずは、カテゴリーテーブル
App/Models/Category.php

class Category extends Model
{
    protected $table = 'm_categories';
    
    // Productモデルを子に持つことを記述
    public function products()
    {
        return $this->hasMany('App\Models\Product');
    }
}

簡単に解説すると、1つのカテゴリーに対して複数の商品を持つので、
return $this->hasMany('商品モデルのパス')と書きます。
ここで、メソッド名をproductsと複数系にしているのは、複数の商品があるよという意味で複数形にしています。

続いて、商品テーブル
App/Models/Product.php

class Product extends Model
{
    protected $table = 'm_products';

    // Categoryモデルを親に持つことを明記
    public function category()
    {
        return $this->belongsTo('App\Models\Category');
    }
}

1つの商品は1つのカテゴリーに属している関係になるので、return $this->belongsTo('カテゴリーモデルのパス')と書きます。
ここで、メソッド名をcategoryと単数系にしているのは、1つの商品は1つのカテゴリーに属しているという意味で単数形にしています。
(1つの商品が複数のカテゴリーを持っていない。)

#4-コントローラ
 コントローラ作成

以下のコマンドでコントローラを作成。
App/Http/ProductsContoroller.php

php artisan make:controller ProductsController

App/Httpフォルダにコントローラファイル ProductsContoroller.php が作られます。

 検索機能の実装

以下が検索機能の実装となります。
1つずつ順に説明していきます。

namespace App\Http\Controllers;

use Illuminate\Http\Request; //Viewからパラメータを受け取れる
use App\Models\Product;    //商品モデルに記載したメソッドが使える
use App\Models\Category;    //カテゴリーモデルに記載したメソッドが使える

class ProductsController extends Controller
{
    public function index(Request $request)
    {
        $productsDetails = Product::with('category')  //解説①
                                ->when($request->product_name, function ($query) use ($request) { //解説②
                                    return $query->where('product_name', 'like', "%$request->product_name%");
                                })
                                ->when($request->product_category, function ($query) use ($request) {
                                    return $query->where('category_id', 'like', "%$request->product_category%");
                                })
                                ->orderByRaw("category_id ASC, product_name ASC") ////解説③
                                ->paginate(15); //ページネーション 15件毎にページを分ける

        $allProductsCategories = Category::get();
        return view('products.search_products', compact('productsDetails', 'allProductsCategories'));
    } //解説⑤
}

 解説① N+1問題対策

$productsDetails = Product::with('category')

ここで何をしているかというと、商品テーブルとそれに関連しているカテゴリーテーブルを持ってきています。
App/Models/Product.phpでcategoryメソッドを書いたので、(belongsTo〜〜と書いた箇所)
with('メソッド名')でリレーション先のデータが取得できるようになります。

withメソッドを使うのは、いわゆるN+1問題対策です。

簡単に説明するために、まずN+1問題対策しない場合を考えてみます。
流れとしては、Product::all() でallメソッドを使用した後、foreachで全ての商品1つ1つに対してリレーション先のカテゴリーテーブルのデータを取得という方法でもリレーション先データ取得はできます。

この場合、
①全ての商品データ取得 
②N個ある商品データ1つ1つのリレーション先データ取得
することになります。
DBへデータが欲しいと要求する回数がN+1回になってしまう。これがN+1問題になります。

withメソッドを使うと、
①全ての商品データとリレーション先のデータを取得
することになり、DBへデータが欲しいと要求する回数は1回ですみます。

 解説② クエリビルダ 条件節 when  +  部分一致検索 where Like

->when($request->product_name, function ($query) use ($request) { 
                                    return $query->where('product_name', 'like', "%$request->product_name%");
                                })

ここでは、検索画面で商品名($request->product_name)を入力し検索ボタンを押下した場合に、
検索画面で入力した商品名(キーワード)に一致する商品のみ取得できるように限定しています。

  • whenメソッドについて

まず、Whenメソッドはある条件を満たした時のみ、Where等のをクエリを実行できるメソッドです。
if文を使っても同様のことができますが、whenメソッドを使うことでスマートになります。

if (商品名で検索した場合) {
  $productsDetails_1 = Product::with('category')
                      ->商品名検索
}
if (カテゴリ名で検索した場合) {
  $productsDetails_2 = Product::with('category')
                      ->カテゴリ名検索
}
if (価格で検索した場合) {
  $productsDetails_3 = Product::with('category')
                      ->価格検索
}
//以下条件が多くなればif文が増える。。。

use ($request)と書くことによって、whenメソッドの中で$requestが使えることができます。
今回では、whereメソッドで$requestが使えることができるようになります。

  • whereメソッドについて

whereメソッドでlikeを使用するとあいまいな文字列検索できます。
下のような書き方になります。

 where('カラム名', 'like', "%キーワード%")

==================上記のポイント=================
①第3引数だけダブルクオーテーション””で囲んでいる
 第1、2引数と同様シングルクオーテーション''で囲んでしまった場合は「 %キーワード% 」で検索してしまうことになります。
(%までカラム名にあるかどうかまで判定してしまう)
 ダブルクオーテーションで囲むことで「 キーワード 」検索できます。

②第3引数でキーワードを%%で囲んでいる
 %%で囲むことで、カラム名のどこかにキーワードが含まれているか判定してくれます。(例:%伝 → 以心伝心)
 %キーワード のように先頭のみに%を書く場合、末尾がキーワードと一致しているかどうか、(例:%伝 → 宣伝)
 キーワード% のように末尾のみに%を書く場合、先頭がキーワードと一致しているかどうかを判定することになります。(例:伝% → 伝言)

※文字列検索ではなく、キーワード部分との完全一致、大小比較する場合、'like' の箇所には ’=’, '>', '<' 等が入ります。
==========================================

 解説③ 複数条件での並び替え orderByRaw

->orderByRaw("category_id ASC, product_name ASC") ////解説③

ここでは、DBから取得したものをカテゴリーIDと商品名を昇順に並び替えしています。

DBからデータ取得してきたものを並べ替えたい時、orderByやorderByRawを使う方法があります。
カテゴリーIDと商品名の複数条件で並び替えをしています。

①orderByを使う場合
下記のように2つ書くことで並び替えができます。

->orderBy('category_id', 'ASC')
->orderBy('product_name', 'ASC')

②orderByRawを使う場合
下記のように書くことで、①と同様に並び替えができます。

->orderByRaw("category_id ASC, product_name ASC") ////解説③

 解説④ compactメソッド

return view('products.search_products', compact('productsDetails', 'allProductsCategories'));

ここでは、Controllerからviewへ変数を渡しています。
compactメソッドだけでなく、withメソッドを使っても変数を渡すことができます。

書き方が異なるので、注意してください。

①compactメソッド

return view('products.search_products')->with('productsDetails', 'allProductsCategories'));

②withメソッド

return view('products.search_products')->with('productsDetails', $productsDetails)->with('allProductsCategories', $allProductsCategories);

複数の変数を渡す場合は、compactメソッドを使用したほうがベターかなと個人的に思います。
どちらを使用するにしても、使用する場合は統一すべきですね。

ちなみに、コードの解説をすると
resources/views/products/search_products.blade.phpへ
2つの変数 $productsDetails, $allProductsCategories を渡していることになります。

#5-view
 商品、カテゴリ検索

以下が、商品名とカテゴリ名前を検索する箇所のコードになります。
商品名はテキストで入力、カテゴリ名はプルダウンで選択できる仕様になっています。

<h2>商品検索画面</h2>
<div class="row">
    <div class="col-sm">
        <form action="{{ route('search.product') }}" method="GET"> //検索ボタン押下した時にroute('search.product')にルーティングされる。後ほど記述。
            <div class="form-group row">
                <label class="col-sm-2 col-form-label">商品名</label>
                <div class="col-sm-5">
                    <input type="text" name="product_name" value="{{ request('product_name') }}" class="form-control">
                </div>
                <div class="col-sm-auto">
                    <button type="search" class="btn btn-primary ">検索</button>
                </div>
            </div>
            <div class="form-group row">
                <label class="col-sm-2">商品カテゴリ</label>
                <div class="col-sm-3">
                    <select name="product_category" value="{{ request('product_category') }}" class="form-control">
                        <option value="">未選択</option>
                        @foreach ($allProductsCategories as $productCategory)
                            <option value="{{ $productCategory->id }}">{{ $productCategory->category_name }}</option> //プルダウンでカテゴリー名を選択できるようにしていて、選択したカテゴリのIDをコントローラへ渡す。
                        @endforeach
                    </select>
                </div>
            </div>
        </form>
    </div>
</div>

 商品検索結果表示

以下が、商品名とカテゴリ名前を検索するした結果一覧表示箇所のコードになります。

<div class="itemTable">
    <p>全{{ $productsDetails->count() }}件</p> //$productsDetailsの中の商品数を表示
    <table class="table table-hover">
        <thead>
            <tr class="table-header">
                <th style="width:50%">商品名</th>
                <th>商品カテゴリ</th>
                <th>価格</th>
                <th></th>
            </tr>
        </thead>
        @foreach ($productsDetails as $productDetails)
            <tr>
                <td>{{ $productDetails->product_name }}</td>
                <td>{{ $productDetails->Category->category_name }}</td> //リレーション先カテゴリー名を表示
                <td>{{ $productDetails->price }}</td>
            </tr>
        @endforeach
    </table>
</div>

#6-ルーティング

以下のようにルーティングを記載します。

Route::get('products', 'ProductsController@index')->name('search.product');

->name('search.product')のように書くと、ルート名をつけることができます。
ルート名は付けなくてもアクセスできますが、できれば付けていた方が良いと思います。

<参考記事>
https://qiita.com/kazuhei/items/935257b0d72fa314d461

以上になります。

1
2
0

Register as a new user and use Qiita more conveniently

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?