Laravel5.5でお手軽にフィルタ&検索付きメモアプリを作るチュートリアル

## この記事について

[Laravel Advent Calendar 2017](https://qiita.com/advent-calendar/2017/laravel) 3日目の記事になります。

2日目の記事は、[@m2wasabi](https://qiita.com/m2wasabi)さんの「[Laravelの開発に最適なDockerCompose作ってみた](http://m2wasabi.hatenablog.com/entry/2017/12/02/laravel-docker-adv)」でした。
Laravelは便利ですが環境構築がちょっと面倒ですよね。わたしはVagrant+Ansibleで環境構築していますが、dockerなどでOSに依存しない開発環境が作れると嬉しいですよね！

この記事では、Laravelのクエリスコープ機能を使って、フィルタリング＆検索機能をお手軽に実装する方法について書きます。
HomesteadなどのLaravel実行環境がある前提で話を進めていきます。チュートリアルを追ってみたい方はまずは実行環境をご用意ください。


## 作るもの

フィルタリング＆検索機能付きのメモ帳アプリを作ってみます。
1つのメモは

- **title** （タイトル）
- **tag** （タグ）

の2つの要素を持ち、

- URLのクエリパラメータから、
- **tagを指定してフィルタリング** しつつ、
- **titleを指定文字で検索** 可能

である、シンプルなメモアプリを目指します。

（※TODOアプリによくあるCRUD機能は省略します。）

## 環境

- PHP 7.1.12
- Laravel 5.5.22

## ソースコード
https://github.com/namaozi/laravel-query-filter-sample

## 下準備

### アプリ作成
この章はクエリスコープ関係ないので適宜読み飛ばしてください。
まずはプレーンなアプリを作ります。

~~~sh:console
$ composer create-project --prefer-dist laravel/laravel laravel-query-filter-sample
~~~

Memoコントローラー・モデル・テーブルをartisanコマンドを叩きながら作っていきます。

### コントローラー作成
メモ一覧を表示するindexアクションだけ作っておきましょう。

~~~sh:console
$ php artisan make:controller MemosController
~~~

~~~php:MemosController.php
<?php

namespace App\Http\Controllers;

use App\Memo;
use Illuminate\Http\Request;

class MemosController extends Controller
{
    /**
     * メモ一覧を表示する。
     *
     * @return \Illuminate\Http\Response
     */
    public function index()
    {
        $memos = Memo::all();
        return $memos;
    }
}
~~~

### テーブル作成
Tagモデルを作って参照させたほうがいいですが、かんたんのためstringでtagを持たせます。

~~~sh:console
$ php artisan make:migration create_memos_table
$ php artisan migrate
~~~

~~~diff:YYYY_MM_DD_hhmmss_create_memos_table.php
<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateMemosTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('memos', function (Blueprint $table) {
            $table->increments('id');
+            $table->string('title');
+            $table->string('tag');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('memos');
    }
}
~~~

### モデル作成
空のモデルでよいですがguardedを宣言しておきます。

~~~sh:console
$ php artisan make:model Memo
~~~

~~~php:Memo.php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Memo extends Model
{
    // カラムに代入を拒否しない
    protected $guarded = [];
}
~~~

### ルーティング変更
初期設定ではルートビュー `/` へのアクセスでwelcomeページに飛ぶようになっているので、ここに `index()` アクションを割り当てましょう。

```diff:web.php
-Route::get('/', function () {
-    return view('welcome');
-});
+Route::get('/', 'MemosController@index');
```

### データ作成
これでメモ一覧がルートビュー `/` で見れるようになっています。が、まだメモのデータが無いので `php artisan tinker` でデータをいくつか作りましょう。

~~~sh:console
$ php artisan tinker
>>> use \App\Memo;
>>> Memo::create(['title' => 'Go Arcade', 'tag' => 'todo'])
=> App\Memo {#750
     title: "Go Arcade",
     tag: "todo",
     updated_at: "2017-12-01 16:56:56",
     created_at: "2017-12-01 16:56:56",
     id: 1,
   }

>>> Memo::create(['title' => 'Buy Milk', 'tag' => 'todo'])
=> （以下省略）
~~~

これでブラウザからルートビューにアクセスしてみると、メモ一覧がJSONで表示されます。

### ビューの作成
このままでもよいですが、さみしいのでテーブル表示するビュー `index.blade.php` を作ってみます。（省略）（[ソースコード](https://github.com/namaozi/laravel-query-filter-sample/blob/b36437627546678e39d9202c0d920a3fc95de890/resources/views/memos/index.blade.php)）
コントローラーを修正してブラウザからみると、こんな感じの表示になりました。

~~~diff:MemosController.php
@@ -15,6 +15,6 @@ class MemosController extends Controller
     public function index()
     {
         $memos = Memo::all();
-        return $memos;
+        return view('memos.index', compact('memos'));
     }
~~~

<img width="348" alt="スクリーンショット 2017-12-02 3.19.41.png" src="https://qiita-image-store.s3.amazonaws.com/0/103754/ee26ed27-03f5-dc4e-6b65-2ff9364a5d16.png">

（※わたしの環境ではIP192.168.56.20でローカルにVMを立てています）

これで下準備は終わりです！

## クエリスコープとは？
ようやく本題です。クエリスコープは指定したモデルのクエリに制約をつけて、クエリを書き換えることができる機能です。
https://readouble.com/laravel/5.5/ja/eloquent.html
グローバルスコープとローカルスコープがありますが、今回はローカルスコープを使います。

## フィルタリング機能の実装
ローカルスコープが非常にフィルタリングと相性がいいので紹介したかったんです！

ローカルスコープのクエリスコープは、Eloquentモデルにscopeを先頭につけたメソッドを定義するだけで利用できます。
クエリスコープを使ってタグの文字列でフィルタリングするメソッドを実装をしてみましょう。

~~~diff:Memo.php
@@ -8,4 +8,18 @@ class Memo extends Model
 {
     // カラムに代入を拒否しない
     protected $guarded = [];
+
+    /**
+     * タグでフィルタリング
+     * @param \Illuminate\Database\Eloquent\Builder $query
+     * @param string|null $tag
+     * @return \Illuminate\Database\Eloquent\Builder
+     */
+    public function scopeTagFilter($query, ?string $tag)
+    {
+        if (!is_null($tag)) {
+            return $query->where('tag', $tag);
+        }
+        return $query;
+    }
 }
~~~

さっそくコントローラーから利用してみましょう。

~~~diff:MemosController.php
@@ -14,7 +14,8 @@ class MemosController extends Controller
      */
     public function index()
     {
-        $memos = Memo::all();
+        $memos = Memo::tagFilter('todo') // todoタグで絞り込み
+            ->get(); //結果を取得
         return view('memos.index', compact('memos'));
     }
 }
~~~

利用するときはEloquentモデルやクエリビルダーから、scope接頭語を外して呼び出します（ [クエリビルダーは2種類ある](http://nextat.co.jp/staff/archives/131) のでややこしいですが）。

また、`scopeTagFilter` の第一引数 `$query` には、メソッドチェーンで渡されてきたクエリが自動的に渡されるので、明示的に渡す必要はありません。

それから、返り値もクエリビルダーであるため、結果を取得するには `get()` メソッドが必要なことに注意してください。

ブラウザで結果を見てみると、Todoタグのメモが絞り込みされているのがわかります。
<img width="341" alt="スクリーンショット 2017-12-02 4.09.11.png" src="https://qiita-image-store.s3.amazonaws.com/0/103754/65aab944-20e7-b5e4-53a6-6adec6a582ee.png">

## URLのクエリパラメータからフィルタリング
さて、コントローラー内で直接検索タグを指定するのは不便、というかユーザが機能を使えないのであんまりうれしくないですね(´･◡･｀)
URLのクエリパラメータから検索タグを `/?tag=account` のように指定できるようにしましょう。

~~~diff:MemosController.php
@@ -14,7 +14,7 @@ class MemosController extends Controller
      */
     public function index()
     {
-        $memos = Memo::tagFilter('todo')
+        $memos = Memo::tagFilter(request('tag'))
             ->get();
         return view('memos.index', compact('memos'));
     }
~~~

Laravelの `request()` ヘルパーを使ってクエリパラメータtagの値をtagFilterに渡しています。
ブラウザで結果を見てみると、これだけの対応でちゃんとクエリパラメータで絞り込み出来ていますね👍

<img width="355" alt="スクリーンショット 2017-12-02 4.30.14.png" src="https://qiita-image-store.s3.amazonaws.com/0/103754/3be4f864-9c7c-86fa-77fc-0a0ea2543ccc.png">

## 検索機能の実装

文字列検索もとてもシンプルに可能なので紹介します。
メモのタイトルで検索できるメソッドを実装してみましょう。

~~~diff:Memo.php
@@ -22,4 +22,18 @@ class Memo extends Model
         }
         return $query;
     }
+
+    /**
+     * タイトルで検索する
+     * @param \Illuminate\Database\Eloquent\Builder $query
+     * @param string|null $word
+     * @return \Illuminate\Database\Eloquent\Builder
+     */
+    public function scopeSearchTitle($query, ?string $word)
+    {
+        if (!is_null($word)) {
+            return $query->where('title', 'like', '%' . $word . '%');
+        }
+        return $query;
+    }
~~~

凝った検索が不要なら、SQLのLIKE句で検索を実装するのが最も楽だと思います。
コントローラーから使ってみてブラウザからアクセスしてみましょう。

~~~diff:MemosController.php
@@ -14,7 +14,7 @@ class MemosController extends Controller
      */
     public function index()
     {
-        $memos = Memo::tagFilter(request('tag'))
+        $memos = Memo::searchTitle(request('word'))
             ->get();
         return view('memos.index', compact('memos'));
     }
~~~

<img width="326" alt="スクリーンショット 2017-12-02 4.58.04.png" src="https://qiita-image-store.s3.amazonaws.com/0/103754/976895a2-8b9c-e94b-ff6d-a16be000d4ed.png">

ちゃんと検索できていますね！😄

## フィルタリングの重ねがけ
クエリスコープの最大の利点がメソッドチェーンが使えることではないでしょうか。
このおかげでフィルタリングの重ねがけのような機能を簡単に使うことが出来ます。
先ほど実装したフィルタリングと検索を同時にやってみましょう。（1つメモを追加してみました）

~~~diff:MemosController.php
@@ -14,7 +14,8 @@ class MemosController extends Controller
      */
     public function index()
     {
-        $memos = Memo::searchTitle(request('word'))
+        $memos = Memo::tagFilter(request('tag'))
+            ->searchTitle(request('word'))
             ->get();
         return view('memos.index', compact('memos'));
     }
~~~

<img width="1221" alt="スクリーンショット 2017-12-02 5.13.54.png" src="https://qiita-image-store.s3.amazonaws.com/0/103754/29dcc246-6eb9-c2ba-6322-8830f0e78013.png">

フィルタと検索2つの絞り込みが同時にできていますね！🎊

## まとめ
この記事では、クエリスコープの機能を使ってお手軽にフィルタ＆検索を実装する方法を順を追って説明しました。
ざっと見返しても、追加した（緑色にハイライトされている）コード量はかなり少ないのではないでしょうか。

URLでフィルタリングや検索ができるようになれば、よく使う組み合わせをブックマークしたり、フィルタ結果を他のユーザと共有することも簡単にできますね。実装のメリットは大きいと思います。
こうしたコスパのいい機能がとても簡単に実装できるLaravel、すばらしいですね！😁

### 追記
この記事では説明しませんでしたが、いろいろなページにまたがってタグなどをフィルタリングしたい場合、タグ一覧などをView Composerでビューにもたせておくのがオススメです。

タグ一覧などからボタンなどで絞り込み可能なUIを作るのも、Laravelの強力なヘルパーを使えば楽に実装できます。是非試してみてください！💁