Laravelでほんの少しハイレベルな検索機能を作ってみた。（初心者向け）

こんにちは、しろうです。

現在、Laravelで小説サイトを作成しています。

そんな中で、（自分的には）ちょっとだけハイレベルな検索機能を作ってみたので、忘れないようにメモとして残しておきます。

検索機能を実装したい人はぜひ、参考にしてください。

**もしミスとかあればコメントして頂けると大変有り難いですm(_ _)m**

##前置き

・クラス名とかひどいのあるかと思いますが、気にしないで頂けると幸いです。
・Formファサードを使ったり、素のformを使ったり混在していますが、許してくださいm(_ _)m
・cssは貼り付けていないので、皆さん側で好きなように変更してください。

##今回作る検索機能

**・複数条件での絞り込み検索ができる。（例：キーワードとジャンルで絞り込む。）
・現在の検索条件がタグとして表示される。
・タグを削除すれば、検索条件から削除される。**

###検索機能のイメージ動画

ちなみにイメージ動作は下記の通りです。

![ezgif.com-gif-maker.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/560864/5c5a7b79-e535-63b4-aab5-f1fc7a2342b3.gif)

下記のように、他のページからジャンルとかをクリックしたら、そのジャンル名で検索してくれる機能もつけてます。

![ezgif.com-gif-maker (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/560864/e1b31eed-8fd4-e3ad-e013-cdaf8db109c7.gif)

さて、じゃあどんどん作っていきます。

##とりあえず検索機能を作る


###viewファイルの作成（検索項目の部分）


![スクリーンショット 2021-01-29 13.53.31.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/560864/7e5f1b20-c3c4-b5e2-33f9-651de4cce204.jpeg)


とりあえず検索項目を作成していきます。

今回は「キーワード検索」と「ジャンル検索」のみを作成しています。
また、CSSは貼り付けないので、皆さんの方で好きなように変更よろしくお願いしますm(_ _)m

```html:search.blade.php
{{ 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つずつ処理しているだけです。

下記少しわかりにくいかと思います。

```html
{{ 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`をつけることができます。

`指定なし`というラジオボタンもほしいので、下記コードで作成しています。

```html
{{ Form::radio('genre','', isset(request()->genre) ? false :true, ['id'=>'search_genre_none'])}}
{{ Form::label('search_genre_none', '指定なし', []) }}
```
`value属性`を`''(空文字)`にしておけば、コントローラー側の処理でいい感じにできます。（解説は後ほど。）

###web.phpにルーティングを追加

とりあえず、なんでもいいのでルーティングを追加します。

```php:web.php 
Route::get('search', 'UsersController@search')->name('search');
```

###ジャンル用のファイルを作成

今回は`config/getValue/radio.php`にジャンル用の配列を作成しました。

```php:config/getValue/radio.php
<?php
return [
    'genre' =>[
        '1' =>'恋愛（異世界）',
        '2'=>'恋愛（現世）',
        '3'=>'ラブコメ',
        '4'=>'ホラー',
        '5'=>'推理（ミステリー小説）',
        '6'=>'異世界ファンタジー',
        '7'=>'現代ファンタジー',
        '8'=>'コメディ',
        '9'=>'SF',
        '10'=>'詩・エッセイ・童話',
        '11'=>'歴史・戦国',
        '12'=>'その他',
    ],
];

```
このようにすることで`config('getValue.radio.genre')`とすれば、ジャンル名の配列を取得することができます。

コントローラー内でこのジャンル名の配列を取得します。

###コントローラーに処理を追加

次にコントローラー側に検索処理を記述していきます。

```php:UsersController.php

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メソッド`を使用しています。

```html
{{ Form::radio('genre','', isset(request()->genre) ? false :true, ['id'=>'search_genre_none'])}}
{{ Form::label('search_genre_none', '指定なし', []) }}
```

ちなみに`latest()`を使えば、降順に並び替えてくれます。（下記の通り）

```php

$query->latest('novels.created_at')->paginate(50);
```

あとは、好きなようにviewファイル側でデザインしてあげれOK。

試しに`search.blade.php`とかで`{{dd($novels)}}`とかで中身を確認してみてください。

##現在の検索条件を表示する機能の作成

![スクリーンショット 2021-01-29 14.04.43.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/560864/a5571fa4-20c5-ce37-35fe-7374c383b7c9.jpeg)

なんて説明すればいいかわかりませんが、ここからは上記の画像のやつを作っていきます。笑

方法としては、そこまで難しくありませんので、ご安心ください！！！

###まずはview側に処理を追加

とりあえず、viewファイルに処理を追加していきます。

下記のようにすれば現在の検索条件を表示することができます。

```html:search.blade.php
<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のファイルを作って、下記のように記述してください。

```JavaScript:search.js
$(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`という文字列が取得できます。

```html
<a href="keyword" class="search_condition_a">
```

次にURLオブジェクトを作成し、`searchParamsメソッド`と`deleteメソッド`を使用して、先ほど取得した、`href属性の初期値`と一致するパラメーターを削除します。

```php
//初期化（URLオブジェクトの作成）
var url = new URL(window.location);
//パラメーター削除
 url.searchParams.delete(attr);
```

もう少し詳しく解説していきます。

URLオブジェクトとは、URLを作成したり、編集したりするときに使えるメソッドが沢山入っている、JavaScriptが用意してくれている便利なやつです。（）

参考：[URL()](https://developer.mozilla.org/ja/docs/Web/API/URL/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つのパラメーターを削除すると、全てのパラメーターがなくなり、期待通りの動作をしてくれなかったからです。（もっと良い方法もあるかもです...）

```php
if (url.search) {
            $(this).attr('href', 'search' +url.search);
        } else {
            $(this).attr('href', 'search');
        }
```

`url.search`とすることで、URLのパラメーターを取得できます。

あとは`attr`メソッドを使って、`href属性`の値を変更してあげればOK。

これでタグの部分は完成です。

##別ページから条件検索をしたい時

別ページから検索したい時は下記のように、直接href属性にパラメーターを仕込んでおけばOK。

```html:test.blade.php
<a href="{{route('search')}}?genre={{$novel->genre->name}}">{{$novel->genre->name}}</a>
```

##並び替え機能も実装

![スクリーンショット 2021-01-29 16.15.34.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/560864/3635ed4d-f4cb-9b4d-fe65-5c14ea4b783e.jpeg)

ついでに並び替え機能の紹介もしておきます。（「新着順」と「更新順」だけ）

###viewファイルに追加

下記のような感じで実装しました。

```html:search.blade.php
<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クラス`を付与するかどうか三項演算子を利用して、決定しています。

```php4
{{strpos(request()->fullUrl(), 'new') !== false  || strpos(request()->fullUrl(), 'sort') === false ? 'sort_active' : ''}}

//省略

{{strpos(request()->fullUrl(), 'update') !== false ? 'sort_active' : ''}}
```

`strpos()`は文字列の中に指定した文字列があるかどうかを判定するメソッドです。
`request()->fullUrl()`で現在のURLが取得できます。


###コントローラーに処理を追加

最後に下記のように処理を追加・変更します。

```php:SearchController.php
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

たぶん、もう少し仕様変更とかしますが、ひとまずこれにて終了です！お疲れ様でした。