21
27

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Laravelチュートリアル - 汎用業務Webアプリを作る (3/4) 検索画面とページネーション

Posted at

はじめに

Django経験者が、既存DjangoアプリをLaravelで再現するチュートリアル形式の記事を書くことでLaravelを学びます。

題材とさせていただいたDjangoアプリはこちらです。

Djangoチュートリアル - 汎用業務Webアプリを最速で作る - Qiita

チュートリアル全体の構成

  1. Laradockで環境を構築する

  2. テーブルとCRUD画面を作る

  3. 検索画面とページネーションを作る(本記事

  4. 認証機能を作る(作成予定)

ソースコード

環境

  • macOS High Sierra 10.13.6
  • php 7.2.16
  • Laravel 5.5.45
  • PostgreSQL 9.6.2

3.1. 一覧画面に検索機能を追加する

現状の一覧画面にはitemsテーブルのレコードが全て表示されています。

これに対して検索する(条件に応じて絞り込む)機能を追加します。

3.1.1. ビューの編集

まず、検索条件を入力する画面を作成します。

もともと前記事では、一覧画面の検索をクリックすると以下のモーダル画面が開くところまでは作成していました。

laravel_search1.png

ビューファイルのモーダル画面部分は以下の内容となっています。

src/resources/views/items/index.blade.php
@extends('base')
@section('content')
  <div class="container">
    <div id="myModal" class="modal fade" tabindex="-1" role="dialog">
      <div class="modal-dialog" role="document">
        <div class="modal-content">
          <div class="modal-header">
            <h5 class="modal-title">検索条件</h5>
            <button type="button" class="close" data-dismiss="modal" aria-label="閉じる">
              <span aria-hidden="true">&times;</span>
            </button>
          </div>
          <form id="filter" method="get">
            <div class="modal-body">
              <!-- TODO:検索フォーム -->
            </div>
          </form>
          <div class="modal-footer">
            <a class="btn btn-outline-secondary" data-dismiss="modal">戻る</a>
            <button type="submit" class="btn btn-outline-secondary" form="filter">検索</button>
          </div>
        </div>
      </div>
    </div>
//略
  </div>
//略

上記のビューファイルの<!-- TODO:検索フォーム -->の部分に代わりに以下を記述します。

src/resources/views/items/index.blade.php
//略
<form method="GET" action="{{ route('index') }}" id="myform">
  <div class="form-group">
    <label>名前</label>
    <input type="text" name="name" class="form-control">
  </div>
  <div class="form-group">
    <label for="sex">性別</label>
    <select id="sex" name="sex" class="form-control">
      <option value="">---------</option>
      <option value="1">男性</option>
      <option value="2">女性</option>
      <option value="3">指定無し</option>
    </select>
  </div>
  <div class="form-group">
    <label>備考</label>
    <textarea name="memo" class="form-control"></textarea>
  </div>
</form>
//略

これにより、検索機能のモーダル画面は以下のように表示されるようになりました。

laravel_search2.png

実際に何か検索条件を入力して、検索をクリックするとブラウザのアドレスバーには以下の様に?以降に検索条件が表示されるようになります。

http://localhost/?name=渋沢+栄一&sex=1&memo=医者

以上で、検索画面のビューファイルの作成は完了です。

3.1.2. コントローラーの編集 〜 クエリビルダを使う 〜

次に、itemsテーブルのレコードを実際に絞り込む処理をItemControllerに追加します。

現時点のItemCobtrollerでの、一覧画面表示メソッドであるindexは以下のようになっていました。

/src/app/Http/Controllers/ItemController.php
//略
class ItemController extends Controller
{
    /**
     * 一覧表示
     *
     * @param Request $request
     * @return Response
     */
    public function index(Request $request){
      $items = Item::orderBy('created_at', 'desc')->get();

      return view('items.index', [
        'items' => $items
      ]);
    }
//略
}

これを以下のように修正します。

なお、今回コントローラーに全ての処理を記述していますが、本記事では後ほどモデルに処理を記述するやり方に関しても説明します。

/src/app/Http/Controllers/ItemController.php
//略
class ItemController extends Controller
{
    /**
     * 一覧表示
     *
     * @param Request $request
     * @return Response
     */
    public function index(Request $request){

        $query = Item::query();

        if(isset($request->name)){
            $query->where('name', 'like', '%'.$request->name.'%');
        }

        if(isset($request->sex)){
            $query->where('sex', $request->sex);
        }

        if(isset($request->memo)){
            $query->where('memo', 'like', '%'.$request->memo.'%');
        }

        $items = $query->orderBy('created_at', 'desc')->get();

        return view('items.index', [
            'items' => $items
        ]);
    }
//略
}
```

- `$query = Item::query();`によって、`Item`モデルの**クエリビルダ**が`$query`に代入されます。

- `where()`はクエリビルダのメソッドのひとつです。
  - 第一引数にカラム名を入れます。
  - 第二引数には比較演算子を入れます。
  - 第三引数にはカラムに対して比較を行う値を入れます。

- 例えば`where('name', 'like', '%foo%')`は、SQL文での`WHERE name LIKE '%foo%'`と同じように、「`name`カラムに、fooという文字列を含むレコードに対して」という意味になります。

- `where('sex', $request->sex);`は、`where('sex', '=', $request->sex);`と同じです。`where()`メソッドでは、第二引数の比較演算子が`'='`の場合は、省略できるようになっています。

- 今回`ItemController`に追加したコードのように、クエリビルダに対して、`where()`といった「条件(制約)となるメソッド」を繰り返し実行し、最後に`get()`メソッドを実行することで結果を取得できます。

##3.1.3. 実際に検索画面を使ってみる

検索処理の作成が完了したので、実際に検索画面を使ってみます。

以下の例では、検索条件の名前欄に`津田`と入力して`検索`をクリックしています。

![laravel_search3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/317369/336208f1-812d-c302-51de-e523151f6b6c.png)


すると、`items`テーブルの`name`カラムに`津田`を含むレコードだけが、一覧画面の明細に表示されるようになりました。

一方、アドレスバーを見てみると`http://localhost/?name=津田&sex=&memo=`と表示されています。

`?`のパラメーターが、`ItemContrroller`の`index`メソッドに渡されたことで、上記のように絞り込みが行われました。

#3.2. 検索機能にローカルスコープを利用する

ここまで検索画面を作るにあたり、コントローラーに検索処理の全てを記述しました。

しかし、一般にコントローラーは肥大化しないようにすることが望ましいと言われています。

コントローラーの肥大化を防ぐ方法のひとつとして、Laravelに用意されたローカルスコープという仕組みを使うことが考えられます。

ローカルスコープについて、Laravel公式ドキュメントでは以下のように説明されています。

> ローカルスコープによりアプリケーション全体で簡単に再利用可能な、一連の共通制約を定義できます。例えば、人気のある(popular)ユーザーを全員取得する必要が、しばしばあるとしましょう。スコープを定義するには、scopeを先頭につけた、Eloquentモデルのメソッドを定義します。
>スコープはいつもクエリビルダインスタンスを返します。
>[Laravel 5.5 Eloquent:利用の開始 - ローカルスコープ](https://readouble.com/laravel/5.5/ja/eloquent.html#local-scopes)より抜粋

今回`ItemController`に追加した各`where()`メソッドが、この汎用業務Webアプリの他の箇所でも今後使われるようになるかどうかはわかりませんが、練習の意味でもローカルスコープを使ってみることにします。

##3.2.1. モデルの編集 〜 ローカルスコープの作成 〜

まず、`Item`モデルに以下を記述します。

```php:src/app/Item.php
//略
class Item extends Model
{
//略    
    /**
     * 名前での絞り込み
     * 
     * @param Builder $query
     * @param string|null $name
     * @return Builder
     */
    public function scopeNameFilter($query, string $name = null){

        if(!$name){
            return $query;
        }

        return $query->where('name', 'like', '%'.$name.'%');
    }

    /**
     * 性別での絞り込み
     * 
     * @param Builder $query
     * @param string|null $sex
     * @return Builder
     */
    public function scopeSexFilter($query, string $sex = null){

        if(!$sex){
            return $query;
        }

        return $query->where('sex', $sex);
    }

    /**
     * 備考での絞り込み
     * 
     * @param Builder $query
     * @param string|null $memo
     * @return Builder
     */
    public function scopeMemoFilter($query, string $memo = null){

        if(!$memo){
            return $query;
        }

        return $query->where('memo', 'like', '%'.$memo.'%');
    }
}
```

- ローカルスコープを定義する場合、その名前の先頭には`scope`を付けます。

## 3.2.2. コントローラーの編集 〜 ローカルスコープの利用 〜

次に、`ItemController`での一覧画面表示メソッドである`index`を以下の通り変更します。

(コメントアウトした部分は本来残す必要は無いのですが、比較のために敢えて残しています)

``````php:/src/app/Http/Controllers/ItemController.php
//略
class ItemController extends Controller
{
    /**
     * 一覧表示
     *
     * @param Request $request
     * @return Response
     */
    public function index(Request $request){

        /* ローカルスコープを利用することとした為、コメントアウト
        $query = Item::query();

        if(isset($request->name)){
            $query->where('name', 'like', '%'.$request->name.'%');
        }

        if(isset($request->sex)){
            $query->where('sex', $request->sex);
        }

        if(isset($request->memo)){
            $query->where('memo', 'like', '%'.$request->memo.'%');
        }

        $items = $query->orderBy('created_at', 'desc')->get();
        */

        $items = Item::nameFilter($request->name)
            ->sexFilter($request->sex)
            ->memoFilter($request->memo)
            ->orderBy('created_at', 'desc')
            ->get();

        return view('items.index', [
            'items' => $items
        ]);
    }
```

-  ローカルスコープを使用する場合例えばそれが`scopeNameFilter`という名前であった場合名前の先頭に付けた`scope`は省略する必要があります  

このようにローカルスコープを使っても従来通りの検索機能が実現できます

以上でローカルスコープを用いた`ItemController`の改善は完了です

#3.3. ページネーションの作成

ここからは一覧画面にページネーションの機能を追加します

現状の一覧画面は全ての明細が1画面に表示されています。(もし検索条件を入力しているのであれば検索条件に一致する全ての明細

例えば以下は`Items`テーブルのレコードが全部で9件あり特に検索条件を入力しなかった場合の一覧画面です

9件全てが1ページに表示されていますね

![laravel_paginate0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/317369/53a4646b-2076-4f16-ab89-8e202f46cabb.png)

これに対して一定の件数ごとにページを切り替えて表示するページネーション機能を組み込みます

##3.3.1. コントローラーの編集 〜 `paginate()`の利用 〜

まず`ItemController`における一覧画面表示メソッドである`index`を変更します

これまではクエリビルダの最後で`get()`にて`items`レコードを取得していましたがここを代わりに`paginate()`に変更します

```php:/src/app/Http/Controllers/ItemController.php
//略
class ItemController extends Controller
{
    /**
     * 一覧表示
     *
     * @param Request $request
     * @return Response
     */
    public function index(Request $request){
//略
        $items = Item::nameFilter($request->name)
            ->sexFilter($request->sex)
            ->memoFilter($request->memo)
            ->orderBy('created_at', 'desc')
            ->paginate(3); //get()であったのを変更

        return view('items.index', [
            'items' => $items
        ]);
    }
//略
}
```

- `paginate()`には引数としてひとつのページに表示する明細数を渡します今回は`3`にしておきます

##3.3.2. ビューの編集 〜 `links()`の利用 〜

次に一覧画面のビューに各ページへのリンクを追加します

現時点で一覧画面のビューファイルで以下のようになっている箇所があるかと思います

```html:src/resources/views/items/index.blade.php
//略
    <div class="row" >
      <div class="col-12">
        <!-- TODO:ページネーション -->
      </div>
    </div>
//略
```

こちらを以下の通り変更します

```html:src/resources/views/items/index.blade.php
    <div class="row" >
      <div class="col-12">
        {{ $items->links() }}
      </div>
    </div>
```

以上でページネーション機能の組み込みは完了です

##3.3.3. 実際にページネーション機能を使ってみる

実際にページネーション機能を使ってみます

一覧画面にアクセスすると左上に各ページへのリンクが表示されるようになりました

また`ItemController``index`メソッドで`paginage(3)`と指定したので一覧画面には明細が3件までしか表示されないようになっています

![laravel_paginate1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/317369/e664387e-be55-874d-fc11-147dcbdc1e95.png)

`2`をクリックします
すると以下の通り2ページ目が表示されます

![laravel_paginate2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/317369/4a567ac5-89ee-abf3-4808-9bb939b8a3de.png)

ここでアドレスバーを見ると`http://localhost/?page=2`となっています

`?`以降の`page`というパラメーターに`2`を与えることによってLaravel側で2ページ目を表示するように処理してくれていることがわかります

##3.3.4. ページネーションをBootstrap4標準のデザインに変更する

Laravel5.5ではページネーションの表示は簡素なものとなっています

これをBootstrap4標準のデザインに変更します

`bootstrap`ディレクトリにある`app.php``return $app;`よりも手前で以下を追加してください

```php:src/bootstrap/app.php
//略
Illuminate\Pagination\AbstractPaginator::defaultView("pagination::bootstrap-4"); // この行を追加
//略
return $app;

```

以上によりページネーションがBootstrap4標準のデザインで表示されるようになりました

![laravel_paginate3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/317369/818dd8ec-7229-ae3d-c930-d8a63c2120a5.png)

#3.4. 検索条件と組み合わせた時のページネーションの不具合を改善する

##3.4.1. 現状のページネーションの問題点

ここまでで検索画面とページネーションを作成しましたがこの2つを組み合わせると想定外の表示になるケースがあります

例えば以下は検索条件の`性別``男性`を指定した時の一覧画面1ページ目です

アドレスバーは`http://localhost/?name=&sex=1&memo=`となっており`sex=1`1は男性)の条件に従って男性だけが表示されています

ここまでは特に問題ありません

![laravel_paginate4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/317369/d5c0e960-450f-495c-4f14-b1b89b4219f8.png)

ただしこの画面のページネーションの2ページ目へのリンクのURLは`http://localhost/?page=2`となっており検索条件に入力した条件(`page`以外のパラメーター)が考慮されていません

ですので2ページ目を表示すると以下のように検索条件を一切無視して性別が女性である明細も表示されてしまいます

![laravel_paginate5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/317369/6bb46254-b221-2836-293a-37f6d9fad078.png)

# 3.4.2. ページネーションに検索条件(リクエストのパラメーター)を引き継ぐ  〜 `appends()`と`all()`の利用 〜

この問題点を改善するため`ItemController`の一覧画面表示メソッドである`index`を以下の通り変更します

```php:/src/app/Http/Controllers/ItemController.php
//略
class ItemController extends Controller
{
    /**
     * 一覧表示
     *
     * @param Request $request
     * @return Response
     */
    public function index(Request $request){
//略
        $items = Item::nameFilter($request->name)
            ->sexFilter($request->sex)
            ->memoFilter($request->memo)
            ->orderBy('created_at', 'desc')
            ->paginate(3)
            ->appends($request->all()); // この行を追加

        return view('items.index', [
            'items' => $items
        ]);
    }
//略
}
```

このように`paginate()`の後に`->appends($request->all())`を追加するだけでページネーションに検索条件(リクエストのパラメーターが引き継がれるようになります

先ほどの問題点の例であれば`http://localhost/?page=2`であった2ページ目へのリンクは`http://localhost/?sex=1&page=2`となります

なぜこのように改善されたのか少し長くなりますが順に説明していきます

- クエリビルダに`pagenate()`メソッドを使うことで`$items`には`Illuminate\Pagination\LengthAwarePaginator`オブジェクトが返されています

- この`LengthAwarePaginator`オブジェクトは様々なプロパティを持っておりそのうちのひとつに`query`があります

- 今回の`ItemController`への処理の改善を行う前つまり`appends($request->all())`を行わなかった場合`query`は常に空の配列になっています

```:$itemsをprint_rで表示したもの
Illuminate\Pagination\LengthAwarePaginator Object
(
    [total:protected] => 9
//略
    [perPage:protected] => 3
    [currentPage:protected] => 1
    [path:protected] => http://localhost
    [query:protected] => Array
        (
        )
//略
```

- `appends()`メソッドは上記の`query`に値をセットしてくれます

- `appends($request->all())`とすることにより検索条件を入力して最初に`検索`をクリックした時のリクエストの全てのパラメータ(つまり検索条件)下記のように`query`へ連想配列でセットされます

```:$itemsをprint_rで表示したもの
Illuminate\Pagination\LengthAwarePaginator Object
(
    [perPage:protected] => 3
    [currentPage:protected] => 1
    [path:protected] => http://localhost
    [query:protected] => Array
        (
            [name] => 
            [sex] => 1
            [memo] => 
        )
```

- ページネーションに表示される各ページへのリンクは上記の`query`が考慮されるので例えば2ページ目へのリンクであれば`http://localhost/?sex=1&page=2`となります

##3.4.3. 改善後のページネーション機能を使ってみる

改善後のページネーション機能を使ってみます

一覧画面の2ページ目でも最初に入力した検索条件が考慮されるようになりました

![laravel_paginate6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/317369/4aa70dd0-4e33-8f0a-b5d5-b3a4fbf2257d.png)


# 最後に

以上で検索画面とページネーションの作成は完了です

次の記事では認証機能を追加する予定です

# 参考

- [Laravel5.5でお手軽にフィルタ&検索付きメモアプリを作るチュートリアル - Qiita](https://qiita.com/namaozi/items/11b65ccb6b7ecaefc23e)

- [Laravelのクエリビルダ記法まとめQueryBuilder/DB Facade - 
 RitoLabo](https://www.ritolab.com/entry/93)

- [Laravelローカルスコープについて解説 - とものブログ](https://se-tomo.com/2018/10/12/laravel%E3%81%AF%E3%83%AD%E3%83%BC%E3%82%AB%E3%83%AB%E3%82%B9%E3%82%B3%E3%83%BC%E3%83%97%E3%81%A7%E7%B5%9E%E3%82%8A%E8%BE%BC%E3%82%82%E3%81%86/)

- [[Laravel 5.5] ページネーションを Bootstrap 4 スタイルにする - Qiita ](https://qiita.com/kamikosi/items/d56e6be42608aeafdb6c)

- [Laravelでページネーションを実装する方法 - 実践的Web開発メソッド](https://blog.hiroyuki90.com/articles/laravel-pagination/)

- [Laravelのページネーションで生成されたリンクにGETパラメータを付与する - Qiita](https://qiita.com/tech31/items/5b14aab55e3438ad3dde)

21
27
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
21
27

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?