Laravelをやってみる② 検索機能編

Laravel備忘録

Laravel備忘録 Laravelをやってみる① 設計編

Laravelをやってみる①では設計を行いましたが、ちょっとすっ飛ばして②では検索機能の実装を行います。

セレクトボックス2つ、テキストボックス1つの複数で検索できるようにします。また、テキストボックス内でスペースを用いて、条件を絞り込めるようにします。

reviewsテーブルとareasテーブルに分けて、リレーションを張りました。
エリア検索のセレクトボックスはareasテーブルから引っ張ってきています。

以下がテーブル詳細です。

reviews Table
areasテーブル

Route

Route::get('/review', 'ReviewController@index')->name('review.index');

表示するだけなのでこれで。検索機能もGETで同じページに戻すだけなので、他は特に必要ないです。
ユーザーが/reviewのディレクトリにアクセスした時に、コントローラーのindexメソッドに渡します。
そして、このrouteに名前をつけておくと、route(‘review.index’)としてアクセスできるようになるので名前は付けるようにします。

Controller

次にコントローラーを書いていきます。ターミナルで

$ php artisan make:controller ReviewController --resource

と入力し、コントローラーを作成します。今回のreviewは表示、追加、編集、削除といったCRUD処理を行います。後ろの–resourceを付けることで、それらの処理を書くためのメソッドが自動で生成されます。

ReviewControllerのindexメソッドを書いていきます。

public function index(Request $request)
{
    //ログインユーザー情報取得
    $user = Auth::user();
    //areasテーブル情報取得
    $areas = Area::all();

    //検索値の取得
    $g = $request->input('g');
    $a = $request->input('a');
    $s = $request->input('s');



    //クエリの取得
    $query = DB::table('reviews');
    //リレーション
    $query->leftJoin('users', 'reviews.user_id', '=', 'users.id');
    $query->leftJoin('areas', 'reviews.area_id', '=', 'areas.id');
    $query->select('reviews.*', 'users.name', 'users.image', 'areas.area_name');
    $query->orderBy('created_at', 'desc');

    if($g !== null) {
        //全角を半角に
        $search_split = mb_convert_kana($g, 's');
        //半角で文字を切り分けて、配列に入れる
        $search_split2 = preg_split('/[\s]+/', $search_split, -1, PREG_SPLIT_NO_EMPTY);
        //配列をforeachでまわして、where条件を付け加える
        foreach($search_split2 as $value){
            $query->where('gelande_name', 'LIKE', '%' . $value . '%');
        }
    }
    if($a !== null) {
        $query->where('area_id', '=', $a);
    }
    if($s !== null) {
        $query->where('star', '=', $s);
    }


    $reviews = $query->get();


    return view('review.show',compact('reviews', 'user', 'areas', 'g', 'a', 's'));
}

indexメソッドの引数にRequest $requestとすることで、FORMからの情報を取得できます。

このRequestなのですが、Illuminate\Http\Requestクラスです。これはSymfony\Component\HttpFoundation\Requestクラスを継承しています。

このインスタンスから得られる情報が以下となります。

  • $_GET
  • $_POST
  • $_COOKIE
  • $_FILES
  • $_SERVER

これを使用するためにはInputファサードやRequestファサードを使う方法、DIを使う方法、フォームリクエストを使う方法があり、今回はDI(Dependency Injection)のメソッドインジェクションを使っています。

DIについての説明はhttps://laraweb.net/surrounding/2001/こちらなどを参考にしてください。

そのクラスで他のインスタンスをnewしてしまうと、そのnewしたインスタンスに依存してしまう=単体テストができないため、外部から突っ込む、と。

話がそれましたが、メソッドの引数にRequest $requsetとすることで、Requestクラスのインスタンスを取得できるということです。その中に、$_REQUESTが入っています。これを使っていきます。

変数$requestがインスタンスとなっているので、ここから取得していくのですが、取得方法はhttps://qiita.com/piotzkhider/items/feaba3acda27d2e432d8
こちらが詳しいかと思います。

  • $_GETのみ取得の場合は query()
  • $_GETと$_POSTで$_GET優先は get()
  • $_GETと$_POSTで$_POST優先は input()
  • $_FILESも含めた全てはall()

こんな感じでしょうか。

今回はそれぞれのキー、g、a、sをinput()で取ってきています。本来ならquery()かget()の方がいいのかと思います。

//ログインユーザー情報取得
$user = Auth::user();
//areasテーブル情報取得
$areas = Area::all();

//検索値の取得
$g = $request->input('g');
$a = $request->input('a');
$s = $request->input('s');

ログインしているユーザー情報はAuthファサードで取れます。
https://readouble.com/laravel/6.x/ja/authentication.html

さらに、areasテーブルと紐づいたAreaクラスに::all()とすることでareasテーブルのデータを全て取れます。
AuthファサードやEloquentの説明は長くなるので割愛。

