はじめに
Laravel 5 でページネーションを実装するのは簡単ですが、Prev, Next だけではなく、最初または最後のページに飛ぶ First, Last リンクを付け足そうとする場合、どうすればいいかということを書いていきます。
環境
PHP 5.4.39
Laravel 5.0.22
MySQL 5.6.23
アプリケーションの準備、設定
データベースの作成:
mysql> CREATE DATABASE hoge;
Laravel のインストール:
(グローバルに Composer がインストールされていてかつパスが通っていると仮定した場合)
cd /path/to
composer create-project laravel/laravel --prefer-dist hoge
cd hoge
chmod -R 777 storage
.env の設定:
DB_HOST=localhost
DB_DATABASE=hoge
DB_USERNAME=yourusername
DB_PASSWORD=yourpassword
PHP ビルトインサーバを起動し、ブラウザで http://localhost:8000/ へ:
php artisan serve
Post モデルの作成:
(最初から入っている User 関連のファイルを削除しておく)
php artisan fresh
php artisan make:model Post
database/migrations/xxx_create_posts_table.php を修正:
class CreatePostsTable extends Migration
{
public function up()
{
Schema::create('posts', function(Blueprint $table) {
$table->increments('id');
$table->text('body');
});
}
マイグレーションを適用:
php artisan migrate
database/seeds/ に PostsTableSeeder.php を作成:
(ダミーデータ追加のコードを書く)
class PostsTableSeeder extends Seeder
{
public function run()
{
DB::table('posts')->truncate();
$data = [];
foreach (range(1, 1000) as $i) {
$data[] = ['body' => 'body'.$i];
}
DB::table('posts')->insert($data);
}
}
database/seeds/DatabaseSeeder.php にて PostsTableSeeder を呼ぶ:
class DatabaseSeeder extends Seeder
{
public function run()
{
Model::unguard();
$this->call('PostsTableSeeder'); // 追加
}
}
ダミーデータを追加:
php artisan optimize
php artisan db:seed
resources/views/ に layouts ディレクトリを作成し、そこに default.blade.php を作成:
(とりあえず Bootstrap CDN を使う)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css">
<title></title>
</head>
<body>
<div class="container">
@yield('content')
</div>
</body>
</html>
これで準備は OK です。
ページネーションの実装
app/Post.php:
(perPage プロパティでページ毎に何件データを表示するか指定できる)
class Post extends Model
{
// protected $perPage = 10;
}
app/Http/Controller/WelcomeController.php:
use App\Post;
class WelcomeController extends Controller
{
public function index()
{
$posts = Post::paginate();
// ここでも perPage を指定できる。動的に変化させたいときなどに便利?
// $posts = Post::paginate(10);
return view('welcome', compact('posts'));
}
}
resources/views/welcome.blade.php を修正:
@extends('layouts.default')
@section('content')
{!! $posts->render() !!}
<hr>
@foreach ($posts as $post)
{{ $post->body }}
<br>
@endforeach
@stop
ブラウザで確認すると Twitter の Bootstrap なスタイルのページネーションが表示されているかと思います。
これを First, Last 付きのページネーションにしていきます。見た目は以下。
First, Last 付きのページネーションの実装
その前に $posts->render()
って何をやっているんでしょう?Laravel 5.0 - Pagination に何かヒントがあるはずです。以下の 2 つが何かあやしそうです。
上記の各クラスの render() メソッドのコードに答えが載っていました。例えば LengthAwarePaginator クラスの場合だと $presenter が空の場合、つまり $posts->render()
にすると BootstrapThreePresenter クラスを呼ぶような実装になっています。ということはここに独自のクラスを差し込むことによって、First, Last 付きのページネーションが作れそうです。ということで作ってみましょう。
app/Http/Pagination/CustomBootstrapPresenter.php を作成:
namespace App\Http\Pagination;
use Illuminate\Pagination\BootstrapThreePresenter;
use Illuminate\Contracts\Pagination\Paginator as PaginatorContract;
class CustomBootstrapThreePresenter extends BootstrapThreePresenter
{
use BootstrapThreeFirstLastButtonRendererTrait;
use UrlWindowPresenterTrait;
/**
* @inheritdoc
*/
public function __construct(PaginatorContract $paginator, CustomUrlWindow $window = null)
{
$this->paginator = $paginator;
$this->window = is_null($window) ? CustomUrlWindow::make($paginator) : $window->get();
}
/**
* @inheritdoc
*/
public function render()
{
if ($this->hasPages()) {
return sprintf(
'<ul class="pagination">%s %s %s %s %s</ul>',
$this->getFirstButton(),
$this->getPreviousButton(),
$this->getLinks(),
$this->getNextButton(),
$this->getLastButton()
);
}
return '';
}
}
BootstrapThreePresenter クラスを参考にし、それを継承して First, Last のリンクのみ追加した形をとりました。ボタン部分の実装はトレイトで書かれていたので、同じような形にしました。
namespace App\Http\Pagination;
trait BootstrapThreeFirstLastButtonRendererTrait
{
/**
* Get the first page pagination element.
*
* @param string $text
* @return string
*/
protected function getFirstButton($text = '««')
{
if ($this->paginator->currentPage() <= 1) {
return $this->getDisabledTextWrapper($text);
}
return $this->getPageLinkWrapper(
$this->paginator->url(1),
$text,
'first'
);
}
/**
* Get the last page pagination element.
*
* @param string $text
* @return string
*/
protected function getLastButton($text = '»»')
{
if (!$this->paginator->hasMorePages()) {
return $this->getDisabledTextWrapper($text);
}
return $this->getPageLinkWrapper(
$this->paginator->url($this->lastPage()),
$text,
'last'
);
}
}
あと、「...」を削除するために UrlWindowPresenterTrait を参考にして独自の UrlWindowPresenterTrait を作成:
( $html .= $this->getDots();
行を削除しただけです )
namespace App\Http\Pagination;
trait UrlWindowPresenterTrait {
/**
* Render the actual link slider.
*
* @return string
*/
protected function getLinks()
{
$html = '';
if (is_array($this->window['first'])) {
$html .= $this->getUrlLinks($this->window['first']);
}
if (is_array($this->window['slider'])) {
$html .= $this->getUrlLinks($this->window['slider']);
}
if (is_array($this->window['last'])) {
$html .= $this->getUrlLinks($this->window['last']);
}
return $html;
}
/**
* Get the links for the URLs in the given array.
*
* @param array $urls
* @return string
*/
protected function getUrlLinks(array $urls)
{
$html = '';
foreach ($urls as $page => $url) {
$html .= $this->getPageLinkWrapper($url, $page);
}
return $html;
}
/**
* Get HTML wrapper for a page link.
*
* @param string $url
* @param int $page
* @param string|null $rel
* @return string
*/
protected function getPageLinkWrapper($url, $page, $rel = null)
{
if ($page == $this->paginator->currentPage()) {
return $this->getActivePageWrapper($page);
}
return $this->getAvailablePageWrapper($url, $page, $rel);
}
}
最後に UrlWindow を継承した CustomUrlWindow クラスを作成:
( 「...」以降、もしくは以前のリンクを除いていきます )
namespace App\Http\Pagination;
use Illuminate\Contracts\Pagination\LengthAwarePaginator as PaginatorContract;
use Illuminate\Pagination\UrlWindow;
class CustomUrlWindow extends UrlWindow
{
/**
* @inheritdoc
*/
protected function getSmallSlider()
{
return [
'first' => $this->paginator->getUrlRange(1, $this->lastPage()),
'slider' => null,
'last' => null,
];
}
/**
* @inheritdoc
*/
protected function getSliderTooCloseToBeginning($window)
{
return [
'first' => $this->paginator->getUrlRange(1, $window + 1),
'slider' => null,
'last' => null,
];
}
/**
* @inheritdoc
*/
protected function getSliderTooCloseToEnding($window)
{
$last = $this->paginator->getUrlRange(
$this->lastPage() - $window,
$this->lastPage()
);
return [
'first' => null,
'slider' => null,
'last' => $last,
];
}
/**
* @inheritdoc
*/
protected function getFullSlider($onEachSide)
{
return [
'first' => null,
'slider' => $this->getAdjacentUrlRange($onEachSide),
'last' => null,
];
}
}
$this->getStart()
, $this->getFinish()
辺りを null にすれば良いかと思います。
これで完了です。
ビュー側では上記で作ったクラスを利用する形をとります:
@extends('layouts.default')
@section('content')
{!! (new App\Http\Pagination\CustomBootstrapPresenter($posts))->render() !!}
<hr>
@foreach ($posts as $post)
{{ $post->body }}
<br>
@endforeach
@stop
これで First, Last が追加され、良い感じに飛べるようになっているかと思います。ビュー側の new 以降が長いなぁってときはコントローラで以下のようにしてもいけるみたいです。
namespace App\Http\Controllers;
use App\Post;
use App\Http\Pagination\CustomBootstrapThreePresenter;
class WelcomeController extends Controller
{
public function index()
{
$posts = Post::paginate();
$presenter = new CustomBootstrapThreePresenter($posts);
return view('welcome', compact('posts', 'presenter'));
}
}
@extends('layouts.default')
@section('content')
{!! $presenter->render() !!}
<hr>
@foreach ($posts as $post)
{{ $post->body }}
<br>
@endforeach
@stop
まとめ
Laravel 自体最近触り始めたばかりなのでちょっと苦労しました。ただ、おそらくここらへんもう少し手軽にできるプラグインなりがあるような気もします。
とりあえず Paginator と LengthAwarePaginator クラスの render() メソッドがどういう感じで動いているかの参考に少しでもなっていれば幸いです。
あと Laravel の流儀がまだ理解できていない状態なので、もし変なコード書いていたら言ってください。