Skip to content

Commit b00fc8d

Browse files
author
Sander
authored
Merge pull request #79 from sumocoders/paginator
Paginator
2 parents d5145e2 + e28fd8c commit b00fc8d

File tree

9 files changed

+412
-94
lines changed

9 files changed

+412
-94
lines changed

config/services.yml

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,13 @@ services:
8989
tags:
9090
- { name: form.type, alias: sumoFile }
9191

92-
pagerfanta.view.sumocoders:
93-
class: SumoCoders\FrameworkCoreBundle\Twig\TwitterBootstrap3View
94-
public: false
95-
tags: [{ name: pagerfanta.view, alias: sumocoders }]
92+
twig.paginator_extension:
93+
class: SumoCoders\FrameworkCoreBundle\Twig\PaginatorExtension
94+
tags: [ 'twig.extension' ]
95+
96+
twig.paginator_runtime:
97+
class: SumoCoders\FrameworkCoreBundle\Twig\PaginatorRuntime
98+
tags: [ 'twig.runtime' ]
9699

97100
SumoCoders\FrameworkCoreBundle\Service\BreadcrumbTrail:
98101
SumoCoders\FrameworkCoreBundle\EventListener\BreadcrumbListener:

docs/development/mails.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Simply extend your email template from the `@SumoCodersFrameworkCore/Mail/base.h
66

77
The base template will place your content in a table-based layout, load and inline styles and return the end result.
88

9-
To send the mail, you can use the default Symfony package: `symfony/mailer`. See the example below.
9+
To send the mail, you can use the default Symfony package: `symfony/mailer`. See the example below.
1010

1111
## Basic example
1212

docs/development/pagination.md

Lines changed: 51 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,77 @@
11
# Using pagination
22

3-
Pagination is a nice way to handle large amounts of data over multiple pages.
3+
Pagination is a nice way to handle large amounts of data over multiple pages. The core bundle has a Paginator class (similar to Pagerfanta), that does most of the heavy lifting.
44

5-
## Controller
5+
## Usage
6+
Define the Paginator object in your repository, where you pass the QueryBuilder object straight to it.
7+
### Repository
68

7-
In the controller the following code should be used. If necessary a custom adapter can be used.
9+
```php
10+
use SumoCoders\FrameworkCoreBundle\Pagination\Paginator;
11+
12+
public function getPaginatedItems(): Paginator
13+
{
14+
$queryBuilder = $this->createQueryBuilder('i')
15+
->where('i.name LIKE :term')
16+
->setParameter('term', 'foo')
17+
->orderBy('i.name');
818

19+
return new Paginator($queryBuilder);
20+
}
921
```
22+
## Controller
23+
24+
In your controller, use the `paginate` method on it to set the correct page. You can also extend this with sorting GET parameters that you pass to your method in the repository. Since the pagination works on a QueryBuilder object, al sorting must be done with orderBy's.
25+
26+
```php
1027
<?php
1128

1229
namespace SumoCoders\FrameworkCoreBundle\Controller;
1330

