ttskch/paginator-bundle

The most thin, simple and customizable paginator bundle for Symfony

Installs: 7 964

Dependents: 0

Suggesters: 0

Security: 0

Stars: 12

Watchers: 2

Forks: 4

Open Issues: 8

Type:symfony-bundle

6.2.0 2024-08-28 02:30 UTC

README

codecov Latest Stable Version Total Downloads

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