ttskch / paginator-bundle
The most thin, simple and customizable paginator bundle for Symfony
Installs: 7 695
Dependents: 0
Suggesters: 0
Security: 0
Stars: 11
Watchers: 2
Forks: 4
Open Issues: 7
Type:symfony-bundle
Requires
- php: ^8.0
- symfony/config: ^5.0|^6.0|^7.0
- symfony/dependency-injection: ^5.0|^6.0|^7.0
- symfony/form: ^5.0|^6.0|^7.0
- symfony/http-foundation: ^5.0|^6.0|^7.0
- twig/twig: >=3.4.3
Requires (Dev)
- ext-pcov: *
- bamarni/composer-bin-plugin: *
- doctrine/orm: ^2.17
- phpspec/prophecy-phpunit: ^2.1
- phpunit/phpunit: ^9.6
- symfony/browser-kit: ^5.0|^6.0|^7.0
- symfony/framework-bundle: ^5.0|^6.0|^7.0
- symfony/http-kernel: ^5.0|^6.0|^7.0
- symfony/phpunit-bridge: ^7.0
- symfony/twig-bundle: ^5.0|^6.0|^7.0
- symfony/yaml: ^5.0|^6.0|^7.0
- dev-main
- 6.2.0
- 6.1.1
- 6.1.0
- 6.0.0
- 5.0.0
- 4.1.0
- 4.0.0
- 3.3.0
- 3.2.0
- 3.1.0
- 3.0.0
- 2.0.1
- 2.0.0
- 1.0.13
- 1.0.12
- 1.0.11
- 1.0.10
- 1.0.9
- 1.0.8
- 1.0.7
- 1.0.6
- 1.0.5
- 1.0.4
- 1.0.3
- 1.0.2
- 1.0.1
- 1.0.0
- dev-dependabot/composer/demo/twig/twig-3.11.2
- dev-dependabot/composer/demo/symfony/process-7.1.7
- dev-dependabot/composer/demo/symfony/http-foundation-7.1.7
- dev-dependabot/composer/demo/symfony/runtime-7.1.7
- dev-dependabot/composer/demo/twig/twig-3.14.0
This package is auto-updated.
Last update: 2024-11-06 21:15:53 UTC
README
The most thin, simple and customizable paginator bundle for Symfony.
Features
- So light weight
- Well typed (PHPStan level max)
- Depends on nothing other than Symfony and Twig
- But also easy to use with Doctrine ORM
- Of course can paginate everything
- Customizable twig-templated views
- Very easy-to-use sortable link feature
- Easy to use with your own search form
- Preset beautiful Bootstrap4/5 theme
Requirements
- PHP: ^8.0
- Symfony: ^5.0|^6.0|^7.0
Demo
👉 Live demo is here
You can also see a sample code on demo/
directory.
Installation
$ composer require ttskch/paginator-bundle
// config/bundles.php return [ // ... Ttskch\PaginatorBundle\TtskchPaginatorBundle::class => ['all' => true], ];
Basic usages
With Doctrine ORM
// FooController.php use Symfony\Component\HttpFoundation\Response; use Ttskch\PaginatorBundle\Counter\Doctrine\ORM\QueryBuilderCounter; use Ttskch\PaginatorBundle\Criteria\Criteria; use Ttskch\PaginatorBundle\Paginator; use Ttskch\PaginatorBundle\Slicer\Doctrine\ORM\QueryBuilderSlicer; /** * @param Paginator<\Traversable<array-key, Foo>, Criteria> $paginator */ public function index(FooRepository $fooRepository, Paginator $paginator): Response { $qb = $fooRepository->createQueryBuilder('f'); $paginator->initialize(new QueryBuilderSlicer($qb), new QueryBuilderCounter($qb), new Criteria('id')); return $this->render('index.html.twig', [ 'foos' => $paginator->getSlice(), ]); }
{# index.html.twig #} <table> <thead> <tr> <th>{{ ttskch_paginator_sortable('id', 'Id') }}</th> <th>{{ ttskch_paginator_sortable('name', 'Name') }}</th> <th>{{ ttskch_paginator_sortable('email', '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() }}
See src/Twig/TtskchPaginatorExtension.php to learn more about twig functions.
Sort with property of joined entity
Just do like as following.
{# index.html.twig #} {# ... #} <th>{{ ttskch_paginator_sortable('id', 'Id') }}</th> <th>{{ ttskch_paginator_sortable('name', 'Name') }}</th> <th>{{ ttskch_paginator_sortable('email', 'Email') }}</th> <th>{{ ttskch_paginator_sortable('bar.id', 'Bar') }}</th> <th>{{ ttskch_paginator_sortable('bar.baz.id', 'Baz') }}</th> {# ... #}
With array
// FooController.php use Symfony\Component\HttpFoundation\Response; use Ttskch\PaginatorBundle\Counter\ArrayCounter; use Ttskch\PaginatorBundle\Criteria\Criteria; use Ttskch\PaginatorBundle\Paginator; use Ttskch\PaginatorBundle\Slicer\ArraySlicer; /** * @param Paginator<array<array{id: int, name: string, email: string}>, Criteria> $paginator */ public function index(Paginator $paginator): Response { $array = [ ['id' => 1, 'name' => 'Tommy Yount', 'email' => 'tommy_yount@gmail.com'], ['id' => 2, 'name' => 'Hye Panter', 'email' => 'hye_panter@gmail.com'], ['id' => 3, 'name' => 'Vi Yohe', 'email' => 'vi_yohe@gmail.com'], ['id' => 4, 'name' => 'Keva Bandy', 'email' => 'keva_bandy@gmail.com'], ['id' => 5, 'name' => 'Hannelore Corning', 'email' => 'hannelore_corning@gmail.com'], ['id' => 6, 'name' => 'Delorse Whitcher', 'email' => 'delorse_whitcher@gmail.com'], ['id' => 7, 'name' => 'Katharyn Marrinan', 'email' => 'katharyn_marrinan@gmail.com'], ['id' => 8, 'name' => 'Jeannine Tope', 'email' => 'jeannine_tope@gmail.com'], ['id' => 9, 'name' => 'Jamila Braggs', 'email' => 'jamila_braggs@gmail.com'], ['id' => 10, 'name' => 'Eden Cunniff', 'email' => 'eden_cunniff@gmail.com'], // ... ['id' => 299, 'name' => 'Deshawn Kennedy', 'email' => 'deshawn_kennedy@gmail.com'], ['id' => 300, 'name' => 'Elenore Evens', 'email' => 'elenore_evens@gmail.com'], ]; $paginator->initialize( new ArraySlicer($array), new ArrayCounter($array), new Criteria('id'), ); return $this->render('index.html.twig', [ 'foos' => $paginator->getSlice(), ]); }
With something other data
Implement slicer and counter by yourself like as following.
use Symfony\Component\HttpFoundation\Response; use Ttskch\PaginatorBundle\Counter\CallbackCounter; use Ttskch\PaginatorBundle\Criteria\Criteria; use Ttskch\PaginatorBundle\Paginator; use Ttskch\PaginatorBundle\Slicer\CallbackSlicer; /** * @param Paginator<TypeOfYourOwnSlice>, Criteria> $paginator */ public function index(Paginator $paginator): Response { $yourOwnData = /* ... */; $paginator->initialize( new CallbackSlicer(function (Criteria $criteria) use ($yourOwnData) { /* ... */ return $yourOwnSlice; }), new CallbackCounter(function (Criteria $criteria) use ($yourOwnData) { /* ... */ return $totalItemsCount; }), new Criteria('default_sort_key'), ); return $this->render('index.html.twig', [ 'yourOwnSlice' => $paginator->getSlice(), ]); }
Configuring
$ bin/console config:dump-reference ttskch_paginator # Default configuration for extension with alias: "ttskch_paginator" ttskch_paginator: page: name: page range: 5 limit: name: limit default: 10 sort: key: name: sort direction: name: direction # "asc" or "desc" default: asc template: pager: '@TtskchPaginator/pager/default.html.twig' sortable: '@TtskchPaginator/sortable/default.html.twig'
Customizing views
Using preset Bootstrap4/5 theme
Just configure bundle like below.
# config/packages/ttskch_paginator.yaml ttskch_paginator: template: pager: '@TtskchPaginator/pager/bootstrap5.html.twig' # pager: '@TtskchPaginator/pager/bootstrap4.html.twig'
Using your own theme
Create your own templates and configure bundle like below.
# config/packages/ttskch_paginator.yaml ttskch_paginator: template: pager: 'your/own/pager.html.twig' sortable: 'your/own/sortable.html.twig'
Using with search form
// FooCriteria.php use Ttskch\PaginatorBundle\Criteria\AbstractCriteria; class FooCriteria extends AbstractCriteria { public ?string $query = null; public function __construct(string $sort) { parent::__construct($sort); } public function getFormTypeClass(): string { return FooSearchType::class; } }
// FooSearchType.php use Symfony\Component\Form\AbstractType; 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 AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void { $builder ->add('query', SearchType::class) ; } public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'data_class' => FooCriteria::class, // if your app depends on symfony/security-csrf adding below is recommended // 'csrf_protection' => false, ]); } public function getParent(): string { return CriteriaType::class; } }
// FooRepository.php use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\ORM\QueryBuilder; use Ttskch\PaginatorBundle\Counter\Doctrine\ORM\QueryBuilderCounter; use Ttskch\PaginatorBundle\Slicer\Doctrine\ORM\QueryBuilderSlicer; /** * @extends ServiceEntityRepository<Foo> */ class FooRepository extends ServiceEntityRepository { // ... /** * @return \Traversable<array-key, Foo> */ public function sliceByCriteria(FooCriteria $criteria): \Traversable { $qb = $this->createQueryBuilderFromCriteria($criteria); $slicer = new QueryBuilderSlicer($qb); return $slicer->slice($criteria); } public function countByCriteria(FooCriteria $criteria): int { $qb = $this->createQueryBuilderFromCriteria($criteria); $counter = new QueryBuilderCounter($qb); return $counter->count($criteria); } private function createQueryBuilderFromCriteria(FooCriteria $criteria): QueryBuilder { return $this->createQueryBuilder('f') ->orWhere('f.name like :query') ->orWhere('f.email like :query') ->setParameter('query', '%'.str_replace('%', '\%', $criteria->query).'%') ; } }
// FooController.php use Symfony\Component\HttpFoundation\Response; use Ttskch\PaginatorBundle\Paginator; /** * @param Paginator<\Traversable<array-key, Foo>, FooCriteria> $paginator */ public function index(FooRepository $fooRepository, Paginator $paginator): Response { $paginator->initialize( $fooRepository->sliceByCriteria(...), $fooRepository->countByCriteria(...), // or if PHP < 8.1 // \Closure::fromCallable([$fooRepository, 'sliceByCriteria']), // \Closure::fromCallable([$fooRepository, 'countByCriteria']), new FooCriteria('id'), ); return $this->render('index.html.twig', [ 'foos' => $paginator->getSlice(), 'form' => $paginator->getForm()->createView(), ]); }
{# index.html.twig #} {{ form(form, {action: path('foo_index'), method: 'get'}) }} <table> <thead> <tr> <th>{{ ttskch_paginator_sortable('id', 'Id') }}</th> <th>{{ ttskch_paginator_sortable('name', 'Name') }}</th> <th>{{ ttskch_paginator_sortable('email', '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() }}
Using with joined query
// FooRepository.php use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\ORM\QueryBuilder; use Ttskch\PaginatorBundle\Counter\Doctrine\ORM\QueryBuilderCounter; use Ttskch\PaginatorBundle\Slicer\Doctrine\ORM\QueryBuilderSlicer; /** * @extends ServiceEntityRepository<Foo> */ class FooRepository extends ServiceEntityRepository { // ... /** * @return \Traversable<array-key, Foo> */ public function sliceByCriteria(FooCriteria $criteria): \Traversable { $qb = $this->createQueryBuilderFromCriteria($criteria); $slicer = new QueryBuilderSlicer($qb, alreadyJoined: true); // **PAY ATTENTION HERE** return $slicer->slice($criteria); } public function countByCriteria(FooCriteria $criteria): int { $qb = $this->createQueryBuilderFromCriteria($criteria); $counter = new QueryBuilderCounter($qb); return $counter->count($criteria); } private function createQueryBuilderFromCriteria(FooCriteria $criteria): QueryBuilder { return $this->createQueryBuilder('f') ->leftJoin('f.bar', 'bar') ->leftJoin('bar.baz', 'baz') ->orWhere('f.name like :query') ->orWhere('f.email like :query') ->orWhere('bar.name like :query') ->orWhere('baz.name like :query') ->setParameter('query', '%'.str_replace('%', '\%', $criteria->query).'%') ; } }
// FooController.php use Symfony\Component\HttpFoundation\Response; use Ttskch\PaginatorBundle\Paginator; /** * @param Paginator<\Traversable<array-key, Foo>, FooCriteria> $paginator */ public function index(FooRepository $fooRepository, Paginator $paginator): Response { $paginator->initialize( $fooRepository->sliceByCriteria(...), $fooRepository->countByCriteria(...), // or if PHP < 8.1 // \Closure::fromCallable([$fooRepository, 'sliceByCriteria']), // \Closure::fromCallable([$fooRepository, 'countByCriteria']), new FooCriteria('f.id') ); return $this->render('index.html.twig', [ 'foos' => $paginator->getSlice(), 'form' => $paginator->getForm()->createView(), ]); }
{# index.html.twig #} {{ form(form, {action: path('foo_index'), method: 'get'}) }} <table> <thead> <tr> <th>{{ ttskch_paginator_sortable('f.id', 'Id') }}</th> <th>{{ ttskch_paginator_sortable('f.name', 'Name') }}</th> <th>{{ ttskch_paginator_sortable('f.email', 'Email') }}</th> <th>{{ ttskch_paginator_sortable('bar.name', 'Bar') }}</th> <th>{{ ttskch_paginator_sortable('baz.name', 'Baz') }}</th> </tr> </thead> <tbody> {% for foo in foos %} <tr> <td>{{ foo.id }}</td> <td>{{ foo.name }}</td> <td>{{ foo.email }}</td> <td>{{ foo.bar.name }}</td> <td>{{ foo.bar.baz.name }}</td> </tr> {% endfor %} </tbody> </table> {{ ttskch_paginator_pager() }}
Getting involved
$ composer install
# Develop...
$ composer tests