14-
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
31+
use App\Repository\ItemRepository;
32+
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
1533
use Symfony\Component\HttpFoundation\Request;
16-
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
17-
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
18-
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
19-
use Pagerfanta\Pagerfanta;
20-
use Pagerfanta\Adapter\DoctrineORMAdapter;
21-
use Pagerfanta\Exception\NotValidCurrentPageException;
22-
23-
class ExampleController extends Controller
34+
use Symfony\Component\HttpFoundation\Response;
35+
use Symfony\Component\Routing\Annotation\Route;
36+
37+
final class ItemController extends AbstractController
2438
{
25-
/**
26-
* @Route("/example/{page}", requirements={"page" = "\d+"}, defaults={"page" = "1"})
27-
* @Template()
28-
*
29-
* @param int $page
30-
*/
31-
public function exampleAction(int $page)
32-
{
33-
$repository = $this->getDoctrine()->getRepository('AcmeExampleBundle:Example');
34-
35-
// returns \Doctrine\ORM\Query object
36-
$query = $repository->getExampleQuery();
37-
38-
$pagerfanta = new Pagerfanta(new DoctrineORMAdapter($query));
39-
$pagerfanta->setMaxPerPage(45);
40-
41-
try {
42-
$pagerfanta->setCurrentPage($page);
43-
} catch(NotValidCurrentPageException $e) {
44-
throw new NotFoundHttpException();
45-
}
46-
47-
return ['my_pager' => $pagerfanta];
48-
}
39+
/**
40+
* @Route("/items", name="item_index")
41+
*/
42+
public function __invoke(
43+
Request $request,
44+
ItemRepository $itemRepository
45+
): Response {
46+
$paginatedItems = $itemRepository->getPaginatedItems();
47+
48+
$paginatedItems->paginate($request->query->getInt('page', 1));
49+
50+
return $this->render('items/index.html.twig', [
51+
'items' => $paginatedItems,
52+
]);
53+
}
4954
}
5055
```
5156

52-
## View
57+
## Template
5358

54-
In the view the following code can be used
59+
In your template, you have access to a Twig extension called `pagination` to render a clean pagination widget.
5560

56-
```
57-
{% if pager.currentPageResults is not empty %}
58-
{% for item in my_pager.currentPageResults %}
61+
The paginated object, in this case `items` is an iterator, so you can count it/loop over it to get the results of the query.
62+
63+
```twig
64+
{% if items|length > 0 %}
65+
{% for item in items %}
5966
<ul>
6067
<li>{{ item.id }}</li>
6168
</ul>
6269
{% endfor %}
6370
{% endif %}
6471
65-
{% if pager.haveToPaginate %}
66-
<div class="pagerfanta">
67-
{{ pagerfanta(my_pager, 'sumocoders') }}
72+
{% if items.hasToPaginate %}
73+
<div class="d-flex justify-content-center">
74+
{{ pagination(items) }}
6875
</div>
6976
{% endif %}
7077
```

