【Laravel 5.8】検索機能実装

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

#目次
[1.はじめに](#1-はじめに)
[2.マイグレーション](#2-マイグレーション)
[3.model](#3-model)
[4.コントローラ](#4-コントローラ)
[5.view](#5-view)
[6.ルーティング](#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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/645220/d35d6682-1ba2-15d1-fe85-04049f602a39.png)

#2-マイグレーション

　前提
-  
以下のER図を元に作成。（必要箇所のみ抜粋）
![スクリーンショット 2021-05-08 14.35.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/645220/e1dc82be-e31d-79ad-4a1e-c4e90e295092.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

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

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

```
class Category extends Model
{
    protected $table = 'm_categories';
    
    // Productモデルを子に持つことを記述
    public function products()
    {
        return $this->hasMany('App\Models\Product');
    }
}
```
簡単に解説すると、１つのカテゴリーに対して複数の商品を持つので、
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');
    }
}

```

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

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

```
php artisan make:controller ProductsController
```

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

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

```
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で全ての商品１つ１つに対してリレーション先のカテゴリーテーブルのデータを取得という方法でもリレーション先データ取得はできます。

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

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

　解説②　クエリビルダ　条件節　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', "%キーワード%")

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

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

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

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

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

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

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

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

```
->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へ
２つの変数　\$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

以上になります。