This tutorial has a new version, check it out!

Rock-Solid, Consistent Collection Endpoints

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.

Start your All-Access Pass
Buy just this tutorial for $12.00

Go back to the test we're working on right now. First, every collection should have an items key for consistency. Assert that with $this->asserter()->assertResponsePropertyExists() for items:

... lines 1 - 7
class ProgrammerControllerTest extends ApiTestCase
{
... lines 10 - 64
public function testFollowProgrammerBattlesLink()
{
... lines 67 - 86
$this->asserter()->assertResponsePropertyExists($response, 'items');
$this->debugResponse($response);
}
... lines 90 - 316
}

Pagination in the Past

Next, open ProgrammerController. The whole reason the other endpoint had an items key was because - in listAction() - we went through our fancy pagination system. Click into the pagination_factory. The key part is that this method eventually creates a PaginatedCollection object:

... lines 1 - 10
class PaginationFactory
{
... lines 13 - 19
public function createCollection(QueryBuilder $qb, Request $request, $route, array $routeParams = array())
{
... lines 22 - 33
$paginatedCollection = new PaginatedCollection($programmers, $pagerfanta->getNbResults());
... lines 35 - 55
return $paginatedCollection;
}
}

This is what we feed to the serializer.

The PaginatedCollection object is something we created. And hey! It has an $items property! So this isn't rocket science. It also has a few other properties: total, count and the pagination links:

... lines 1 - 4
class PaginatedCollection
{
private $items;
private $total;
private $count;
private $_links = array();
... lines 14 - 25
}

So if we want every collection endpoint to be identical, every endpoint should return a PaginatedCollection.

Creating a PaginatedCollection

We could do this the simple way: $collection = new PaginatedCollection() and pass it $battles and the total items - which right now is count($battles):

... lines 1 - 23
class ProgrammerController extends BaseController
{
... lines 26 - 154
public function battlesListAction(Programmer $programmer)
{
$battles = $this->getDoctrine()->getRepository('AppBundle:Battle')
->findBy(['programmer' => $programmer]);
$collection = new PaginatedCollection($battles, count($battles));
... lines 161 - 162
}
}

There's not actually any pagination going on.

At the bottom, pass that $collection to createApiResponse():

... lines 1 - 23
class ProgrammerController extends BaseController
{
... lines 26 - 154
public function battlesListAction(Programmer $programmer)
{
$battles = $this->getDoctrine()->getRepository('AppBundle:Battle')
->findBy(['programmer' => $programmer]);
$collection = new PaginatedCollection($battles, count($battles));
return $this->createApiResponse($collection);
}
}

Done! Run that test:

./vendor/bin/phpunit --filter testFollowProgrammerBattlesLink

Yes! Now we have an items key, and total, count and _links... which is empty.

Adding Real Pagination

And really: if we're going to all of this trouble to use the PaginatedCollection, shouldn't we go one extra half-step and actually add pagination? After all, it'll make this endpoint even more consistent by having those pagination links.

Change the $collection = line to $this->get('pagination_factory')->createCollection():

... lines 1 - 23
class ProgrammerController extends BaseController
{
... lines 26 - 154
public function battlesListAction(Programmer $programmer, Request $request)
{
... lines 157 - 159
$collection = $this->get('pagination_factory')->createCollection(
... lines 161 - 164
);
... lines 166 - 167
}
}

This needs a few arguments. The first is a query builder. So instead of making this full query for battles, we need to just return the query builder. Rename this to a new method called - createQueryBuilderForProgrammer() and pass it the $programmer object:

... lines 1 - 23
class ProgrammerController extends BaseController
{
... lines 26 - 154
public function battlesListAction(Programmer $programmer, Request $request)
{
$battlesQb = $this->getDoctrine()->getRepository('AppBundle:Battle')
->createQueryBuilderForProgrammer($programmer);
... lines 159 - 167
}
}

I'll hold command and I'll click Battle to jump into BattleRepository. Add that method: public function createQueryBuilderForProgrammer() with a Programmer $programmer argument:

... lines 1 - 4
use AppBundle\Entity\Programmer;
... lines 6 - 7
class BattleRepository extends EntityRepository
{
public function createQueryBuilderForProgrammer(Programmer $programmer)
{
... lines 12 - 14
}
}

Fortunately, the query is easy: return $this->createQueryBuilder('battle'), then ->andWhere('battle.programmer = :programmer') with setParameter('programmer', $programmer):

... lines 1 - 7
class BattleRepository extends EntityRepository
{
public function createQueryBuilderForProgrammer(Programmer $programmer)
{
return $this->createQueryBuilder('battle')
->andWhere('battle.programmer = :programmer')
->setParameter('programmer', $programmer);
}
}

Perfect! Back in ProgrammerController, rename the variable to $battlesQb and pass it to createCollection():

... lines 1 - 23
class ProgrammerController extends BaseController
{
... lines 26 - 154
public function battlesListAction(Programmer $programmer, Request $request)
{
$battlesQb = $this->getDoctrine()->getRepository('AppBundle:Battle')
->createQueryBuilderForProgrammer($programmer);
$collection = $this->get('pagination_factory')->createCollection(
$battlesQb,
... lines 162 - 164
);
... lines 166 - 167
}
}

The second argument is the request object. You guys know what to do: type-hint a new argument with Request and pass that in:

... lines 1 - 16
use Symfony\Component\HttpFoundation\Request;
... lines 18 - 23
class ProgrammerController extends BaseController
{
... lines 26 - 154
public function battlesListAction(Programmer $programmer, Request $request)
{
$battlesQb = $this->getDoctrine()->getRepository('AppBundle:Battle')
->createQueryBuilderForProgrammer($programmer);
$collection = $this->get('pagination_factory')->createCollection(
$battlesQb,
$request,
... lines 163 - 164
);
... lines 166 - 167
}
}

The third argument is the name of the route the pagination links should point to. That's this route: api_programmers_battles_list. Finally, the last argument is any route parameters that need to be passed to the route. This route has a nickname, so pass nickname => $programmer->getNickname():

... lines 1 - 23
class ProgrammerController extends BaseController
{
... lines 26 - 154
public function battlesListAction(Programmer $programmer, Request $request)
{
... lines 157 - 159
$collection = $this->get('pagination_factory')->createCollection(
$battlesQb,
$request,
'api_programmers_battles_list',
['nickname' => $programmer->getNickname()]
);
... lines 166 - 167
}
}

Done. We basically changed one line to create a real paginated collection. And now, we celebrate. Run the test:

./vendor/bin/phpunit --filter testFollowProgrammerBattlesLink

That is real pagination pretty much out of the box. Yea, this only has three results and only one page: but if this programmer keeps having battles, we're covered.

We've really perfected a lot of traditional REST endpoints. Now, let's talk about what happens when endpoints get weird...

Leave a comment!