Laravelのみで処理を完結できるなら、Paginate処理はびっくりするくらい簡単です。
が、このご時世。API経由でデータを提供・取得しなければならないことも多々ありますよね。一方で、せっかくフレームワークに便利なPaginate処理があるのに、APIのみ独自ロジックを実装するのは避けたい。。。
というわけで、API経由でLaravelのPaginateを利用する方法を試して見ました。
APIの作成はさておき、クライアント側(API呼び出し側)ではいくつかの実装方法が考えられますが、ここではblade等で実装するPaginate処理(UI操作)とできるだけ同じになるような方法を選択してみました。(なので、ページ遷移が発生するなど、Ajaxという観点では少し汚れたコードです)。
##準備
あるものを極力利用して端折れるところは端折ります。
###環境
以下の環境を利用しています。
- Laravel5.2(Routeの設定の仕方とか少しかわりましたね)
- Mac(El Capitan + MAMP)
- MySQL
###テーブル
テーブルは、migrateすると普通に使えるusersテーブルを使うことにします。生成は、
php artisan maigrate
で完了です。
###データ
Paginationのテストをするとき、しんどいのはたくさんのデータを作成することです。
ここでは、データ生成にFakerを利用し、かつ、Seederでループさせることでテスト用のデータを作成してみます。
まずは、Seederの雛形を生成。
php artisan make:seeder UsersSeeder
記述します。とりあえず35レコードくらい作る。
<?php
use Illuminate\Database\Seeder;
use App\User;
class UsersSeeder extends Seeder
{
public function run()
{
//truncate
User::truncate();
//faker
$faker = Faker\Factory::create('ja_JP');
//insert
for($i=0;$i<35;$i++)
{
$user = User::create();
$user->name = $faker->userName();
$user->email = $faker->unique()->email();
$user->password = Hash::make($user->email);
$user->save();
}
}
}
作成したSeederクラスを実行(シードの対象と)するには、DatabaseSeeder.phpの編集が必要なので行ないます。5.2から少しシンプルになった感じですね。
<?php
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
public function run()
{
$this->call(UsersSeeder::class);
}
}
Seederを実行します。
php artisan db:seed
Seederを実行します。
Hash::make()しているので遅いです。今回は別にHash化はどうでもいいので消してもいいです。
Seederの実行が完了したら、念のためDBを覗いて見ておいた方がいいでしょう。
##普通のPaginateを試してみる
まず、オーソドックスなPaginateも簡単におさらいしておきます。
###Route
とりあえず1つのRouteがあればいいのですが、ここでは、後で使う3つのRoute全てを定義しておきます。
なお、Laravel5.2から、Web系の処理はwebミドルウエアを適用しておいたほうが無難なので、groupで囲っています。
まずはusersを利用します。
Route::group(['middleware' => ['web']], function () {
//普通の一覧用
Route::get('users','UsersController@index');
//JSON API
Route::get('json','UsersController@json');
//APIを呼び出す一覧用
Route::get('ajax','UsersController@ajax');
});
###Controller
UsersController.phpというものを作成し、そこに処理を記述します。
まずは雛形を作ります。
php artisan make:controller UsersController
####indexの実装
まず、
- App\User;
- Illuminate\Support\Facades\Input;
の2つを追加でuseしています(Inputは後から使います)。
ロジックは何ら複雑なことはせず、idを降順でソートして、users.index Viewに渡しているだけです。
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Http\Requests;
use App\Http\Controllers\Controller;
use App\User;
use Illuminate\Support\Facades\Input;
class UsersController extends Controller
{
//普通にページネート
public function index()
{
$query = User::query();
$users = $query->orderBy('id','desc')->paginate(10);
return view('users.index')->with('users',$users);
}
}
わざわざ$queryなんて作る必要はありませんが、私の個人的な趣味(他の記述との整合性)のためにこのような少し冗長な書きかとなっております。すみません。
##View
最後にViewを作ります。
テンプレート等にわざわざ分ける必要も無いのですがこれも習慣として分けました。
とりあえずjQueryとBootstrapを使えるようにしているだけです。
ここではjQueryを使っていますが、Angularつかったりしても、基本概念は同じです。
layout.blade.phpをviews直下に作りました。
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
</head>
<body>
<div class="container">
@yield('content')
</div>
<!-- js -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
@yield('script')
</body>
</html>
viewsの下にusersフォルダを作成し、その中にindex.blade.phpを作成しました。これだけで基本的なPaginationが動きます。シンプル。
@extends('layout')
@section('content')
<h1>一覧表示(普通)</h1>
<table class="table table-striped">
@foreach($users as $user)
<tr>
<td>{{$user->id}}</td>
<td>{{$user->name}}</td>
<td>{{$user->email}}</td>
</tr>
@endforeach
</table>
<!-- page control -->
{!! $users->render() !!}
@stop
###確認して見る。
/usersにアクセスしてみます。ま、普通に動きますわな。
上記のメールアドレス等はFakerが自動生成したものです。個人情報を公開しているわけではありません。
このときページをクリックすると、
http://localhost:8000/users?page=2
のように?page=2という形式を取っています。APIを利用するページでも同じようにしてみたいと思います。
##PaginateをAPI(JSON)化する
いよいよ本題。
まずは、サーバ側です。サーバ側をJSON化するのはすごく簡単です。
###Controllerをいじる
下記のようにreturnをResponse::json($users)に変更するだけ。json()の方。
class UsersController extends Controller
{
//普通にページネート
public function index()
{
$query = User::query();
$users = $query->orderBy('id','desc')->paginate(10);
return view('users.index')->with('users',$users);
}
//jsonを返す
public function json()
{
$query = User::query();
$users = $query->orderBy('id','desc')->paginate(10);
return \Response::json($users);
}
}
なお、真面目な実装においては、わざわざ別のメソッドを用意せず、リクエストのタイプによりレスポンスだけを分けた方がいいと思いますが、ここでは、別のメソッドとして実装します。
###返ってくるJSONを確認してみる
ここで、JSON用のURL(/json)を叩いてみます。すると、下記の様に値がJSONで返ってきます。
{"total":35,"per_page":10,"current_page":1,"last_page":4,"next_page_url":"http:\/\/localhost:8000\/json?page=2","prev_page_url":null,"from":1,"to":10,"data":[{"id":35,"name":"kana.tanabe","email":"hiroshi09@yahoo.co.jp","created_at":"2016-01-30 10:45:41","updated_at":"2016-01-30 01:45:41"},{"id":34,"name":"dtanabe","email":"akira59@hotmail.co.jp","created_at":"2016-01-30 10:45:41","updated_at":"2016-01-30 01:45:41"},{"id":33,"name":"yoshimoto.rika","email":"suzuki.rika@gmail.com","created_at":"2016-01-30 10:45:41","updated_at":"2016-01-30 01:45:41"},{"id":32,"name":"rika.hirokawa","email":"nagisa.rika@suzuki.com","created_at":"2016-01-30 10:45:40","updated_at":"2016-01-30 01:45:40"},{"id":31,"name":"shirokawa","email":"vsuzuki@hotmail.co.jp","created_at":"2016-01-30 10:45:40","updated_at":"2016-01-30 01:45:40"},{"id":30,"name":"momoko.kudo","email":"nagisa.momoko@gmail.com","created_at":"2016-01-30 10:45:40","updated_at":"2016-01-30 01:45:40"},{"id":29,"name":"dyoshimoto","email":"nhirokawa@yahoo.co.jp","created_at":"2016-01-30 10:45:39","updated_at":"2016-01-30 01:45:39"},{"id":28,"name":"akira.uno","email":"suzuki.rika@hotmail.co.jp","created_at":"2016-01-30 10:45:39","updated_at":"2016-01-30 01:45:39"},{"id":27,"name":"rika50","email":"nagisa.naoko@hirokawa.jp","created_at":"2016-01-30 10:45:39","updated_at":"2016-01-30 01:45:39"},{"id":26,"name":"momoko65","email":"luno@mail.goo.ne.jp","created_at":"2016-01-30 10:45:38","updated_at":"2016-01-30 01:45:38"}]}
ここで重要なのは、JSONの頭の方の、
{"total":35,"per_page":10,"current_page":1,"last_page":4,"next_page_url":"http:\/\/localhost:8000\/json?page=2","prev_page_url":null,"from":1,"to":10,"data":[...]}
の部分。ページネート処理に必要なトータルレコード数やページ数、1ページあたりの行数などが含まれています。APIとして利用する場合はクライアント側でこれらの情報を利用してPaginate処理を手動実装する必要があります。
実際のデータは"data":[]という形で配列で返って来ています。
###Pageを送ってみる
先程は/ajaxと何もパラメータを指定しませんでしたが、今度は、/json?page=2として、pageパラメータを付与してみます。
current_pageが2となり、どうやら普通の処理の際と同様にページ送りがされているようです。
また、next_page_urlの有無等も変化しているようなので、これらを応用してPaginate処理を行なってみます。
{"total":35,"per_page":10,"current_page":2,"last_page":4,"next_page_url":"http:\/\/localhost:8000\/json?page=3","prev_page_url":"http:\/\/localhost:8000\/json?page=1","from":11,"to":20,"data":[{"id":25,"name":"tanabe.akira","email":"uno.hiroshi@yoshimoto.com","created_at":"2016-01-30 10:45:38","updated_at":"2016-01-30 01:45:38"},{"id":24,"name":"taro.nagisa","email":"naoko.suzuki@yoshimoto.biz","created_at":"2016-01-30 10:45:38","updated_at":"2016-01-30 01:45:38"},{"id":23,"name":"hhirokawa","email":"naoko.uno@hirokawa.biz","created_at":"2016-01-30 10:45:38","updated_at":"2016-01-30 01:45:38"},{"id":22,"name":"naoko86","email":"uno.hiroshi@yahoo.co.jp","created_at":"2016-01-30 10:45:37","updated_at":"2016-01-30 01:45:37"},{"id":21,"name":"rika19","email":"akira95@mail.goo.ne.jp","created_at":"2016-01-30 10:45:37","updated_at":"2016-01-30 01:45:37"},{"id":20,"name":"momoko66","email":"anagisa@suzuki.info","created_at":"2016-01-30 10:45:37","updated_at":"2016-01-30 01:45:37"},{"id":19,"name":"taro66","email":"hiroshi.hirokawa@yahoo.co.jp","created_at":"2016-01-30 10:45:36","updated_at":"2016-01-30 01:45:36"},{"id":18,"name":"kudo.hiroshi","email":"dnagisa@uno.jp","created_at":"2016-01-30 10:45:36","updated_at":"2016-01-30 01:45:36"},{"id":17,"name":"rnagisa","email":"momoko70@suzuki.com","created_at":"2016-01-30 10:45:36","updated_at":"2016-01-30 01:45:36"},{"id":16,"name":"qtanabe","email":"momoko.hirokawa@yahoo.co.jp","created_at":"2016-01-30 10:45:35","updated_at":"2016-01-30 01:45:35"}]}
##APIを利用してみる
では、APIを利用する画面を作ってみます。
なお、ここでは同じサーバ上にあるLaravelで画面を作ってみますが、通常は別のサーバ上で素のHTMLなどで書くことになるかと思います。その際、クライアント側はほぼおなじですが、サーバはクロスドメイン問題等があるので、API側をJSONPにする必要があると思います。
###Controller
まずは、ControllerにてViewを指定します。/ajaxというリクエストを受けるとusers/ajax.blade.phpで受けます。
また、指定するページ番号は自分自身にパラメーターを送ることで処理します。pageが何もしていされなかった場合はpage=1とするようにしました。
//ajaxでページネート
public function ajax()
{
$page = Input::get('page');
if(empty($page)) $page = 1;
return view('users.ajax')->with('page',$page);
}
###View
処理の肝はViewです。
HTMLは至ってシンプルでテーブルとページネーションの基本構造を記述しているだけで、後はjQueryのappendで要素を追加するようにしています。
一見複雑そうに見えますが、大したことはしていません。大きくは、
- テーブルの表示
- ページネーターの表示
の2つで、テーブルの表示はとにかくpage番号さえ正しく取得できれば、あとは戻ってきたJSONを表示するだけです。ページネーターは、next_page_urlやprev_page_urlがnullかどうか確認しながら、<<や>>ボタンをdisabledにしたりしています。
users/ajax.blade.php
@extends('layout')
@section('content')
<h1>一覧表示(API経由) page={{$page}}</h1>
<table class="table table-striped" id="table">
</table>
<!-- pagenate -->
<ul class="pagination" id="pagination">
</ul>
@stop
@section('script')
<script>
$(function(){
//phpから現在のページを受け取る
var page = {{$page}};
$.getJSON('/json?page={{$page}}',null,function(json){
//グローバルパラメータ取得
var next_page_url = json.next_page_url;
var prev_page_url = json.prev_page_url;
var last_page = json.last_page;
//使わないけど試しに取得しておく
var total = json.total;
var per_page = json.per_page;
var current_page = json.current_page;
// alert(next_page_url);
//テーブルを描画
for(row in json.data)
{
var id = json.data[row].id;
var name = json.data[row].name;
var email = json.data[row].email;
$("#table").append("<tr>\
<td>"+id+"</td>\
<td>"+name+"</td>\
<td>"+email+"</td>\
</tr>");
}
//ページネーターを描画
//Prev 制御
if(prev_page_url == null){
$("#pagination").append("<li class='disabled'><a href=''>«</a></li>");
}else{
$("#pagination").append("<li><a href='/ajax?page="+(page-1)+"'>«</a></li>");
}
//ページリンク
for(var i=0;i<last_page;i++)
{
var link_page = i+1;
//activeにするかどうか
if(page==link_page)
{
$("#pagination").append("<li class='active'><a href='/ajax?page="+link_page+"'>"+link_page+"</a></li>");
}else{
$("#pagination").append("<li><a href='/ajax?page="+link_page+"'>"+link_page+"</a></li>");
}
}
//Next制御
if(next_page_url == null){
$("#pagination").append("<li class='disabled'><a href=''>»</a></li>");
}else{
$("#pagination").append("<li><a href='/ajax?page="+(page+1)+"'>»</a></li>");
}
});
});
</script>
@stop
###確認してみる
当然ですが、普通に動いています。
上記のメールアドレス等はFakerが自動生成したものです。個人情報を公開しているわけではありません。
##積み残し
今回実装したPaginateは問題があります。というのは、ページの数だけページリンクをループしているので、ものすごくページがある場合、ページネーターが長くなります。Laravelでは一定のページ数になった際、
1,2,3,....100,101,102....999,1000,1001
というように、途中が...となる処理を行っています。
正確には、この処理をPagination部分に加える必要がありますが、手間が掛かるのでまた別の機会に。
ただ、最初(5つ)...最後(5つ)くらいの処理は入れたほうがいいでしょうね。
雰囲気だけメモ程度に。
//ページネーターを描画
//Prev 制御
if(prev_page_url == null){
$("#pagination").append("<li class='disabled'><a href=''>«</a></li>");
}else{
$("#pagination").append("<li><a href='/ajax?page="+(page-1)+"'>«</a></li>");
}
if(total > 10){
//制御表示(前半)
for(var i=0;i<5;i++){
var link_page = i+1;
//activeにするかどうか
if(page==link_page)
{
$("#pagination").append("<li class='active'><a href='/ajax?page="+link_page+"'>"+link_page+"</a></li>");
}else{
$("#pagination").append("<li><a href='/ajax?page="+link_page+"'>"+link_page+"</a></li>");
}
}
//spacer
$("#pagination").append("<li class='disabled'><span>...</span></li>");
//制御表示(後半)
for(var i=last_page-4;i<=last_page;i++){
var link_page = i;
//activeにするかどうか
if(page==link_page)
{
$("#pagination").append("<li class='active'><a href='/ajax?page="+link_page+"'>"+link_page+"</a></li>");
}else{
$("#pagination").append("<li><a href='/ajax?page="+link_page+"'>"+link_page+"</a></li>");
}
}
}else{
//ページリンク(通常表示)
for(var i=0;i<last_page;i++)
{
var link_page = i+1;
//activeにするかどうか
if(page==link_page)
{
$("#pagination").append("<li class='active'><a href='/ajax?page="+link_page+"'>"+link_page+"</a></li>");
}else{
$("#pagination").append("<li><a href='/ajax?page="+link_page+"'>"+link_page+"</a></li>");
}
}
}
//Next制御
if(next_page_url == null){
$("#pagination").append("<li class='disabled'><a href=''>»</a></li>");
}else{
$("#pagination").append("<li><a href='/ajax?page="+(page+1)+"'>»</a></li>");
}