Reusable Pagination System
Keep on Learning!
If you liked what you've learned so far, dive in! Subscribe to get access to this tutorial plus video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeSince pagination always looks the same, no matter what you're listing, I really want to organize my code so that pagination is effortless in the future. This took way too many lines of code.
Inside of the Pagination/ directory, create a new PHP class called PaginationFactory. There, add a new public function createCollection() method: this will create the entire final PaginatedCollection object for some collection resource. To do this, we'll need to pass it a few things, starting with the $qb and the $request - we'll use that to find the current page. The method will also need to know the route for the links and any $routeParams it needs:
| // ... lines 1 - 2 | |
| namespace AppBundle\Pagination; | |
| use Doctrine\ORM\QueryBuilder; | |
| // ... lines 6 - 7 | |
| use Symfony\Component\HttpFoundation\Request; | |
| // ... lines 9 - 10 | |
| class PaginationFactory | |
| { | |
| // ... lines 13 - 19 | |
| public function createCollection(QueryBuilder $qb, Request $request, $route, array $routeParams = array()) | |
| { | |
| // ... lines 22 - 53 | |
| } | |
| } |
Go back to ProgrammerController, copy the logic, remove it and put it into PaginationFactory. Add the missing use statements: by auto-completing the classes DoctrineORMAdapter and Pagerfanta. Now, delete $route and $routeParams since those are passed as arguments. Remove the $qb variable for the same reason:
| // ... lines 1 - 5 | |
| use Pagerfanta\Adapter\DoctrineORMAdapter; | |
| use Pagerfanta\Pagerfanta; | |
| // ... lines 8 - 10 | |
| class PaginationFactory | |
| { | |
| // ... lines 13 - 19 | |
| public function createCollection(QueryBuilder $qb, Request $request, $route, array $routeParams = array()) | |
| { | |
| $page = $request->query->get('page', 1); | |
| $adapter = new DoctrineORMAdapter($qb); | |
| $pagerfanta = new Pagerfanta($adapter); | |
| $pagerfanta->setMaxPerPage(10); | |
| $pagerfanta->setCurrentPage($page); | |
| $programmers = []; | |
| foreach ($pagerfanta->getCurrentPageResults() as $result) { | |
| $programmers[] = $result; | |
| } | |
| $paginatedCollection = new PaginatedCollection($programmers, $pagerfanta->getNbResults()); | |
| $createLinkUrl = function($targetPage) use ($route, $routeParams) { | |
| return $this->router->generate($route, array_merge( | |
| $routeParams, | |
| array('page' => $targetPage) | |
| )); | |
| }; | |
| $paginatedCollection->addLink('self', $createLinkUrl($page)); | |
| $paginatedCollection->addLink('first', $createLinkUrl(1)); | |
| $paginatedCollection->addLink('last', $createLinkUrl($pagerfanta->getNbPages())); | |
| if ($pagerfanta->hasNextPage()) { | |
| $paginatedCollection->addLink('next', $createLinkUrl($pagerfanta->getNextPage())); | |
| } | |
| if ($pagerfanta->hasPreviousPage()) { | |
| $paginatedCollection->addLink('prev', $createLinkUrl($pagerfanta->getPreviousPage())); | |
| } | |
| return $paginatedCollection; | |
| } | |
| } |
In fact, move that back to ProgrammerController: we'll want it in a minute:
| // ... lines 1 - 18 | |
| class ProgrammerController extends BaseController | |
| { | |
| // ... lines 21 - 76 | |
| public function listAction(Request $request) | |
| { | |
| $qb = $this->getDoctrine() | |
| ->getRepository('AppBundle:Programmer') | |
| ->findAllQueryBuilder(); | |
| // ... lines 82 - 84 | |
| $response = $this->createApiResponse($paginatedCollection, 200); | |
| return $response; | |
| } | |
| // ... lines 89 - 187 | |
| } |
The only other problem here is $this->generateUrl(): that method does not exist outside of the controller. That's ok: since we do need to generate URLs, this just means we need the router. Add a __construct() function at the top with RouterInterface as an argument. I'll use the Alt + enter PHPStorm shortcut to create and set that property:
| // ... lines 1 - 8 | |
| use Symfony\Component\Routing\RouterInterface; | |
| class PaginationFactory | |
| { | |
| private $router; | |
| public function __construct(RouterInterface $router) | |
| { | |
| $this->router = $router; | |
| } | |
| // ... lines 19 - 54 | |
| } |
Back inside createCollection(), change $this->generateUrl() to $this->router->generate():
| // ... lines 1 - 10 | |
| class PaginationFactory | |
| { | |
| // ... lines 13 - 19 | |
| public function createCollection(QueryBuilder $qb, Request $request, $route, array $routeParams = array()) | |
| { | |
| // ... lines 22 - 35 | |
| $createLinkUrl = function($targetPage) use ($route, $routeParams) { | |
| return $this->router->generate($route, array_merge( | |
| $routeParams, | |
| array('page' => $targetPage) | |
| )); | |
| }; | |
| // ... lines 42 - 53 | |
| } | |
| } |
Our work in this class is done! Next, register this as service in app/config/services.yml - let's call it pagination_factory. How creative! Set the class to PaginationFactory and pass one key for arguments: @router:
| // ... lines 1 - 5 | |
| services: | |
| // ... lines 7 - 25 | |
| pagination_factory: | |
| class: AppBundle\Pagination\PaginationFactory | |
| arguments: ['@router'] |
Tip
If you're using Symfony 3.3, your app/config/services.yml contains some extra code
that may break things when following this tutorial! To keep things working - and learn
about what this code does - see https://knpuniversity.com/symfony-3.3-changes
Copy the service name and open ProgrammerController to hook this all up. Now, just use $paginatedCollection = $this->get('pagination_factory')->createCollection() and pass it the 4 arguments: $qb, $request, the route name - api_programmers_collection - and the route params:
| // ... lines 1 - 18 | |
| class ProgrammerController extends BaseController | |
| { | |
| // ... lines 21 - 76 | |
| public function listAction(Request $request) | |
| { | |
| // ... lines 79 - 81 | |
| $paginatedCollection = $this->get('pagination_factory') | |
| ->createCollection($qb, $request, 'api_programmers_collection'); | |
| // ... lines 84 - 87 | |
| } | |
| // ... lines 89 - 187 | |
| } |
Actually, most of the time you won't have route params. So head back into PaginationFactory and make that argument optional:
| // ... lines 1 - 10 | |
| class PaginationFactory | |
| { | |
| // ... lines 13 - 19 | |
| public function createCollection(QueryBuilder $qb, Request $request, $route, array $routeParams = array()) | |
| { | |
| // ... lines 22 - 53 | |
| } | |
| } |
Much better.
Now, PhpStorm should be happy... but it's still not! It looks more like someone stole it's ice cream. Ah, I forgot to return $paginatedCollection in PaginationFactory:
| // ... lines 1 - 10 | |
| class PaginationFactory | |
| { | |
| // ... lines 13 - 19 | |
| public function createCollection(QueryBuilder $qb, Request $request, $route, array $routeParams = array()) | |
| { | |
| // ... lines 22 - 52 | |
| return $paginatedCollection; | |
| } | |
| } |
PhpStorm was complaining that createCollection() didn't look like it returned anything... and it was right! The robots are definitely taking over.
Run the test to see if we broke anything:
./bin/phpunit -c app --filter filterGETProgrammersCollectionPaginated
We didn't! What a delightful surprise.
Now, if you want some sweet pagination, just create a QueryBuilder, pass it into the PaginationFactory, pass that to createApiResponse and then go find some ice cream.
Hi,
In my case, i applied this <strong>rawurldecode</strong> function to fix query params encoding:
Before rawurldecode:
After rawurldecode:
=)