src/Pagination/Paginator.php

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
<?php
2+
3+
namespace SumoCoders\FrameworkCoreBundle\Pagination;
4+
5+
use Doctrine\ORM\QueryBuilder as DoctrineQueryBuilder;
6+
use Doctrine\ORM\Tools\Pagination\CountWalker;
7+
use Doctrine\ORM\Tools\Pagination\Paginator as DoctrinePaginator;
8+
use ArrayIterator;
9+
use IteratorAggregate;
10+
use Traversable;
11+
use Countable;
12+
use Iterator;
13+
14+
class Paginator implements Countable, IteratorAggregate
15+
{
16+
public const PAGE_SIZE = 30;
17+
18+
private DoctrineQueryBuilder $queryBuilder;
19+
private int $currentPage;
20+
private int $startPage;
21+
private int $endPage;
22+
private int $pageSize;
23+
private Traversable $results;
24+
private int $numResults;
25+
26+
public function __construct(DoctrineQueryBuilder $queryBuilder, int $pageSize = self::PAGE_SIZE)
27+
{
28+
$this->queryBuilder = $queryBuilder;
29+
$this->pageSize = $pageSize;
30+
}
31+
32+
public function paginate(int $page = 1): self
33+
{
34+
$this->currentPage = max(1, $page);
35+
$firstResult = ($this->currentPage - 1) * $this->pageSize;
36+
37+
$query = $this->queryBuilder
38+
->setFirstResult($firstResult)
39+
->setMaxResults($this->pageSize)
40+
->getQuery();
41+
42+
if (0 === count($this->queryBuilder->getDQLPart('join'))) {
43+
$query->setHint(CountWalker::HINT_DISTINCT, false);
44+
}
45+
46+
$paginator = new DoctrinePaginator($query, true);
47+
48+
$useOutputWalkers = count($this->queryBuilder->getDQLPart('having') ?: []) > 0;
49+
$paginator->setUseOutputWalkers($useOutputWalkers);
50+
51+
$this->results = $paginator->getIterator();
52+
$this->numResults = $paginator->count();
53+
54+
return $this;
55+
}
56+
57+
public function getCurrentPage(): int
58+
{
59+
return $this->currentPage;
60+
}
61+
62+
public function getLastPage(): int
63+
{
64+
return (int) ceil($this->numResults / $this->pageSize);
65+
}
66+
67+
public function getPageSize(): int
68+
{
69+
return $this->pageSize;
70+
}
71+
72+
public function hasPreviousPage(): bool
73+
{
74+
return $this->currentPage > 1;
75+
}
76+
77+
public function getPreviousPage(): int
78+
{
79+
return max(1, $this->currentPage - 1);
80+
}
81+
82+
public function getStartPage(): int
83+
{
84+
return $this->startPage;
85+
}
86+
87+
public function getEndPage(): int
88+
{
89+
return $this->endPage;
90+
}
91+
92+
public function hasNextPage(): bool
93+
{
94+
return $this->currentPage < $this->getLastPage();
95+
}
96+
97+
public function getNextPage(): int
98+
{
99+
return min($this->getLastPage(), $this->currentPage + 1);
100+
}
101+
102+
public function hasToPaginate(): bool
103+
{
104+
return $this->numResults > $this->pageSize;
105+
}
106+
107+
public function getNumResults(): int
108+
{
109+
return $this->numResults;
110+
}
111+
112+
public function getNumberOfPages(): int
113+
{
114+
$numberOfPages = $this->calculateNumberOfPages();
115+
116+
if (0 === $numberOfPages) {
117+
return 1;
118+
}
119+
120+
return $numberOfPages;
121+
}
122+
123+
private function calculateNumberOfPages(): int
124+
{
125+
return (int) ceil($this->getNumResults() / self::PAGE_SIZE);
126+
}
127+
128+
public function getResults(): Traversable
129+
{
130+
return $this->results;
131+
}
132+
133+
public function count(): int
134+
{
135+
return count($this->getResults());
136+
}
137+
138+
public function getIterator(): Traversable
139+
{
140+
$results = $this->getResults();
141+
142+
if ($results instanceof Iterator) {
143+
return $results;
144+
}
145+
146+
if ($results instanceof IteratorAggregate) {
147+
return $results->getIterator();
148+
}
149+
150+
return new ArrayIterator($results);
151+
}
152+
153+
public function calculateStartAndEndPage(): void
154+
{
155+
$startPage = $this->currentPage - 3;
156+
$endPage = $this->currentPage + 3;
157+
158+
if ($this->startPageUnderflow($startPage)) {
159+
$endPage = $this->calculateEndPageForStartPageUnderflow($startPage, $endPage);
160+
$startPage = 1;
161+
}
162+
163+
if ($this->endPageOverflow($endPage)) {
164+
$startPage = $this->calculateStartPageForEndPageOverflow($startPage, $endPage);
165+
$endPage = $this->getNumberOfPages();
166+
}
167+
168+
$this->startPage = $startPage;
169+
$this->endPage = $endPage;
170+
}
171+
172+
private function startPageUnderflow($startPage): bool
173+
{
174+
return $startPage < 1;
175+
}
176+
177+
private function endPageOverflow($endPage): bool
178+
{
179+
return $endPage > $this->getNumberOfPages();
180+
}
181+
182+
private function calculateEndPageForStartPageUnderflow($startPage, $endPage): int
183+
{
184+
return min($endPage + (1 - $startPage), $this->getNumberOfPages());
185+
}
186+
187+
private function calculateStartPageForEndPageOverflow($startPage, $endPage): int
188+
{
189+
return max($startPage - ($endPage - $this->getNumberOfPages()), 1);
190+
}
191+
}

src/Twig/PaginatorExtension.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
namespace SumoCoders\FrameworkCoreBundle\Twig;
4+
5+
use Twig\Extension\AbstractExtension;
6+
use Twig\TwigFunction;
7+
8+
final class PaginatorExtension extends AbstractExtension
9+
{
10+
public function getFunctions(): array
11+
{
12+
return [
13+
new TwigFunction('pagination', [PaginatorRuntime::class, 'renderPagination'], ['is_safe' => ['html']]),
14+
];
15+
}
16+
}

0 commit comments

Comments
 (0)