ttskch/paginator-bundle

为 Symfony 服务的最轻量、最简单且可定制的分页器包

安装数: 6,472

依赖者: 0

建议者: 0

安全: 0

星标: 11

关注者: 2

分支: 4

开放问题: 4

类型:symfony-bundle

6.2.0 2024-08-28 02:30 UTC

README

codecov Latest Stable Version Total Downloads

为 Symfony 提供最轻量、最简单且可定制的分页器包。

功能

  • 非常轻量
  • 严格类型化(PHPStan 级别最大)
  • 除了 Symfony 和 Twig 之外,不依赖任何其他内容
  • 但也很容易与 Doctrine ORM 一起使用
  • 当然可以分页任何内容
  • 可定制的 twig-templated 视图
  • 非常易用的 可排序链接 功能
  • 易于与 您自己的搜索表单 一起使用
  • 预设美观的 Bootstrap4/5 主题

需求

  • PHP: ^8.0
  • Symfony: ^5.0|^6.0|^7.0

演示

👉 在线演示在这里

您还可以在 demo/ 目录 中查看示例代码。

安装

$ composer require ttskch/paginator-bundle
// config/bundles.php

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

基本用法

与 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() }}

查看 src/Twig/TtskchPaginatorExtension.php 了解有关 twig 函数的更多信息。

按连接实体的属性排序

只需像以下这样做。

{# 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>

{# ... #}

使用数组

// 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(),
    ]);
}

使用其他类型的数据

按如下方式自行实现切片器和计数器。

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(),
    ]);
}

配置

$ 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'

自定义视图

使用预设的 Bootstrap4/5 主题

只需按以下方式配置包。

# config/packages/ttskch_paginator.yaml

ttskch_paginator:
  template:
    pager: '@TtskchPaginator/pager/bootstrap5.html.twig'
#   pager: '@TtskchPaginator/pager/bootstrap4.html.twig'

使用您自己的主题

创建自己的模板并按以下方式配置包。

# config/packages/ttskch_paginator.yaml

ttskch_paginator:
  template:
    pager: 'your/own/pager.html.twig'
    sortable: 'your/own/sortable.html.twig'

与搜索表单一起使用

// 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() }}

与连接查询一起使用

// 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() }}

参与其中

$ composer install

# Develop...

$ composer tests