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:
=)