Laravelのページネーション機能のリンクの数はどうやって決まっているのか

2022/11/19に公開

バージョン

・PHP 8.1.9
・Laravel 9.24.0

前置き

自作しようと思うと大変なページネーションだが、Laravelでは簡単に実装することができる。

以下のように、DBからのデータ取得にpaginate(1ページに表示したいデータの数)を使用し、

public function index()
{
    return view('user.index', [
        'users' => DB::table('users')->paginate(15)
    ]);
}



viewファイルでlinks()で表示することができる。

{{ $users->links() }}



さらにページネーションのリンクの数を調整したい場合は、onEachSide()を使用することで実現できる。
ドキュメントには、

onEachSideメソッドを使用して、ページネータが生成するリンクの中央のスライディングウィンドウ内の現在のページの両側に表示する追加のリンク数を制御できます。

とあるが、ページネーションの動作を確認してみると、その動きは複雑で「これはどういうルールでリンクが表示されているんだ?」となったのが調査の始まり。

ページネーションのデザイン変更などを含む細かいアレンジを行う際は、以下のコマンドを実行することで作成されるviewsフォルダ配下のvendor/pagination/tailwind.blade.phpの中身を変更することで実現できる。

php artisan vendor:publish --tag=laravel-pagination

ページネーションのリンクはこのファイル内で使用されている配列$elementsを元に表示されている。
なのでこの$elementsがどういう風に作られているかを調べればLaravelのページネーションリンクの表示ルールが分かる。
ということでググってみたがピンとくる情報がヒットせず、自分で追いかけてみることにした。

そもそもどういうデータが渡ってきている?

例えばこのようなページネーションの場合は以下のようなデータが$elementsに入っている。

^ array:3 [0 => array:2 [1 => "http://localhost:8080?page=1"
    2 => "http://localhost:8080?page=2"
  ]
  3 => "..."
  4 => array:4 [7 => "http://localhost:8080?page=7"
    8 => "http://localhost:8080?page=8"
    9 => "http://localhost:8080?page=9"
    10 => "http://localhost:8080?page=10"
  ]
]

これはページングの数が多く、"..."がある場合
"..."がない場合は、リンク先のすべてのURLが1つの配列に格納される形になっている。

結論

ページの数や現在のページで分岐が多く、長くなるので結論から。

四角枠内が分岐の条件だが、"全ページ数"や"現在のページ"でページネーションのリンクの数を決めるメソッドが変わる。
またonEachSideメソッドの引数で表示されるリンクの数が変化する。
onEachSideメソッドを使用していないときの初期値は3になっている模様。

ロジックを追いかけてみる

$elementsはIlluminate\Pagination\LengthAwarePaginator.php内、LengthAwarePaginatorクラスのelementsメソッドで作成されている。

/**
* Get the array of elements to pass to the view.
*
* @return array
*/
protected function elements()
{
    $window = UrlWindow::make($this);

    return array_filter([
        $window['first'],
        is_array($window['slider']) ? '...' : null,
        $window['slider'],
        is_array($window['last']) ? '...' : null,
        $window['last'],
    ]);
}

UrlWindow::make($this)は"first"、"slider"、"last"のキーを持つ配列が返ってくるようになっており、"slider"、"last"それぞれに値があるかどうかでページネーションに"..."が入ってくるか変わってくる。

上記のmakeメソッドを追いかけるとUrlWindowクラスの以下の記述にたどり着く。

/**
* Get the window of URLs to be shown.
*
* @return array
*/
public function get()
{
    $onEachSide = $this->paginator->onEachSide;

    if ($this->paginator->lastPage() < ($onEachSide * 2) + 8) {
        return $this->getSmallSlider();
    }

    return $this->getUrlSlider($onEachSide);
}

$this->paginator->onEachSideはviewファイルで指定したonEachSideメソッドの引数が渡される。(onEachSideメソッド自体を使っていない場合は3がデフォルトで渡される。)

