本記事では、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サイトの商品検索画面を作成。
最終目標画面
#2-マイグレーション
前提
マイグレーション作成
まずは、以下のコマンドでカテゴリーのマイグレーションを作成。
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
以上になります。