🎻

Symfonyでページネーションを実装するならttskch/pagerfanta-bundleがおすすめ!

2020/02/01に公開

2020/07/30 追記

Pagerfantaのオリジナルのリポジトリがメンテナンス終了 したこと(ちゃんと BabDev/Pagerfanta に引き継がれていますが)や、そもそもPagerfantaに依存する意味があまりないと考えるに至ったため、ttskch/paginator-bundle という新しいバンドルを作成しました。

以下の記事でコンセプトや使い方について詳しく解説していますので、よろしければこちらもご参照ください!

[Symfony] シンプルでカスタマイズしやすい最強のページネーションバンドル


こんにちは、たつきちです。

Symfonyでページネーションを実装するためのバンドルといえば knplabs/knp-paginator-bundle が有名ですね。

僕も昔はよく使っていたんですが、機能てんこ盛りな割りに微妙に痒いところに手が届かない(確か当時はそんな印象でした)ので、もっと軽量で柔軟な実装を使いたいと思い、どこかのタイミングから whiteoctober/Pagerfanta を積極的に使うようになりました。

初めのうちはSymfonyとの繋ぎ込み部分は自分で一所懸命書いてたんですが、よくよく調べてみると公式が WhiteOctoberPagerfantaBundle というバンドルを公開してくれていることに気付き、早速使ってみました。

しかし、

  • ソート機能を提供するユーティリティがない
  • 検索機能を提供するユーティリティがない
  • Bootstrap4ベースのテンプレートがなかった(今はあるみたいです)

これらの原因から結局自分のアプリ側で結構な量のコードを書かなければならず、バンドルとしてはあまり満足できませんでした。

そこでいっそ自作してしまえと作ったのが以下のバンドルです。

そうです、ステマです(笑)

このバンドルを作ったのはもう2年ほど前ですが、未だに自分のプロジェクトで便利に使いまくっている現役バリバリの神バンドルなので、ここで使い方などについて解説してみようかなと思います。

よさそうじゃんと思っていただけた方はぜひ使ってみてください!

公式のWhiteOctoberPagerfantaBundleと比べていいところ

  • 軽量!(故に拡張しやすい!)
  • 設定ファイルで細かく設定できる!
  • 簡単に拡張できるTwigベースのテンプレート!
  • ソート機能の実装が簡単!
  • 検索機能の実装が簡単!
  • (Bootstrap4ベースのテンプレートをプリセット!)

動作イメージ

こんな感じの、ソート・検索に対応したページネーション&Bootstrap4ベースの美しいページャーが簡単に作れます。

システム要件

  • PHP ^7.1.3
  • Symfony ^4.0

Symfony 3以下の環境は動作保証外なのでご注意ください。

インストール方法

composer require して、

$ composer require ttskch/pagerfanta-bundle

config/bundles.php に登録すればOKです。

// config/bundles.php

return [
    // ...
    Ttskch\PagerfantaBundle\TtskchPagerfantaBundle::class => ['all' => true],
];

具体的な使い方

先ほどお見せした動作イメージのうち、検索機能以外の部分だけなら、以下のような簡潔なコードで実装できます。

# config/packages/ttskch_pagerfanta.yaml

ttskch_pagerfanta:
    template:
        pager: '@TtskchPagerfanta/pager/bootstrap4.html.twig'

まずページャーのテンプレートをデフォルトのものからBootstrap4ベースのものに変更します。

// FooController.php

public function index(FooRepository $fooRepository, Ttskch\PagerfantaBundle\Context $context)
{
    $context->initialize('id');

    $queryBuilder = $fooRepository
        ->createQueryBuilder('f')
        ->orderBy(sprintf('f.%s', $context->criteria->sort), $context->criteria->direction)
    ;

    $adapter = new DoctrineORMAdapter($queryBuilder);
    $pagerfanta = new Pagerfanta($adapter);
    $pagerfanta
        ->setMaxPerPage($context->criteria->limit)
        ->setCurrentPage($context->criteria->page)
    ;

    return $this->render('index.html.twig', [
        'pagerfanta' => $pagerfanta,
    ]);
}

ページネーションの生成自体は、Pagerfantaを素直に使う感じです。

コントローラーに引数で注入している Ttskch\PagerfantaBundle\Context クラスのインスタンスが、ページネーションの現在の状態に関する各種情報を保持しており、その中でも criteria というプロパティにソート条件、表示件数、ページ番号が入っています。

最初の $context->initialize('id'); により、デフォルトでどのプロパティでソートするかを指定しています。

