gstreamer0.10-ffmpeg
gstreamer0.10-plugins-good
packages.
Tip
In this course we're using Symfony 2, but starting in episode 4, we use Symfony 3. If you'd like to see the finished code for this tutorial in Symfony 3, download the code from episode 4 and check out the start directory!
Hey, Guys! Welcome to Episode 3 of our REST in Symfony Series. In this episode, we're going to cover some really important details we haven't talked about yet, like pagination, filtering, and taking the serializer and doing really cool and custom things with it.
If you're following along with me, use the same project we've been building. If you're
just joining, where have you been? Ah, it's fine: download the code from this page
and move into the start/
directory. Start up the built-in PHP web server to get
things running:
./app/console server:run
Let's talk about pagination first, because the /api/programmers
endpoint doesn't
have it. Eventually, once someone talks about our cool app on Reddit, we're going
to have a lot of programmers here: too many to return all at once. First, think about
pagination on the web. How does it work? Usually, it's done with query parameters:
something like ?page=1
, ?page=2
, and so on. Sometimes, it's done in the URL - like
/products/1
and /products/2
. For API's, query parameters is better.
Second, on the web, we don't make the user guess those URLs: we give them links, like "next" and "previous", and maybe even "first" and "last".
So why would building an API be any different? Let's use query parameters and include links to help the API client get around.
Like always, we're gonna start with a test because it's the easiest way to try things
out and it helps us think about the API's design. In ProgrammerControllerTest
,
find the testProgrammersCollection()
method and copy this to make a new test for
pagination:
... lines 1 - 5 | |
class ProgrammerControllerTest extends ApiTestCase | |
{ | |
... lines 8 - 71 | |
public function testGETProgrammersCollectionPaginated() | |
{ | |
... lines 74 - 88 | |
} | |
... lines 90 - 194 | |
} |
To make this interesting, we need more programmers - like 25. Add a for
loop to
do this: for i=0; i<25; i++
. In each loop, create a programmer with the super
creative name of Programmer plus the $i
value. This means that we'll have programmers
zero through 24. The avatarNumber
is required, but we don't care about its value:
... lines 1 - 5 | |
class ProgrammerControllerTest extends ApiTestCase | |
{ | |
... lines 8 - 71 | |
public function testGETProgrammersCollectionPaginated() | |
{ | |
for ($i = 0; $i < 25; $i++) { | |
$this->createProgrammer(array( | |
'nickname' => 'Programmer'.$i, | |
'avatarNumber' => 3, | |
)); | |
} | |
// page 1 | |
... lines 82 - 88 | |
} | |
... lines 90 - 194 | |
} |
Keep the same URL and the 200 status code assertion. Below, start basic with a sanity
check for page 1: assert that the programmer with index 5 is equal to Programmer5
:
... lines 1 - 5 | |
class ProgrammerControllerTest extends ApiTestCase | |
{ | |
... lines 8 - 71 | |
public function testGETProgrammersCollectionPaginated() | |
{ | |
... lines 74 - 80 | |
// page 1 | |
$response = $this->client->get('/api/programmers'); | |
$this->assertEquals(200, $response->getStatusCode()); | |
$this->asserter()->assertResponsePropertyEquals( | |
$response, | |
'programmers[5].nickname', | |
'Programmer5' | |
); | |
} | |
... lines 90 - 194 | |
} |
I'll use multiple lines to keep things clear. Index 5 is actually the 6th programmer, but since we start with Programmer0, this should definitely be Programmer5.
It might also be useful to tell the API client how many results are on this page
and how many results there are in total. I want to show 10 results per page in the
API so add a line that looks for a new property called count
that's set to 10. Let's
also have another property called total
. That'll be the total number of results.
In this case, that should be 25:
... lines 1 - 5 | |
class ProgrammerControllerTest extends ApiTestCase | |
{ | |
... lines 8 - 71 | |
public function testGETProgrammersCollectionPaginated() | |
{ | |
... lines 74 - 89 | |
$this->asserter()->assertResponsePropertyEquals($response, 'count', 10); | |
$this->asserter()->assertResponsePropertyEquals($response, 'total', 25); | |
... line 92 | |
} | |
... lines 94 - 198 | |
} |
Finally, the API response needs to have those links! And by "links", I mean that I
want to add a new field - maybe called "next" - whose value will be the URL to get
the next page of results. Use the asserter again and change this to
assertResponsePropertyExists()
. Let's assert that there is an _links.next
key,
which means the JSON will have an _links
key and a next
key under that:
... lines 1 - 5 | |
class ProgrammerControllerTest extends ApiTestCase | |
{ | |
... lines 8 - 71 | |
public function testGETProgrammersCollectionPaginated() | |
{ | |
... lines 74 - 91 | |
$this->asserter()->assertResponsePropertyExists($response, '_links.next'); | |
} | |
... lines 94 - 198 | |
} |
By moving things under _links
, it makes it a little more obvious that next
isn't
a property of a programmer, but something different: a link.
Oh, and you probably saw my mistake above: change the line above to total
, not count
.
And here's where things get really cool. In our test, we need to make a request to page 2 and make sure we see the next 10 programmers. Instead of hardcoding the URL, we can read the next link and use that for the next request. It's like the API version of clicking links!
Use $this->asserter()
and then a method called readResponseProperty()
to read
the _links.next
property. Now, add $response = $this->client->get($nextUrl)
to
go to the next page:
... lines 1 - 5 | |
class ProgrammerControllerTest extends ApiTestCase | |
{ | |
... lines 8 - 71 | |
public function testGETProgrammersCollectionPaginated() | |
{ | |
... lines 74 - 93 | |
// page 2 | |
$nextLink = $this->asserter()->readResponseProperty($response, '_links.next'); | |
$response = $this->client->get($nextLink); | |
... lines 97 - 103 | |
} | |
... lines 105 - 209 | |
} |
Ok, let's test page 2! Copy some of the asserts that we just wrote. This time, the
programmer with index 5 should be Programmer15
because we're looking at results
11 through 20. Next, the count
should still be 10, and the total
still 25 - but
let's save a little code and remove that line:
... lines 1 - 5 | |
class ProgrammerControllerTest extends ApiTestCase | |
{ | |
... lines 8 - 71 | |
public function testGETProgrammersCollectionPaginated() | |
{ | |
... lines 74 - 96 | |
$this->assertEquals(200, $response->getStatusCode()); | |
$this->asserter()->assertResponsePropertyEquals( | |
$response, | |
'programmers[5].nickname', | |
'Programmer15' | |
); | |
$this->asserter()->assertResponsePropertyEquals($response, 'count', 10); | |
} | |
... lines 105 - 209 | |
} |
The next
link is nice. But we can do even more by also having a first
link,
a last
link and a prev
link unless we're on page 1. Copy the code from earlier
that clicked the next
link. Ooh, and let me fixing my formatting!
This time, use the _links.last
key and update the variable to be $lastUrl
. When
we make a request to the final page, programmers[4]
will be the last programmer
because we started with index 0. The name should be Programmer24
. And on this last
page, count
should be just 5:
... lines 1 - 5 | |
class ProgrammerControllerTest extends ApiTestCase | |
{ | |
... lines 8 - 71 | |
public function testGETProgrammersCollectionPaginated() | |
{ | |
... lines 74 - 104 | |
$lastLink = $this->asserter()->readResponseProperty($response, '_links.last'); | |
$response = $this->client->get($lastLink); | |
$this->assertEquals(200, $response->getStatusCode()); | |
$this->asserter()->assertResponsePropertyEquals( | |
$response, | |
'programmers[4].nickname', | |
'Programmer24' | |
); | |
... line 114 | |
$this->asserter()->assertResponsePropertyEquals($response, 'count', 5); | |
} | |
... lines 117 - 221 | |
} |
I'm also going to use the asserter with assertResponsePropertyDoesNotExist()
to make sure that there is no programmer here with index 5. Specifically,
check for no programmers[5].nickname
path:
... lines 1 - 5 | |
class ProgrammerControllerTest extends ApiTestCase | |
{ | |
... lines 8 - 71 | |
public function testGETProgrammersCollectionPaginated() | |
{ | |
... lines 74 - 113 | |
$this->asserter()->assertResponsePropertyDoesNotExist($response, 'programmers[5].name'); | |
$this->asserter()->assertResponsePropertyEquals($response, 'count', 5); | |
} | |
... lines 117 - 221 | |
} |
There's a small bug in my asserter code: if I just check for programmers[5]
, it thinks
it exists but is set to null
. That's why I'm checking for the nickname
key.
That's it! Our pagination system is now really well-defined. Next, we'll bring this all to life.
// 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
}
}