Buy

Reusable Pagination System

Since 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:

55 lines src/AppBundle/Pagination/PaginationFactory.php
... 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:

55 lines src/AppBundle/Pagination/PaginationFactory.php
... 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:

189 lines src/AppBundle/Controller/Api/ProgrammerController.php
... 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:

55 lines src/AppBundle/Pagination/PaginationFactory.php
... 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():

55 lines src/AppBundle/Pagination/PaginationFactory.php
... 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.

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:

189 lines src/AppBundle/Controller/Api/ProgrammerController.php
... 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:

55 lines src/AppBundle/Pagination/PaginationFactory.php
... 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:

55 lines src/AppBundle/Pagination/PaginationFactory.php
... 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.

Leave a comment!

  • 2016-08-11 weaverryan

    Ha, oh totally - you're 100% right :). I hadn't even noticed this!

  • 2016-08-09 Chuck Norris

    Hi Ryan,

    It's really nothing, but for consistency, shouldn't you rename "programmers" variable into "items" inside createCollection method ?

  • 2016-03-03 weaverryan

    Cheers Jovan :)

  • 2016-03-01 Jovan Perović

    This is like 20th lesson or so I listened that was recorded by you. Must say, I enjoy every second of those. Very knowledgeable, funny, fast... just keep doing what you're doing :)