[Symfony] シンプルでカスタマイズしやすい最強のページネーションバンドル
TtskchPaginatorBundle
シンプルでカスタマイズしやすい最強のページネーションバンドルを作りました。
ttskch/TtskchPaginatorBundle: The most thin and simple paginator bundle for Symfony
こんな感じの美しいページネーションが簡単に実装できます。
特徴
- 超軽量
- Symfony以外に一切依存なし
- ページャー部分のHTMLなどはtwigで簡単にカスタマイズ可能
- 見出し部分を簡単にソートのためのリンクにできる
- 検索フォームとの連動も簡単
- ページャー部分のHTMLのBootstrap4テーマをプリセット
動作要件
- PHP >=7.1.3
- Symfony ^4.0|^5.0
デモ
https://ttskchpaginatorbundle.herokuapp.com/
こちらで実際に動いているサンプルを触れます。
使い方
インストール
普通にcomposerでインストールして、 bundles.php
に追加してください。
$ composer require ttskch/paginator-bundle
// config/bundles.php
return [
// ...
Ttskch\PaginatorBundle\TtskchPaginatorBundle::class => ['all' => true],
];
Doctrine ORMとあわせて使う
TtskchPaginatorBundle自体はDoctrine ORMにロックインはしていませんが、Doctrine ORMとあわせて使うためのユーティリティをデフォルトで提供しているので、それを使えばとても少ない変更で簡単にページネーションを導入できます。
Before
// FooController.php
public function index(FooRepository $fooRepository)
{
return $this->render('index.html.twig', [
'foos' => $fooRepository->findAll(),
]);
}
{# index.html.twig #}
<table>
<thead>
<tr>
<th>Id</th>
<th>Name</th>
<th>Email</th>
</tr>
</thead>
<tbody>
{% for foo in foos %}
<tr>
<td>{{ foo.id }}</td>
<td>{{ foo.name }}</td>
<td>{{ foo.email }}</td>
</tr>
{% endfor %}
</tbody>
</table>
After
// FooController.php
+ use Ttskch\PaginatorBundle\Context;
+ use Ttskch\PaginatorBundle\Doctrine\Counter;
+ use Ttskch\PaginatorBundle\Doctrine\Slicer;
+
- public function index(FooRepository $fooRepository)
+ public function index(FooRepository $fooRepository, Context $context)
{
+ $qb = $fooRepository->createQueryBuilder('f');
+ $context->initialize('id', new Slicer($qb), new Counter($qb));
+
return $this->render('index.html.twig', [
- 'foos' => $fooRepository->findAll(),
+ 'foos' => $context->slice,
]);
}
{# index.html.twig #}
<table>
<thead>
<tr>
- <th>Id</th>
- <th>Name</th>
- <th>Email</th>
+ <th>{{ ttskch_paginator_sortable('id') }}</th>
+ <th>{{ ttskch_paginator_sortable('name') }}</th>
+ <th>{{ ttskch_paginator_sortable('email') }}</th>
</tr>
</thead>
<tbody>
{% for foo in foos %}
<tr>
<td>{{ foo.id }}</td>
<td>{{ foo.name }}</td>
<td>{{ foo.email }}</td>
</tr>
{% endfor %}
</tbody>
</table>
+
+ {{ ttskch_paginator_pager() }}
こんな感じです。
$context->initialize('id', new Slicer($qb), new Counter($qb));
で、 $context
が内部的に handleRequest()
を実行して、URLのクエリパラメータをもとにページネーションを実行してくれます。
new Slicer($qb)
と new Counter($qb)
はそれぞれ、
- 現在のページの
Foo
エンティティのコレクションを作成するためのコールバック -
Foo
エンティティの総数を算出するためのコールバック
です。これらがTtskchPaginatorBundleが用意してくれているユーティリティです。
ビュー側では、
-
ttskch_paginator_sortable()
関数でソートのためのリンクを出力 -
ttskch_paginator_pager()
関数でページャーを出力
しています。
ビューのテンプレートを変更する
ビューのテンプレートは設定で自由に変更できます。
# config/packages/ttskch_paginator.yaml
ttskch_paginator:
template:
pager: 'your/own/pager.html.twig'
sortable: 'your/own/sortable.html.twig'
プリセットされているBootstrap4スタイルのページャーを使いたいだけなら、以下のようにすればOKです。
# config/packages/ttskch_paginator.yaml
ttskch_paginator:
template:
pager: '@TtskchPaginator/pager/bootstrap4.html.twig'
検索フォームとあわせて使う
検索フォームとあわせて使う場合は、リポジトリクラスにQueryBuilderの組み立てるを書きます。
このとき、送信された検索条件やページネーションの情報をいちいちリクエストオブジェクトから取り出したりする必要はありません。TtskchPaginatorBundleが Criteria
というリクエスト条件を表すクラスを持っていて、コントローラで $context->initialize()
したときに適切に Criteria
に値がセットされます。
デフォルトの Criteria
はページ番号や1ページあたりの表示件数など、ページネーション情報しか持たないため、これを継承して独自の FooCriteria
を作るところから始めます。
とりあえず文字列で検索するための query
というプロパティを追加しました。
// FooCriteria.php
use Ttskch\PaginatorBundle\Entity\Criteria;
class FooCriteria extends Criteria
{
public $query;
}
続いて、この FooCriteria
に対応するFormTypeを作ります。TtskchPaginatorBundleが提供しているデフォルトの CriteriaType
を継承することで、ページネーション情報に関する処理を気にする必要がなくなります。
// FooSearchType.php
use Symfony\Component\Form\Extension\Core\Type\SearchType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Ttskch\PaginatorBundle\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,
// symfony/security-csrfがインストールされている環境では以下が必要
// 'csrf_protection' => false,
]);
}
}
次に、リポジトリクラスに FooCriteria
をもとにQueryBuilderを組み立てるメソッドを作ります。
// FooRepository.php
private function createQueryBuilderFromCriteria(FooCriteria $criteria)
{
return $this->createQueryBuilder('f')
->orWhere('f.name like :query')
->orWhere('f.email like :query')
->setParameter('query', '%'.str_replace('%', '\%', $criteria->query).'%')
;
}
$criteria->query
をLIKE句に食わせて検索しているだけですね。
このQueryBuilderを組み立てる処理だけはアプリ固有なので自分で書く必要がどうしてもありますが、これをもとに Slicer
Counter
を作る処理にはTtskchPaginatorBundleが提供するユーティリティを活用できます。
// FooRepository.php
use Ttskch\PaginatorBundle\Doctrine\Counter;
use Ttskch\PaginatorBundle\Doctrine\Slicer;
// ...
public function sliceByCriteria(FooCriteria $criteria)
{
$qb = $this->createQueryBuilderFromCriteria($criteria);
$slicer = new Slicer($qb);
return $slicer($criteria);
}
public function countByCriteria(FooCriteria $criteria)
{
$qb = $this->createQueryBuilderFromCriteria($criteria);
$counter = new Counter($qb);
return $counter($criteria);
}
private function createQueryBuilderFromCriteria(FooCriteria $criteria)
{
return $this->createQueryBuilder('f')
->orWhere('f.name like :query')
->orWhere('f.email like :query')
->setParameter('query', '%'.str_replace('%', '\%', $criteria->query).'%')
;
}
こんな感じで、検索条件適用済みのQueryBuilderを渡して Slicer
Counter
を作り、それらの処理を FooCriteria
を渡して実行するようにすればOKです。( Slicer
Counter
はinvokableです)
最後にコントローラの実装です。
$context->initialize()
の第2、第3引数で先ほど作ったリポジトリクラスのメソッドを渡します。
また、Criteria/FormTypeを独自に拡張した場合は、第4、第5引数でそのクラス名を渡します。
そうすることで、 $context->form
で handleRequest()
済みのFormオブジェクトが取得できます。
// FooController.php
public function index(FooRepository $fooRepository, Context $context)
{
$context->initialize(
'id',
[$fooRepository, 'sliceByCriteria'],
[$fooRepository, 'countByCriteria'],
FooCriteria::class,
FooSearchType::class
);
return $this->render('index.html.twig', [
'foos' => $context->slice,
'form' => $context->form->createView(),
]);
}
あとはビューにフォームを設置すれば完了です。
{# index.html.twig #}
+ {{ form(form, {action: path('foo_index'), method: 'get'}) }}
+
<table>
<thead>
<tr>
<th>{{ ttskch_paginator_sortable('id') }}</th>
<th>{{ ttskch_paginator_sortable('name') }}</th>
<th>{{ ttskch_paginator_sortable('email') }}</th>
</tr>
</thead>
<tbody>
{% for foo in foos %}
<tr>
<td>{{ foo.id }}</td>
<td>{{ foo.name }}</td>
<td>{{ foo.email }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{{ ttskch_paginator_pager() }}
まとめ
- バンドル固有のコードが少なく済んで、いざとなったらいつでも捨てて別の実装に移行できる
- だけどやりたいことは全部できる
というのを意識して作りました。
いつでも捨てられるというのが特に大事だと思っていて、実際に「バンドルを使わずに自分で実装したとしてもどうせ書かないといけないようなもの」だけしか提供していません。
もし普段からページネーションを自力で書いているという方がいたら、試しに使ってみていただけると嬉しいです💪
ttskch/TtskchPaginatorBundle: The most thin and simple paginator bundle for Symfony
Discussion