Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This tutorial has a new version, check it out!

GET a Collection of Programmers

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 $10.00

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

Login Subscribe

Our API client will need a way to fetch a collection of programmers. Piece of cake. Start with public function listAction(). For the URI, use /api/programmers and add an @Method("GET"):

... lines 1 - 67
/**
* @Route("/api/programmers")
* @Method("GET")
*/
public function listAction()
{
... lines 74 - 85
}
... lines 87 - 98

So, the URI you POST to when creating a resource - /api/programmers - will be the same as the one you'll GET to fetch a collection of programmer resources. And yes, you can filter and paginate this list - all stuff we'll do later on.

Inside listAction(), start like we always do: with a query. $programmers = $this->getDoctrine()->getRepository('AppBundle:Programmer') and for now, we'll find everything with findAll():

... lines 1 - 71
public function listAction()
{
$programmers = $this->getDoctrine()
->getRepository('AppBundle:Programmer')
->findAll();
... lines 77 - 85
}
... lines 87 - 98

Next, we need to transform this array of Programmers into JSON. We'll want to re-use some logic from before, so let's create a new private function called serializeProgrammer() and add a Programmer argument. Inside, we can steal the manual logic that turns a Programmer into an array and just return it:

... lines 1 - 87
private function serializeProgrammer(Programmer $programmer)
{
return array(
'nickname' => $programmer->getNickname(),
'avatarNumber' => $programmer->getAvatarNumber(),
'powerLevel' => $programmer->getPowerLevel(),
'tagLine' => $programmer->getTagLine(),
);
}
... lines 97 - 98

That's a small improvement - at least we can re-use this stuff from inside this controller. In showAction(), use $this->serializeProgrammer() and pass it the variable.

... lines 1 - 46
public function showAction($nickname)
{
... lines 49 - 59
$data = $this->serializeProgrammer($programmer);
$response = new Response(json_encode($data), 200);
... lines 63 - 65
}
... lines 67 - 98

Back in listAction(), we'll need to loop over the Programmers and serialize them one-by-one. So start by creating a $data array with a programmers key that's also set to an empty array. We're going to put the programmers there:

... lines 1 - 71
public function listAction()
{
$programmers = $this->getDoctrine()
->getRepository('AppBundle:Programmer')
->findAll();
$data = array('programmers' => array());
... lines 78 - 98

Avoid JSON Hijacking

Why? You can structure your JSON however you want, but by putting the collection inside a key, we have room for more root keys later like maybe count or offset for pagination. Second, your outer JSON should always be an object, not an array. So, curly braces instead of square brackets. If you have square brackets, you're vulnerable to something called JSON Hijacking. While not as bad as a car hijacking is something you want to avoid.

Turning the Programmers into JSON

Loop through $programmers, and one-by-one, say $data['programmers'][] and push on $this->serializeProgrammer(). The end is the same as showAction(), so just copy that:

... lines 1 - 71
public function listAction()
{
$programmers = $this->getDoctrine()
->getRepository('AppBundle:Programmer')
->findAll();
$data = array('programmers' => array());
foreach ($programmers as $programmer) {
$data['programmers'][] = $this->serializeProgrammer($programmer);
}
$response = new Response(json_encode($data), 200);
$response->headers->set('Content-Type', 'application/json');
return $response;
}
... lines 87 - 98

That ought to do it. Update testing.php to make another call out to /api/programmers. Let's see what that looks like:

php testing.php

Woh, ok! We've got a database full of programmers. NERDS! Creating a new endpoint is getting easier - that trend will continue.

Returning JSON on Create

Remember how we're returning a super-reassuring text message from our POST endpoint? Well, you can do this, but usually, you'll return the resource you just created. That's easy now - so let's do it. Just, $data = $this->serializeProgrammer(). Then json_encode() that in the Response. And don't forget to set the Content-Type header:

... lines 1 - 18
public function newAction(Request $request)
{
... lines 21 - 32
$data = $this->serializeProgrammer($programmer);
$response = new Response(json_encode($data), 201);
... lines 35 - 39
$response->headers->set('Content-Type', 'application/json');
return $response;
}
... lines 44 - 100

To see if this is working, dump again right after the first request:

35 lines testing.php
... lines 1 - 18
// 1) Create a programmer resource
$response = $client->post('/api/programmers', [
'body' => json_encode($data)
]);
echo $response;
echo "\n\n";die;
... lines 25 - 35

Hit it!

php testing.php

That's a helpful response: it has a Location header and shows you the resource immediately. Because, why not?

JsonResponse

We're about to move onto testing - which makes this all so much fun. But first, we can shorten things. Each endpoint json_encodes the data and sets the Content-Type header. Use a class called JsonResponse instead. And instead of passing it the JSON string, just pass it the data array. The other nice thing is that you don't need to set the Content-Type header, it does that for you:

... lines 1 - 19
public function newAction(Request $request)
{
... lines 22 - 33
$data = $this->serializeProgrammer($programmer);
$response = new JsonResponse($data, 201);
... lines 36 - 41
return $response;
}
... lines 44 - 98

API consistency is king, and this is just one less spot for me to mess up and forget to set that header. Make sure you update the other spots, which is pretty much a copy-and-paste operation - be careful to keep the right status code.

Take out the extra die() statement in testing.php and let's try this whole thing out:

php testing.php

It's lovely. To make sure it doesn't break, we need to add tests! And that will be a whole lot more interesting than you think.

Leave a comment!

0
Login or Register to join the conversation
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial uses an older version of Symfony. The concepts of REST are still valid, but I recommend using API Platform in new Symfony apps.

What PHP libraries does this tutorial use?

// 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
    },
    "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
    }
}
userVoice