//クエリの取得
$query = DB::table('reviews');
//リレーション
$query->leftJoin('users', 'reviews.user_id', '=', 'users.id');
$query->leftJoin('areas', 'reviews.area_id', '=', 'areas.id');
$query->select('reviews.*', 'users.name', 'users.image', 'areas.area_name');
$query->orderBy('created_at', 'desc');

if($g !== null) {
    //全角スペースを半角に
    $search_split = mb_convert_kana($g, 's');
    //半角で文字を切り分けて、配列に入れる
    $search_split2 = preg_split('/[\s]+/', $search_split, -1, PREG_SPLIT_NO_EMPTY);
    //配列をforeachでまわして、where条件を付け加える
    foreach($search_split2 as $value){
        $query->where('gelande_name', 'LIKE', '%' . $value . '%');
    }
}
if($a !== null) {
    $query->where('area_id', '=', $a);
}
if($s !== null) {
    $query->where('star', '=', $s);
}


$reviews = $query->get();

return view('review.show',compact('reviews', 'user', 'areas', 'g', 'a', 's'));

当初はEloquentでリレーションを張ったのでそれでやろうと思いましたが、DBファサードを使った方が細かくできるということで今回はこちらでやります。
DB::table(‘テーブル名’)でクエリビルダインスタンスを取得し、変数に格納します。
さらにその変数に対してチェインでクエリを書いていきます。
生のSQLとほぼ同じなのでわかりやすいですが、これも生SQLと同じで順番があるようなので調べるようにしましょう。

$_GETの値があるかどうかを調べてあったら、mb_convert_kanaで全角スペースを半角スペースに変換します。preg_splitでその半角スペースで区切られた文字列を半角スペース毎に配列に格納されます。第4引数でPREG_SPLIT_NO_EMPTYを指定することで、半角スペースが続いても空文字列として返さなくできます。

$reviews = $query->get();

return view('review.show',compact('reviews', 'user', 'areas', 'g', 'a', 's'));

最後に、クエリに対してget()で値を取得します。
コントローラーで取得した変数をviewに渡します。

View

<form method="GET" action="{{ route('review.index') }}">
  <div class="form-row justify-content-center">
    <div class="col-md-4 mb-3">
      <label class="mr-sm-2 sr-only" for="a">AreaSearch</label>
      <select class="custom-select mr-sm-2" id="a" name="a">
        <option value="">エリア検索</option>
        @foreach($areas as $area)
          <option value="{{ $area->id }}" @if($a == $area->id) selected @endif>{{ $area->area_name }}</option>
        @endforeach
      </select>
    </div>
    <div class="col-md-4 mb-3">
      <label class="mr-sm-2 sr-only" for="s">StarSearch</label>
      <select class="custom-select mr-sm-2" id="s" name="s">
        <option value="">星の数</option>
          @for($i=5; $i>=1; $i--)
            <option value="{{ $i }}" @if($s == $i) selected @endif>★ x {{ $i }}</option>
          @endfor
      </select>
    </div>
  </div>
  <div class="form-row justify-content-center">
    <div class="col-md-8 mb-3">
      <label class="mr-sm-2 sr-only" for="g">ゲレンデ名検索</label>
      <input name="g" class="form-control mr-sm-2" id="g" type="search" placeholder="ゲレンデ名検索" aria-label="Search" value="{{ $g }}">
    </div>
    <div class="col-md-8 mb-3">
      <label class="mr-sm-2 sr-only" for="search">検索</label>
      <button class="btn btn-info btn-block mr-sm-2" id="search" type="submit">検索</button>
    </div>
  </div>
</form>

検索フォームはこのような感じになります。エリアの選択部分は、コントローラーで記述した

$areas = Area::all();

で取って、viewに渡しています。それをforeachでまわして、1つずつ取得しています。
次に表示部分です。

<div class="gelande-card">
    <aside>
        @if($review->image == null)
            <img src="img/noimage.jpeg" class="button">
        @else
            <img src="/storage/profile_images/{{ $review->image }}" class="button" style="object-fit: cover">
        @endif
    </aside>
    <article>
        <h2>{{ $review->title }}</h2>
        <h3>{{ $review->gelande_name }}</h3>
        <ul>
            <li><i class="i fas fa-mountain" style="color:cornflowerblue"></i><span>{{ $review->area_name }}</span></li>
            <li>
                @for($i=1; $i<=$review->star; $i++)
                    <i class="fas fa-star" style="color:gold"></i>
                @endfor
            </li>
            <li><i class="i fas fa-pen"></i><span>{{ date('Y/n/j', strtotime($review->created_at)) }}</span></li>
        </ul>
        <p>{{ $review->comment }}</p>
        <p class="gelande">by {{ $review->name }}
            @if($review->user_id == $user->id)
            <a href="{{ route('review.edit',[ 'id' => $review->id ]) }}"><i class="i fas fa-edit"></i></a></p>
            @endif
    </article>
</div>

まず、$reviewsにはリレーションを張っているので色々な情報が入っています。

dd($reviews)とやると、こんな感じで出てきます。連想配列になっているので、foreachでまわして取ってあげます。
以上です。