Laravel 5 で First, Last 付きのページネーションを作る

  • 47
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

はじめに

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 を作成:
(ダミーデータ追加のコードを書く)

PostsTablerSeeder.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 を呼ぶ:

DatabaseSeder.php
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 を使う)

default.blade.php
<!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 プロパティでページ毎に何件データを表示するか指定できる)

Post.php
class Post extends Model
{
    // protected $perPage = 10;
}

app/Http/Controller/WelcomeController.php:

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 を修正:

welcome.blade.php
@extends('layouts.default')

@section('content')
    {!! $posts->render() !!}
    <hr>
    @foreach ($posts as $post)
        {{ $post->body }}
        <br>
    @endforeach
@stop

ブラウザで確認すると Twitter の Bootstrap なスタイルのページネーションが表示されているかと思います。

example1.png

これを First, Last 付きのページネーションにしていきます。見た目は以下。

example2.png

First, Last 付きのページネーションの実装

その前に $posts->render() って何をやっているんでしょう?Laravel 5.0 - Pagination に何かヒントがあるはずです。以下の 2 つが何かあやしそうです。

上記の各クラスの render() メソッドのコードに答えが載っていました。例えば LengthAwarePaginator クラスの場合だと $presenter が空の場合、つまり $posts->render() にすると BootstrapThreePresenter クラスを呼ぶような実装になっています。ということはここに独自のクラスを差し込むことによって、First, Last 付きのページネーションが作れそうです。ということで作ってみましょう。

app/Http/Pagination/CustomBootstrapPresenter.php を作成:

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 のリンクのみ追加した形をとりました。ボタン部分の実装はトレイトで書かれていたので、同じような形にしました。

BootstrapThreeFirstLastButtonRendererTrait.php
namespace App\Http\Pagination;

trait BootstrapThreeFirstLastButtonRendererTrait
{
    /**
     * Get the first page pagination element.
     *
     * @param  string  $text
     * @return string
     */
    protected function getFirstButton($text = '&laquo;&laquo;')
    {
        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 = '&raquo;&raquo;')
    {
        if (!$this->paginator->hasMorePages()) {
            return $this->getDisabledTextWrapper($text);
        }

        return $this->getPageLinkWrapper(
            $this->paginator->url($this->lastPage()),
            $text,
            'last'
        );
    }
}

あと、「...」を削除するために UrlWindowPresenterTrait を参考にして独自の UrlWindowPresenterTrait を作成:
( $html .= $this->getDots(); 行を削除しただけです )

UrlWindowPresenterTrait.php
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 クラスを作成:
( 「...」以降、もしくは以前のリンクを除いていきます )

CustomUrlWindow.php
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 にすれば良いかと思います。
これで完了です。

ビュー側では上記で作ったクラスを利用する形をとります:

welcome.blade.php
@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 以降が長いなぁってときはコントローラで以下のようにしてもいけるみたいです。

WelcomeController.php
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'));
    }
}
welcome.blade.php
@extends('layouts.default')

@section('content')
    {!! $presenter->render() !!}
    <hr>
    @foreach ($posts as $post)
    {{ $post->body }}
    <br>
    @endforeach
@stop

まとめ

Laravel 自体最近触り始めたばかりなのでちょっと苦労しました。ただ、おそらくここらへんもう少し手軽にできるプラグインなりがあるような気もします。

とりあえず Paginator と LengthAwarePaginator クラスの render() メソッドがどういう感じで動いているかの参考に少しでもなっていれば幸いです。

あと Laravel の流儀がまだ理解できていない状態なので、もし変なコード書いていたら言ってください。

関連リンク