開発ブログ

株式会社Nextatのスタッフがお送りする技術コラムメインのブログ。

電話でのお問合わせ 075-744-6842 ([月]-[金] 10:00〜17:00)

  1. top >
  2. 開発ブログ >
  3. PHP >
  4. Laravel >
  5. 【Laravel】Paginatorのラッパークラス
no-image

【Laravel】Paginatorのラッパークラス

こんにちは、スズキです。

Laravelの機能にPaginatorというものがあります。
これがなかなか便利な機能でして、Laravelでページネーションを実装する際はほぼ必ずといっていいほど利用すると思います。
このPaginatorですが、先日開発作業をしていて、
「Paginatorのデータをコントローラ側で加工してからView側(blade側)に渡すようにしたい」
という状況が発生しました。
今回は、上記要望によりその際に作成したラッパークラスを紹介しようと思います。

なお、記事が若干長めのため、結論(最終結果)のみほしい場合は記事の最後を見てください。
 

手順1 ラッパークラスの骨組みの作成

 
では、ラッパークラスの骨組みから作成します。

まず、ラッパークラスの使い方からです。

本来の検索処理が以下のようになっていたとします。
    /**
     * 検索処理を行います。
     */
    public function search($perPage)
    {
        $searchResult = $this->searchSub()->paginate($perPage);
        return $searchResult;
    }

    /**
     * 検索処理を行います。(サブ処理)
     */
    private function searchSub()
    {
        $query = Table1::with([
            'table2',
            'table2.table3'
        ]);
        $query->select([
            'table1.*',
        ]);

        return $query;
    }
 
これをラッパークラスを挟むことによって以下のように修正します。
    /**
     * 検索処理を行います。
     */
    public function search($perPage)
    {
        $searchResult = $this->searchSub()->paginate($perPage);
        $convertSearchResult =  new ConvertPaginatorToSearchResultModel($searchResult);
        return $convertSearchResult;
    }

では、ラッパークラスの中身ですが、ラッパークラスの一番シンプルな構造は以下のようなものを想定しています。
/**
 * Paginator情報を検索結果モデル情報に変換します。
 */
class ConvertPaginatorToSearchResultModel {

    /**
     * 元情報
     * @var LengthAwarePaginator
     */
    private $__originPaginator;

    /**
     * 一覧情報
     * @var array
     */
    public $details = [];

    /**
     * コンストラクタ
     * @param LengthAwarePaginator $originPaginator
     */
    public function __construct(LengthAwarePaginator $originPaginator)
    {
        // 引数格納
        $this->__originPaginator = $originPaginator;

        // 初期化処理実行
        $this->init();
    }

    /**
     * 初期化処理を行います。
     */
    private function init()
    {
        $retDetails = [];
        foreach($this->__originPaginator as $detailInfo)
        {
            $table1 = $detailInfo;
            $table2 = $table1->table2;
            $table3 = $table2->table3;

            // 名称
            $name = $table1->name;
            // 距離
            $distance = $table2->distance . "m";
            // 価格
            $price = number_format($table3->price) . "円";

            // 戻り値に追加
            $retDetails[] = (object)[
                // 名称
                'name' => $name,
                // 距離
                'distance' => $distance,
                // 価格
                'price' => $price,
            ];
        }

        // 格納
        $this->details = $retDetails;
    }
}

上記の状態で完成としても一応は問題が無いのですが、このままでは使いにくいのでここから更に改良を加えていきます。
 

手順2 イテレータの実装


手順1で作成したラッパークラス(の骨組み)ですが、
このままでは以下のような利用方法になります。
        $perPage = 10;
        $searchResult = $this->search($perPage);
        foreach($searchResult->details as $detailInfo)
        {
            // 〜〜略〜〜
        }
これだと、ラッパークラスを挟む前と挟んだ後とで、使い方が変わってしまいます。
できれば、ラッパークラスを挟んだ後も本来の使い方と同様に以下のように利用できた方が自然ですよね?
        // 検索処理実行
        $perPage = 10;
        $searchResult = $this->search($perPage);
        foreach($searchResult as $detailInfo)
        {
            // 〜〜略〜〜
        }

というわけで、直接foreachで利用できるようにするためにイテレータを実装します。
イテレータの実装は、IteratorAggregateを継承し、getIteratorメソッドを追加することで簡単に実装することができます。
/**
 * Paginator情報を検索結果モデル情報に変換します。
 */
class ConvertPaginatorToSearchResultModel implements IteratorAggregate {

    // 〜〜略〜〜

    /**
     * イテレータを取得します。
     */
    function getIterator()
    {
        foreach ($this->details as $key => $val) {
            yield $key => $val;
        }
    }
}
 

手順3 count機能の実装


以下のようにcountメソッドで件数を得られるようにします。
なお、後述するようにcountメソッドを使わなくても、手順4によりtotalメソッドで件数を得られるようになるので、本対応はなくても問題ないです。
        // 検索処理実行
        $perPage = 10;
        $searchResult = $this->search($perPage);
        if(count($searchResult)) {
            foreach($searchResult as $detailInfo)
            {
                // 〜〜略〜〜
            }
        }
        else {
            // 〜〜略〜〜
        }

count機能は以下のように、
Countableを継承し、countメソッドを追加することで実装することができます。
/**
 * Paginator情報を検索結果モデル情報に変換します。
 */
