こんにちは、しろうです。
現在、Laravelで小説サイトを作成しています。
そんな中で、(自分的には)ちょっとだけハイレベルな検索機能を作ってみたので、忘れないようにメモとして残しておきます。
検索機能を実装したい人はぜひ、参考にしてください。
もしミスとかあればコメントして頂けると大変有り難いですm(_ _)m
##前置き
・クラス名とかひどいのあるかと思いますが、気にしないで頂けると幸いです。
・Formファサードを使ったり、素のformを使ったり混在していますが、許してくださいm(_ _)m
・cssは貼り付けていないので、皆さん側で好きなように変更してください。
##今回作る検索機能
・複数条件での絞り込み検索ができる。(例:キーワードとジャンルで絞り込む。)
・現在の検索条件がタグとして表示される。
・タグを削除すれば、検索条件から削除される。
###検索機能のイメージ動画
ちなみにイメージ動作は下記の通りです。
下記のように、他のページからジャンルとかをクリックしたら、そのジャンル名で検索してくれる機能もつけてます。
さて、じゃあどんどん作っていきます。
##とりあえず検索機能を作る
###viewファイルの作成(検索項目の部分)
とりあえず検索項目を作成していきます。
今回は「キーワード検索」と「ジャンル検索」のみを作成しています。
また、CSSは貼り付けないので、皆さんの方で好きなように変更よろしくお願いしますm(_ _)m
{{ Form::open(['route'=>'search','method'=>'GET','id'=>'side_search_form','autocomplete'=>"off"]) }}
<div class="search_keyword">
<div class="search">
<input name="keyword" value="{{request('keyword')}}" type="search" class="searchTerm" placeholder="キーワード検索">
<button type="submit" class="searchButton" form="side_search_form">
<i class="fa fa-search fa-xs"></i>
</button>
</div>
</div>
<div class="search_genre search_side_item_border">
<h4>ジャンル</h4>
<div class="ripples_radio">
{{ Form::radio('genre','', isset(request()->genre) ? false :true, ['id'=>'search_genre_none'])
}}
{{ Form::label('search_genre_none', '指定なし', []) }}
</div>
@foreach ($genres as $index => $genre)
<div class="ripples_radio">
{{ Form::radio('genre',$genre, $genre == request()->genre ? true :false,
['id'=>'search_genre'.$index]) }}
{{ Form::label('search_genre'.$index, $genre, []) }}
</div>
@endforeach
</div>
{{ Form::close() }}
解説していきます。
・'autocomplete'=>"off"
・・・これをつけると検索履歴が表示されなくなります。お好みでどうぞ。
・value="{{request('keyword')}}"
・・・このようにするとvalue値が**「keywordというパラメーター(クエリ文字列)の値」**になります。
・requestメソッド
・・・リクエストデータを取得できるやつです。
・@foreach ($genres as $index => $genre)
・・・この$genres
はコントローラーから検索できるジャンル名を返しています。単純にジャンル名を1つずつ処理しているだけです。
下記少しわかりにくいかと思います。
{{ Form::radio('genre',$genre, $genre == request()->genre ? true :false,
['id'=>'search_genre'.$index]) }}
Form::radio
は第三引数がtrueならchecked
をつけるので、ジャンル名とパラメーターのジャンル名(現在検索しているジャンル名)が一致するなら、checked
をつけるようにしています。
例えば、http://127.0.0.1:8000/search?genre=恋愛(現世)
とかなら、request()->genre
によって、恋愛(現世)
という文字列を取得できます。
なので、$genre == request()->genre
とすることで、現在検索しているジャンルボタンにのみchecked
をつけることができます。
指定なし
というラジオボタンもほしいので、下記コードで作成しています。
{{ Form::radio('genre','', isset(request()->genre) ? false :true, ['id'=>'search_genre_none'])}}
{{ Form::label('search_genre_none', '指定なし', []) }}
value属性
を''(空文字)
にしておけば、コントローラー側の処理でいい感じにできます。(解説は後ほど。)
###web.phpにルーティングを追加
とりあえず、なんでもいいのでルーティングを追加します。
Route::get('search', 'UsersController@search')->name('search');
###ジャンル用のファイルを作成
今回はconfig/getValue/radio.php
にジャンル用の配列を作成しました。
<?php
return [
'genre' =>[
'1' =>'恋愛(異世界)',
'2'=>'恋愛(現世)',
'3'=>'ラブコメ',
'4'=>'ホラー',
'5'=>'推理(ミステリー小説)',
'6'=>'異世界ファンタジー',
'7'=>'現代ファンタジー',
'8'=>'コメディ',
'9'=>'SF',
'10'=>'詩・エッセイ・童話',
'11'=>'歴史・戦国',
'12'=>'その他',
],
];
このようにすることでconfig('getValue.radio.genre')
とすれば、ジャンル名の配列を取得することができます。
コントローラー内でこのジャンル名の配列を取得します。
###コントローラーに処理を追加
次にコントローラー側に検索処理を記述していきます。
public function search(Request $request)
{
//SQL文を書くためにqueryメソッドを使う。
$query = Novel::query();
//ジャンル名を取得。(viewファイルに返すだけ。)
$genres = config('getValue.radio.genre');
//keywordがあるかどうか。
if ($request->filled('keyword')) {
//検索キーワードとタイトルが一致するレコードを絞り込む
$query->where('title', 'like', '%'.$request->get('keyword').'%');
}
//genreがあるかどうか。
if ($request->filled('genre')) {
//検索ジャンルとジャンル名が一致するレコードのidをgenresテーブルから取得
$genre_id = Genre::where('name', $request->genre)->first()->id;
//genre_idが一致するレコードをnovelsテーブルから絞り込む
$query->where('genre_id', $genre_id);
}
//条件に一致するレコードを作成日で降順に並び替えて取得
$novels = $query->latest('novels.created_at')->paginate(50);
//viewファイルは好きなファイルにしてください。
return view("search", compact('novels', 'genres'));
}
たぶん、ほとんど読めばわかるんじゃないかと思います。
$request->filled('genre')
を使えば、**リクエストに値が存在して、かつ、空でない場合
**にtrueを返してくれます。
似たものに$request->has('genre')
というのがありますが、これだと空であってもtrueになります。
それだと、下記のような指定なし(value値が空文字列)
の場合でも実行されてしまうので、今回はfilledメソッド
を使用しています。
{{ Form::radio('genre','', isset(request()->genre) ? false :true, ['id'=>'search_genre_none'])}}
{{ Form::label('search_genre_none', '指定なし', []) }}
ちなみにlatest()
を使えば、降順に並び替えてくれます。(下記の通り)
$query->latest('novels.created_at')->paginate(50);
あとは、好きなようにviewファイル側でデザインしてあげれOK。
試しにsearch.blade.php
とかで{{dd($novels)}}
とかで中身を確認してみてください。
##現在の検索条件を表示する機能の作成
なんて説明すればいいかわかりませんが、ここからは上記の画像のやつを作っていきます。笑
方法としては、そこまで難しくありませんので、ご安心ください!!!
###まずはview側に処理を追加
とりあえず、viewファイルに処理を追加していきます。
下記のようにすれば現在の検索条件を表示することができます。
<div class="search_condition">
<ul>
@if(!empty(request()->keyword))
<li class="search_condition_item">
{{request()->keyword }}<a href="keyword" class="search_condition_a"><i class="fas fa-times search_condition_delete"></i></a>
</li>
@endif
@if(!empty(request()->genre))
<li class="search_condition_item">
{{request()->genre }}<a href="genre" class="search_condition_a"><i class="fas fa-times search_condition_delete"></i></a>
</li>
@endif
</ul>
</div>
見て大体わかると思いますので、ざっくり説明していきます。
まずempty()
は引数が空ならtrueを返すので、!(エクスクラメーション)
をつけて、中身が空ではない時
にif文内のhtmlが表示されるようにします。
そしてif文の条件
を「!empty(request()->keyword)
」みたいな感じにすることで、現在keywordで検索をしているのかどうか、がわかります。(このkeyword
というのはinputのname属性
のことです。)
例えば、http://127.0.0.1:8000/search?keyword=&genre=
みたいな感じで、keyword
が空になっているなら、処理は実行されません。
逆にhttp://127.0.0.1:8000/search?keyword=人生楽しいね&genre=
とかだと、「人生楽しいね」というキーワードで検索されていることになるので、if文内の処理が実行されます。
そして、if文内の処理は下記のようにします。
<li class="search_condition_item">
{{request()->keyword }}<a href="keyword" class="search_condition_a"><i class="fas fa-times search_condition_delete"></i></a>
</li>
まず{{request()->keyword }}
とすることで「人生楽しいね」が表示されます。
でもって、aタグのhref属性
にkeyword
と記述して、JavaScriptでこいつを操作していきます。
###JavaScriptを記述
ここからはJavaScript(今回はjQuery)の出番です。
なんでもいいので、JavaScritのファイルを作って、下記のように記述してください。
$(function () {
$('.search_condition_a').each(function () {
//href属性を取得
attr = $(this).attr('href');
//初期化
var url = new URL(window.location);
//パラメーター削除
url.searchParams.delete(attr);
if (url.search) {
$(this).attr('href', 'search' +url.search);
} else {
$(this).attr('href', 'search');
}
});
});
まず、$('.search_condition_a').each(function () {}
とすることで、search_condition_aクラス
の要素(aタグの部分)をループさせています。
attr = $(this).attr('href');
とすることで、ループされたaタグのhref属性を取得しています。
例えば、下記の場合はkeyword
という文字列が取得できます。
<a href="keyword" class="search_condition_a">
次にURLオブジェクトを作成し、searchParamsメソッド
とdeleteメソッド
を使用して、先ほど取得した、href属性の初期値
と一致するパラメーターを削除します。
//初期化(URLオブジェクトの作成)
var url = new URL(window.location);
//パラメーター削除
url.searchParams.delete(attr);
もう少し詳しく解説していきます。
URLオブジェクトとは、URLを作成したり、編集したりするときに使えるメソッドが沢山入っている、JavaScriptが用意してくれている便利なやつです。()
参考:URL()
そして、引数にはwindow.location
として、現在開いているURLを与えます。
次にsearchParams
で、URLのパラメーター(URLの?以降の部分)を取得、deleteメソッドで引数に与えたものと一致するパラメーターを削除します。
例えば、attr
にkeyword
という文字列が入っている場合は、url.searchParams.delete(attr);
によって、
・http://127.0.0.1:8000/search?keyword=人生楽しいね&genre=恋愛(異世界)
からkeywordの部分が削除されるので、
・http://127.0.0.1:8000/search?genre=恋愛(異世界)
こんな感じになります。
最後にパラメーターがあるかないかで条件分岐させています。
こうしないと、他のページから単一条件で検索ページに飛んだときに、1つのパラメーターを削除すると、全てのパラメーターがなくなり、期待通りの動作をしてくれなかったからです。(もっと良い方法もあるかもです...)
if (url.search) {
$(this).attr('href', 'search' +url.search);
} else {
$(this).attr('href', 'search');
}
url.search
とすることで、URLのパラメーターを取得できます。
あとはattr
メソッドを使って、href属性
の値を変更してあげればOK。
これでタグの部分は完成です。
##別ページから条件検索をしたい時
別ページから検索したい時は下記のように、直接href属性にパラメーターを仕込んでおけばOK。
<a href="{{route('search')}}?genre={{$novel->genre->name}}">{{$novel->genre->name}}</a>
##並び替え機能も実装
ついでに並び替え機能の紹介もしておきます。(「新着順」と「更新順」だけ)
###viewファイルに追加
下記のような感じで実装しました。
<div class="search_sort">
<button
class="sort_btn {{strpos(request()->fullUrl(), 'new') !== false || strpos(request()->fullUrl(), 'sort') === false ? 'sort_active' : ''}}"
name="sort" value="new" form="side_search_form" type="submit">
<span class="spot"></span>新着順
</button>
<button class="sort_btn {{strpos(request()->fullUrl(), 'update') !== false ? 'sort_active' : ''}}"
name="sort" value="update" form="side_search_form" type="submit">
<span class="spot"></span>更新順
</button>
</div>
順に解説していきます。
まずformタグ
の外側でボタンを作る時はform属性
でformタグのid属性
を指定します。
下記の2つの部分は現在のURLによってsort_activeクラス
を付与するかどうか三項演算子を利用して、決定しています。
{{strpos(request()->fullUrl(), 'new') !== false || strpos(request()->fullUrl(), 'sort') === false ? 'sort_active' : ''}}
//省略
{{strpos(request()->fullUrl(), 'update') !== false ? 'sort_active' : ''}}
strpos()
は文字列の中に指定した文字列があるかどうかを判定するメソッドです。
request()->fullUrl()
で現在のURLが取得できます。
###コントローラーに処理を追加
最後に下記のように処理を追加・変更します。
public function search(Request $request){
//省略
if ($request->filled('sort') && $request->sort === 'new') {
$query->latest('novels.created_at');
}elseif ($request->filled('sort') && $request->sort === 'update') {
$query->latest('novels.updated_at');
} else {
$query->latest('novels.created_at');
}
//こっちのlatestは消す。
$novels = $query->paginate(50);
return view("search", compact('novels', 'genres'));
}
これで並び替え機能も完成です。
簡単簡単。
##最後に
これで一応、イメージ動画みたいな感じの検索機能が実装できたかと思います。
昔から検索機能を作るのは苦手なので、少し苦労しました...なんか条件分岐が多いですし疲れますね。(コードのせいかも。)
駆け足だったので、わかりにくい部分があるかも・・・その時はコメントしてくださいませm(_ _)m
たぶん、もう少し仕様変更とかしますが、ひとまずこれにて終了です!お疲れ様でした。