やりたいこと

データベース:ペジネーション 5.8 Laravel

このページの「独自ペジネータ作成」の欄にあるように、
Illuminate\Pagination\LengthAwarePaginatorインスタンスを作成して、自前の Collection を Collection のままページネーションしたい。

結論

ペジネーションは引数に Collection を許容しているので実装が可能。
LengthAwarePaginatorインスタンスの第一引数に渡す Collection を、slice()メソッドを使って Collection の型を崩さずに区切る。

これだけ見ると「自明では?」と思うけど、初心者(私)はどハマりしたので過程も含めて書いておきます。

ハマった流れ

まず「Eloquent ORM から取ってきた結果をページネーションしたいわね」と思い Laravel くんドキュメント(前述)を参照しました。

Note: 自前でペジネーターインスタンスを生成する場合、ペジネーターに渡す結果の配列を自分で”slice”する必要があります。その方法を思いつかなければ、 [array_slice] PHP 関数を調べてください。

なるほど!!!!!

と思い array_slice()を使用して記述したのが以下のような書き方。

    // app/Services/ItemsService.php
    // 前略。$columnsには取得したいカラム名を格納

    $items = ItemMaster::query()
        ->select($columns)
        ->with('relatedUsers')
        ->get();

    // 表示する件数のみ取得するために、ページ番号($page)と最大表示件数($limit)から$offsetを計算
    $offset = ($page * $limit) - $limit;
    // ドキュメントに言われたように表示する部分のみ切り取った配列を用意
    $slice = array_slice($items->toArray(), $offset, $limit);

    return new LengthAwarePaginator($slice, count($items), $limit, $page);

これでも$columnsの値を参照する分には特に問題ありません。

ただ、return を受け取った先の Resource なんかでリレーションの情報が欲しくて$this->relatedUsers...なんていうふうに書くとその時点でエラーになってしまいます。

原因は$slice = array_slice($items->toArray(), $offset, $limit);の部分で、collection 型だった$items を配列に戻してしまっているので、この時点でリレーションの情報は消えてしまっていると。
(Collection から配列に変換する際、採用されるのは collection 内の attributes 部分だけ)

解決した流れ

公式ドキュメントでarray_sliceを推しているということは、LengthAwarePaginatorには collection を渡せないのか……?

と思いLengthAwarePaginatorの定義を見てみました。

    // LengthAwarePaginator.php

    class LengthAwarePaginator extends AbstractPaginator implements Arrayable, ArrayAccess, Countable, IteratorAggregate, JsonSerializable, Jsonable, LengthAwarePaginatorContract
    {

    // 略

        /**
         * Create a new paginator instance.
         *
         * @param  mixed  $items
         * @param  int  $total
         * @param  int  $perPage
         * @param  int|null  $currentPage
         * @param  array  $options (path, query, fragment, pageName)
         * @return void
         */
        public function __construct($items, $total, $perPage, $currentPage = null, array $options = [])
        {
            $this->options = $options;

            foreach ($options as $key => $value) {
                $this->{$key} = $value;
            }

            $this->total = $total;
            $this->perPage = $perPage;
            $this->lastPage = max((int) ceil($total / $perPage), 1);
            $this->path = $this->path !== '/' ? rtrim($this->path, '/') : $this->path;
            $this->currentPage = $this->setCurrentPage($currentPage, $this->pageName);
            $this->items = $items instanceof Collection ? $items : Collection::make($items);
        }

__constructを確認すると、任意の配列を受け取る第一引数は$itemsと定義されています。

この引数の初期化処理を見てみると、
$this->items = $items instanceof Collection ? $items : Collection::make($items);
「collection で来たらそのまま格納して、そうじゃなかったら collection 型にして返す」。
バリバリに collection 型を出す想定なので、引数に collection を渡しても大丈夫そうです。

であれば後は、問題の箇所だったarray_sliceの collection バージョンがあれば万事解決ということ。

コレクション 5.8 Laravel

slice() Slice メソッドは指定したインデックスからコレクションを切り分けます。

ググれば一発ですね。

解決後のコード

// app/Services/ItemsService.php
// 前略。$columnsには取得したいカラム名を格納

    $items = ItemMaster::query()
        ->select($columns)
        ->with('relatedUsers')
        ->get();

    // 表示する件数のみ取得するために、ページ番号($page)と最大表示件数($limit)から$offsetを計算
    $offset = ($page * $limit) - $limit;
    // *collection型を崩さずに切り取る*
    $slice = $items->slice($offset, $limit);

    // $sliceの中身はcollectionなので、この戻り値を受け取った先でもcollectionに対する操作が出来る
    return new LengthAwarePaginator($slice, count($items), $limit, $page);

これで無事に Resource 側でもリレーションを参照できるようになりました。

まとめ

Collection に対する理解が浅かった&各動作の戻り値を意識しなかったために起きた悲劇でした。初心者のハマるポイントは誰にも予想ができない。
(名称からして LengthAwarePaginator が slice 周りもサポートしてくれるものだと思ってたけど違うんですね)

もし他に良い実装方法があれば教えて頂けると嬉しいです:)
では。