Buy Access to Course
04.

Reusable Pagination System

Share this awesome video!

|

Keep on Learning!

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

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:

// ... 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:

29 lines | app/config/services.yml
// ... 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.