Posted at

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


はじめに

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

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

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


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


  1. Laradockで環境を構築する


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


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


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



ソースコード

https://github.com/shonansurvivors/Laravel-Simple-CRUD-Sample/tree/%232


環境


  • 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

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

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

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


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

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

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

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

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


ローカルスコープによりアプリケーション全体で簡単に再利用可能な、一連の共通制約を定義できます。例えば、人気のある(popular)ユーザーを全員取得する必要が、しばしばあるとしましょう。スコープを定義するには、scopeを先頭につけた、Eloquentモデルのメソッドを定義します。

スコープはいつもクエリビルダインスタンスを返します。

Laravel 5.5 Eloquent:利用の開始 - ローカルスコープより抜粋


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


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

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


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を以下の通り変更します。

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


/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

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


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

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

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


/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()の利用 〜

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

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


src/resources/views/items/index.blade.php

//略

<div class="row" >
<div class="col-12">
<!-- TODO:ページネーション -->
</div>
</div>
//略

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


src/resources/views/items/index.blade.php

    <div class="row" >

<div class="col-12">
{{ $items->links() }}
</div>
</div>

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


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

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

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

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

laravel_paginate1.png

2をクリックします。

すると、以下の通り2ページ目が表示されます。

laravel_paginate2.png

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

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


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

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

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

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


src/bootstrap/app.php

//略

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


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

laravel_paginate3.png


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


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

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

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

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

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

laravel_paginate4.png

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

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

laravel_paginate5.png


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

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


/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


最後に

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

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


参考