class ConvertPaginatorToSearchResultModel implements IteratorAggregate, Countable {

    // 〜〜略〜〜

    /**
     * 件数を取得します。
     */
    public function count()
    {
        return count($this->details);
    }
}
 

手順4 Paginatorのメソッドを呼び出せるようにする


LaravelでPaginatorを利用する場合、
基本的にはPaginatorのtotalメソッド、firstItemメソッド、lastItemメソッド、renderメソッドは利用すると思います。
それぞれのメソッドを再定義しても問題ないですが、既にあるメソッドを再実装するのは面倒ですよね?

というわけでは、該当メソッドが見つからない場合は本来のPaginatorデータのメソッドを呼び出すように、
ラッパークラスに以下の処理を追加します。
    /**
     * 本メソッドは該当メソッドが見つからない場合に呼び出されます。
     */
    public function __call($method, $parameters)
    {
        // total等の関数は本来のPaginatorが保持する関数を利用する
        $result = call_user_func_array([$this->__originPaginator, $method], $parameters);
        return $result;
    }

また、firstItemメソッドは取得件数が0件の場合に1を返す不具合があるので、
ついでに、以下の記述を追加してfirstItemメソッドを上書きしてしまいましょう。
    /**
     * 件数の開始位置を取得します。
     */
    public function firstItem()
    {
        // firstItemメソッドは表示件数が0件の場合も「1」を返すため、この対策が必要
        $total = $this->__originPaginator->total();
        if($total == 0) {
            return 0;
        }
        return $this->__originPaginator->firstItem();
    }
 

最終結果


手順1〜手順4の内容の内、共通的な処理は基底クラスに持っていくことで最終的に以下のラッパークラスが出来上がります。

●基底クラス
use IteratorAggregate;
use Countable;
use \Illuminate\Contracts\Pagination\LengthAwarePaginator;

/**
 * 基底Paginatorラッパークラス
 */
class BaseConvertPaginator implements IteratorAggregate, Countable {

    /**
     * 元情報
     * @var LengthAwarePaginator
     */
    protected $__originPaginator;

    /**
     * 一覧情報
     * @var array
     */
    protected $details = [];

    /**
     * コンストラクタ
     * @param LengthAwarePaginator $originPaginator
     */
    public function __construct(LengthAwarePaginator $originPaginator)
    {
        // 引数格納
        $this->__originPaginator = $originPaginator;
    }

    /**
     * 本メソッドは該当メソッドが見つからない場合に呼び出されます。
     * @param $method
     * @param $parameters
     * @return mixed
     */
    public function __call($method, $parameters)
    {
        // total等の関数は本来のPaginatorが保持する関数を利用する
        $result = call_user_func_array([$this->__originPaginator, $method], $parameters);
        return $result;
    }

    /**
     * イテレータを取得します。
     * @return \Generator
     */
    function getIterator()
    {
        foreach ($this->details as $key => $val) {
            yield $key => $val;
        }
    }

    /**
     * 件数を取得します。
     * @return mixed
     */
    public function count()
    {
        return count($this->details);
    }

    /**
     * 件数の開始位置を取得します。
     */
    public function firstItem()
    {
        // firstItemメソッドは表示件数が0件の場合も「1」を返すため、この対策が必要
        $total = $this->__originPaginator->total();
        if($total == 0) {
            return 0;
        }
        return $this->__originPaginator->firstItem();
    }
}


●ラッパークラス
/**
 * Paginator情報を検索結果モデル情報に変換します。
 */
class ConvertPaginatorToSearchResultModel extends BaseConvertPaginator {

    /**
     * コンストラクタ
     * @param LengthAwarePaginator $originPaginator
     */
    public function __construct(LengthAwarePaginator $originPaginator)
    {
        // 親コンストラクタ処理実行
        parent::__construct($originPaginator);

        // 初期化処理実行
        $this->init();
    }

    /**
     * 初期化処理を行います。
     */
    private function init()
    {
        $retDetails = [];
        foreach($this->__originPaginator as $detailInfo)
        {
            $table1 = $detailInfo;
            $table2 = $table1->table2;
            $table3 = $table2->table3;

            // 名称
            $name = $table1->name;
            // 距離
            $distance = $table2->distance . "m";
            // 価格
            $price = number_format($table3->price) . "円";

            // 戻り値に追加
            $retDetails[] = (object)[
                // 名称
                'name' => $name,
                // 距離
                'distance' => $distance,
                // 価格
                'price' => $price,
            ];
        }

        // 格納
        $this->details = $retDetails;
    }
}


上記対応により、blade側では以下のように通常のPaginatorと同じように利用することができます。
    {{ $searchResult->total() }}件中 {{ $searchResult->firstItem() }}~{{ $searchResult->lastItem() }}件 表示
    <br>
    {!! $searchResult->appends(\Input::all())->render() !!}
    <br>
    @foreach($searchResult as $detailInfo)
        <p>
            名称:{{ $detailInfo->name }}
            <br>
            距離:{{ $detailInfo->distance }}
            <br>
            価格:{{ $detailInfo->price }}
        </p>
    @endforeach


長くなりましたが、以上で本記事は終了になります。
個人的にはなかなか使いやすかったので、みなさんも機会があれば利用してみてください。
TOPに戻る