例えば、$onEachSideの引数が1の場合だと、ページの数が10未満でgetSmallSliderメソッドが実行され、10以上だとgetUrlSliderメソッドが実行される。


getSmallSliderメソッドは以下のように、firstのみ値が入った配列が返されるようになっている。
また、getUrlRangeメソッドではすべてのページのURLが取得される。

/**
* Get the slider of URLs there are not enough pages to slide.
*
* @return array
*/
protected function getSmallSlider()
{
    return [
        'first' => $this->paginator->getUrlRange(1, $this->lastPage()),
        'slider' => null,
        'last' => null,
    ];
}



getUrlSliderメソッドでは再度分岐が生じており、現在のページにより処理が変わる。
全体のページ数が12、$onEachSideを1とした場合、
・現在のページが1~5ページだとgetSliderTooCloseToBeginningメソッド
・現在のページが6~7ページだとgetFullSliderメソッド
・現在のページが8~12ページだとgetSliderTooCloseToEndingメソッド
ということになる。

/**
* Create a URL slider links.
*
* @param  int  $onEachSide
* @return array
*/
protected function getUrlSlider($onEachSide)
{
    $window = $onEachSide + 4;

    if (! $this->hasPages()) {
        return ['first' => null, 'slider' => null, 'last' => null];
    }

    if ($this->currentPage() <= $window) {
        return $this->getSliderTooCloseToBeginning($window, $onEachSide);
    }

    elseif ($this->currentPage() > ($this->lastPage() - $window)) {
        return $this->getSliderTooCloseToEnding($window, $onEachSide);
    }

    return $this->getFullSlider($onEachSide);
}



$onEachSide=1とした場合、getSliderTooCloseToBeginningメソッドはfirstに1~6ページのURL、lastに最後から2ページのURLが入る。
※割愛するがgetStart()は最初の2ページ、getFinish()は最後の2ページのURLを取得する。
アウトプットは以下

getSliderTooCloseToEndingメソッドはfirstに最初の2ページのURL、lastに7~12ページのURLが入る。
アウトプットは以下

getFullSliderメソッドは最初と最後に2ページづつ、"slider"にはgetAdjacentUrlRangeにより$onEachSideの数だけ、現在のページの前後にリンクが表示される。
アウトプットは以下

/**
* Get the slider of URLs when too close to the beginning of the window.
*
* @param  int  $window
* @param  int  $onEachSide
* @return array
*/
protected function getSliderTooCloseToBeginning($window, $onEachSide)
{
    return [
        'first' => $this->paginator->getUrlRange(1, $window + $onEachSide),
        'slider' => null,
        'last' => $this->getFinish(),
    ];
}

/**
* Get the slider of URLs when too close to the ending of the window.
*
* @param  int  $window
* @param  int  $onEachSide
* @return array
*/
protected function getSliderTooCloseToEnding($window, $onEachSide)
{
    $last = $this->paginator->getUrlRange(
        $this->lastPage() - ($window + ($onEachSide - 1)),
        $this->lastPage()
    );

    return [
        'first' => $this->getStart(),
        'slider' => null,
        'last' => $last,
    ];
}

/**
* Get the slider of URLs when a full slider can be made.
*
* @param  int  $onEachSide
* @return array
*/
protected function getFullSlider($onEachSide)
{
    return [
        'first' => $this->getStart(),
        'slider' => $this->getAdjacentUrlRange($onEachSide),
        'last' => $this->getFinish(),
    ];
}

まとめ

Laravelのページネーションのリンクの数は"全ページ数"、"現在のページ"、"onEachSideメソッドの引数"の3つに依存し、特に"全ページ数"、"現在のページ"によってはメソッド自体が変化する。

Laravelのページネーションはviewファイルへの1行の記述ですぐに表示できるような優れものだが、使うからにはどういう挙動をするのか、知っておきたかったので整理できてよかった。

Discussion