{# index.html.twig #}

<table>
    <thead>
    <tr>
        <th>{{ ttskch_pagerfanta_sortable('id') }}</th>
        <th>{{ ttskch_pagerfanta_sortable('name') }}</th>
        <th>{{ ttskch_pagerfanta_sortable('email') }}</th>
    </tr>
    </thead>
    <tbody>
    {% for item in pagerfanta.getCurrentPageResults() %}
        <tr>
            <td>{{ item.id }}</td>
            <td>{{ item.name }}</td>
            <td>{{ item.email }}</td>
        </tr>
    {% endfor %}
    </tbody>
</table>

{{ ttskch_pagerfanta_pager(pagerfanta) }}

Viewはこんな感じです。

ttskch_pagerfanta_sortable() というTwig拡張関数を呼ぶだけで、ソートのためのリンクをレンダリングできます。便利!

ttskch_pagerfanta_pager() というTwig拡張関数にページネーションを渡すだけで、美しいページャーをレンダリングできます。便利!

検索機能を実装する

検索機能を実装する場合、特別なことは必要なく、

  1. URLクエリパラメーターで検索条件を送る
  2. コントローラーでページネーションを作るときに検索条件で絞り込む

ということをすればOKなのですが、TtskchPagerfantaBundleなら、 Criteria クラスを拡張することで、これを簡単かつ明示的に実装することができます。

検索条件を保持させるCriteriaエンティティを作る

まず、 Ttskch\PagerfantaBundle\Entity\Criteria クラスを継承して、独自の検索条件を保持するためのCriteriaを作ります。

// FooCriteria.php

use Ttskch\PagerfantaBundle\Entity\Criteria;

class FooCriteria extends Criteria
{
    public $query;
}

検索フォームのFormTypeを作る

次に、 Ttskch\PagerfantaBundle\Form\CriteriaType クラスを継承したFormTypeを作り、先ほど作ったCriteriaを data_class に指定します。

parent::buildForm($builder, $options); を呼び出すのを忘れずに!

// FooSearchType.php

use Symfony\Component\Form\Extension\Core\Type\SearchType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Ttskch\PagerfantaBundle\Form\CriteriaType;

class FooSearchType extends CriteriaType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        parent::buildForm($builder, $options);

        $builder
            ->add('query', SearchType::class)
        ;
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => FooCriteria::class,
        ]);
    }
}

Criteriaで検索するためのメソッドをリポジトリに追加

クエリがある程度複雑になるため、先ほどまでのようにコントローラーから直接 createQueryBuilder するのではなく、リポジトリクラスに専用のメソッドを生やしてあげましょう。

// FooRepository.php

public function createQueryBuilderFromCriteria(FooCriteria $criteria)
{
    return $this->createQueryBuilder('f')
        ->where('f.name like :query')
        ->orWhere('f.email like :query')
        ->setParameter('query', sprintf('%%%s%%', str_replace('%', '\%', $criteria->query)))
        ->orderBy(sprintf('f.%s', $criteria->sort), $criteria->direction)
    ;
}

あとは普通に使うだけ

ここまで準備ができたらあとは今までどおり普通に使う感じです。

  • 最初の $context->initialize() に、ソート条件だけでなくCriteriaのクラス名とFormTypeのクラス名を渡す
  • $context->form->createView() をViewに渡す

のがポイントです。

// FooController.php

public function index(FooRepository $fooRepository, Ttskch\PagerfantaBundle\Context $context)
{
    $context->initialize('id', FooCriteria::class, FooSearchType::class);

    $queryBuilder = $fooRepository->createQueryBuilderFromCriteria($context->criteria);

    $adapter = new DoctrineORMAdapter($queryBuilder);
    $pagerfanta = new Pagerfanta($adapter);
    $pagerfanta
        ->setMaxPerPage($context->criteria->limit)
        ->setCurrentPage($context->criteria->page)
    ;

    return $this->render('index.html.twig', [
        'form' => $context->form->createView(),
        'pagerfanta' => $pagerfanta,
    ]);
}

Viewの実装は何も変える必要はありません。

にコントローラーから受け取った form を適当な箇所にレンダリングするだけでOKです。

{# index.html.twig #}

{{ form(form, {action: path('index'), method: 'get'}) }}

<table>
    <thead>
    <tr>
        <th>{{ ttskch_pagerfanta_sortable('id') }}</th>
        <th>{{ ttskch_pagerfanta_sortable('name') }}</th>
        <th>{{ ttskch_pagerfanta_sortable('email') }}</th>
    </tr>
    </thead>
    <tbody>
    {% for item in pagerfanta.getCurrentPageResults() %}
        <tr>
            <td>{{ item.id }}</td>
            <td>{{ item.name }}</td>
            <td>{{ item.email }}</td>
        </tr>
    {% endfor %}
    </tbody>
</table>

{{ ttskch_pagerfanta_pager(pagerfanta) }}

簡単!便利!

まとめ

  • ttskch/pagerfanta-bundle は我ながらよくできたいいバンドル
  • ぜひ使ってみてね
  • よかったらスターしてね
GitHubで編集を提案

Discussion