gstreamer0.10-ffmpeg
gstreamer0.10-plugins-good
packages.
To handle pagination, we're going to install the WhiteOctoberPagerfantaBundle. To install the bundle, run:
composer require white-october/pagerfanta-bundle
Pagerfanta is a great library for pagination, whether you're doing things on the web or building an API. While we're waiting, enable the bundle in AppKernel
:
... lines 1 - 5 | |
class AppKernel extends Kernel | |
{ | |
public function registerBundles() | |
{ | |
$bundles = array( | |
... lines 11 - 20 | |
new WhiteOctober\PagerfantaBundle\WhiteOctoberPagerfantaBundle(), | |
); | |
... lines 23 - 33 | |
} | |
... lines 35 - 39 | |
} |
And that's it for setup: no configuration needed. Now just wait for Composer, and we're ready!
Open up ProgrammerController
and find the listAction()
that we need to work on. Pagination is pretty easy: you basically need to tell the pagination library what page you're on and give it a query builder. Then, you can use it to fetch the correct results for that page.
To read the page query parameter, type-hint the Request
argument and say $page = $request->query->get('page', 1);
. The 1
is the default value in case there is no query parameter:
... lines 1 - 20 | |
class ProgrammerController extends BaseController | |
{ | |
... lines 23 - 74 | |
/** | |
* @Route("/api/programmers") | |
* @Method("GET") | |
*/ | |
public function listAction(Request $request) | |
{ | |
$page = $request->query->get('page', 1); | |
... lines 82 - 102 | |
} | |
... lines 104 - 202 | |
} |
Go Deeper!
You could also use $request->query->getInt('page', 1)
instead of get()
to convert
the page
query parameter into an integer. See accessing request data for other
useful methods.
Next, replace $programmers
with $qb
, standing for query builder. And instead of calling findAll()
, use a new method called findAllQueryBuilder()
:
... lines 1 - 20 | |
class ProgrammerController extends BaseController | |
{ | |
... lines 23 - 78 | |
public function listAction(Request $request) | |
{ | |
$page = $request->query->get('page', 1); | |
$qb = $this->getDoctrine() | |
->getRepository('AppBundle:Programmer') | |
->findAllQueryBuilder(); | |
... lines 86 - 102 | |
} | |
... lines 104 - 202 | |
} |
That doesn't exist yet, so let's go add it!
I'll hold cmd
and click to go into the ProgrammerRepository
. Add the new method: public function findAllQueryBuilder()
. For now, just return $this->createQueryBuilder();
with an alias of programmer
:
... lines 1 - 8 | |
class ProgrammerRepository extends EntityRepository | |
{ | |
... lines 11 - 28 | |
public function findAllQueryBuilder() | |
{ | |
return $this->createQueryBuilder('programmer'); | |
} | |
} |
Perfect!
This is all we need to use Pagerfanta. In the controller, start with $adapter = new DoctrineORMAdapter()
- since we're using Doctrine - and pass it the query builder. Next, create a $pagerfanta
variable set to new Pagerfanta()
and pass it the adapter.
On the Pagerfanta object, call setMaxPerPage()
and pass it 10. And then call $pagerfanta->setCurrentPage()
and pass it $page
:
... lines 1 - 10 | |
use Pagerfanta\Adapter\DoctrineORMAdapter; | |
use Pagerfanta\Pagerfanta; | |
... lines 13 - 20 | |
class ProgrammerController extends BaseController | |
{ | |
... lines 23 - 78 | |
public function listAction(Request $request) | |
{ | |
... lines 81 - 85 | |
$adapter = new DoctrineORMAdapter($qb); | |
$pagerfanta = new Pagerfanta($adapter); | |
$pagerfanta->setMaxPerPage(10); | |
$pagerfanta->setCurrentPage($page); | |
... lines 90 - 102 | |
} | |
... lines 104 - 202 | |
} |
Ultimately, we need Pagerfanta to return the programmers that should be showing right now based on whatever page is being requested. To get that, use $pagerfanta->getCurrentPageResults()
. But there's a problem: instead of returning an array of Programmer
objects, this returns a type of traversable object with those programmes inside. This confuses the serializer. To fix that, create a new programmers array: $programmers = []
.
Next, loop over that traversable object from Pagerfanta and push each Programmer
object into our simple array. This gives us a clean array of Programmer
objects:
... lines 1 - 20 | |
class ProgrammerController extends BaseController | |
{ | |
... lines 23 - 78 | |
public function listAction(Request $request) | |
{ | |
... lines 81 - 90 | |
$programmers = []; | |
foreach ($pagerfanta->getCurrentPageResults() as $result) { | |
$programmers[] = $result; | |
} | |
... lines 95 - 102 | |
} | |
... lines 104 - 202 | |
} |
And that means we're dangerous. In createApiResponse
, we still need to pass in the programmers
key, but we also need to add count
and total
. Add the total
key and set it to $pagerfanta->getNbResults()
.
For count
, that's easy: that's the current number of results that are shown on this page. Just use count($programmers)
:
... lines 1 - 20 | |
class ProgrammerController extends BaseController | |
{ | |
... lines 23 - 78 | |
public function listAction(Request $request) | |
{ | |
... lines 81 - 95 | |
$response = $this->createApiResponse([ | |
'total' => $pagerfanta->getNbResults(), | |
'count' => count($programmers), | |
'programmers' => $programmers, | |
], 200); | |
return $response; | |
} | |
... lines 104 - 202 | |
} |
We're definitely not done, but this should be enough to return a valid response on page 1 at least. Test it out. Copy the method name and use --filter
to just run that test:
./bin/phpunit -c app --filter testGETProgrammersCollectionPaginated
This fails. But look closely: we do have programmers 0 through 9 in the response for page 1. It fails when trying to read the _links.next
property because we haven't added those yet.
Before we add those, there's one improvement I want to make. Since we'll use pagination in a lot of places, we're going to need to duplicate this JSON structure. Why not create an object with these properties, and then let the serializer turn that object into JSON?
Create a new directory called Pagination
. And inside of that, a new class to model this called PaginatedCollection
. Make sure it's in the AppBundle\Pagination
namespace. Very simply: give this 3 properties: items
, total
and count
:
... lines 1 - 2 | |
namespace AppBundle\Pagination; | |
class PaginatedCollection | |
{ | |
private $items; | |
private $total; | |
private $count; | |
... lines 12 - 18 | |
} |
Generate the constructor and allow items
and total
to be passed. We don't need the count
because again we can set it with $this->count = count($items)
. That should do it!
... lines 1 - 4 | |
class PaginatedCollection | |
{ | |
... lines 7 - 12 | |
public function __construct(array $items, $totalItems) | |
{ | |
$this->items = $items; | |
$this->total = $totalItems; | |
$this->count = count($items); | |
} | |
} |
But something did just change: this object has an items
property instead of programmers
. That will change the JSON response. I made this change because I want to re-use this class for other resources. With a little serializer magic, you could make this dynamic: programmers
in this case and something else like battles
in other situations. But instead, I'm going to stay with items
. This is something you often see with APIs: if they have their collection results under a key
, they often use the same key - like items
- for all responses.
But this means that I just changed our API. In the test, search for programmers
: all of these keys need to change to items
, so make sure you find them all:
... lines 1 - 5 | |
class ProgrammerControllerTest extends ApiTestCase | |
{ | |
... lines 8 - 53 | |
public function testGETProgrammersCollection() | |
{ | |
... lines 56 - 66 | |
$this->asserter()->assertResponsePropertyIsArray($response, 'items'); | |
$this->asserter()->assertResponsePropertyCount($response, 'items', 2); | |
$this->asserter()->assertResponsePropertyEquals($response, 'items[1].nickname', 'CowboyCoder'); | |
} | |
... line 71 | |
public function testGETProgrammersCollectionPaginated() | |
{ | |
... lines 74 - 83 | |
$this->asserter()->assertResponsePropertyEquals( | |
$response, | |
'items[5].nickname', | |
'Programmer5' | |
); | |
... lines 89 - 97 | |
$this->asserter()->assertResponsePropertyEquals( | |
$response, | |
'items[5].nickname', | |
'Programmer15' | |
); | |
... lines 103 - 107 | |
$this->asserter()->assertResponsePropertyEquals( | |
$response, | |
'items[4].nickname', | |
'Programmer24' | |
); | |
$this->asserter()->assertResponsePropertyDoesNotExist($response, 'items[5].name'); | |
... line 115 | |
} | |
... lines 117 - 221 | |
} |
Using the new class is easy: $paginatedCollection = new PaginatedCollection()
. Pass it $programmers
and $pagerfanta->getNbResults()
.
To create the ApiResponse
pass it the $paginatedCollection
variable directly: $response = $this->createApiResponse($paginatedCollection)
:
... lines 1 - 10 | |
use AppBundle\Pagination\PaginatedCollection; | |
... lines 12 - 21 | |
class ProgrammerController extends BaseController | |
{ | |
... lines 24 - 79 | |
public function listAction(Request $request) | |
{ | |
... lines 82 - 96 | |
$paginatedCollection = new PaginatedCollection($programmers, $pagerfanta->getNbResults()); | |
... lines 98 - 117 | |
$response = $this->createApiResponse($paginatedCollection, 200); | |
return $response; | |
} | |
... lines 122 - 220 | |
} |
Try the test!
./bin/phpunit -c app --filter testGETProgrammersCollectionPaginated
It still fails, but only once it looks for the links. The first response looks exactly how we want it to. Okay, that's awesome - so now let's add some links.
Hey Dirk
Actually no, count($programmers)
it's the count of results shown in the page, and $pagerfanta->getNbResults()
is the total number of results found in the database.
Cheers!
I had to add "->orderBy('programmer.id', 'ASC')" in findAllQueryBuilder for this to work. I don't understand why it's always given me programmers in a random order.
Yo Kreviouss!
I agree - that's weird. But, we can easily debug and see what's going on. If you remove this orderBy line and refresh, you should be able to click the database icon in the web debug toolbar to go to the "Doctrine" tab on the profiler. On this page, you can see what the query looks like, to see if Doctrine is doing anything weird. It *should* simply be executing a query with *no* ORDER BY on it. If this is true, then it's actually your database that is returning things in a random order. In the profiler, you can click to get a "runnable" version of your query. If you run that directly on your database, you can see what happens: does it return in a random order, or ordered by id?
Let me know what you find out! Cheers!
I fact, it's only on test mode. If i go to http://localhost:8000/api/programmers threw the navigator I have a normal request answer (from Programmer0 to Programmer9 with no ORDER BY). If I take the SQL and run it directly to my database, same good answer.
It's only when I run the test command that the answer is in a random order.
My normal and test database are build the same way.
I have tried to load fixtures in test database to see test environment in the navigator. And then, same answer as with the test command. A randomly ordered answer.
So it seems to be a test environment issue...
(sorry for my bad French English ><)
Huh, very interesting! Is your test database also in MySQL, or is it in Sqlite? I like how you debugged this: to load the fixtures in the test environment and then browse the site in the test environment. That's very puzzling :), as the same query should be used in both environments. Are you able to look at the test database to see if the data is loaded in the same way? I'm just trying to think of what could possibly be different between the test and normal databases...
// composer.json
{
"require": {
"php": ">=5.3.3",
"symfony/symfony": "2.6.*", // v2.6.11
"doctrine/orm": "~2.2,>=2.2.3,<2.5", // v2.4.7
"doctrine/dbal": "<2.5", // v2.4.4
"doctrine/doctrine-bundle": "~1.2", // v1.4.0
"twig/extensions": "~1.0", // v1.2.0
"symfony/assetic-bundle": "~2.3", // v2.6.1
"symfony/swiftmailer-bundle": "~2.3", // v2.3.8
"symfony/monolog-bundle": "~2.4", // v2.7.1
"sensio/distribution-bundle": "~3.0,>=3.0.12", // v3.0.21
"sensio/framework-extra-bundle": "~3.0,>=3.0.2", // v3.0.7
"incenteev/composer-parameter-handler": "~2.0", // v2.1.0
"hautelook/alice-bundle": "0.2.*", // 0.2
"jms/serializer-bundle": "0.13.*", // 0.13.0
"white-october/pagerfanta-bundle": "^1.0" // v1.2.4
},
"require-dev": {
"sensio/generator-bundle": "~2.3", // v2.5.3
"behat/behat": "~3.0", // v3.0.15
"behat/mink-extension": "~2.0.1", // v2.0.1
"behat/mink-goutte-driver": "~1.1.0", // v1.1.0
"behat/mink-selenium2-driver": "~1.2.0", // v1.2.0
"phpunit/phpunit": "~4.6.0" // 4.6.4
}
}
Is the total from NBresults and Count() not the same thing?