This tutorial has a new version, check it out!

JSON Responses + Route Generation

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

Okay, this is cool... but what about APIs and JavaScript frontends and all that new fancy stuff? How does Symfony stand up to that? Actually, it stands up wonderfully: Symfony is a first-class tool for building APIs. Seriously, you're going to love it.

Since the world is now a mix of traditional apps that return HTML and API's that feed a JavaScript frontend, we'll make an app that's a mixture of both.

Right now, the notes are rendered server-side inside of the show.html.twig template. But that's not awesome enough! If an aquanaut adds a new comment, I need to see it instantly, without refreshing. To do that, we'll need an API endpoint that returns the notes as JSON. Once we have that, we can use JavaScript to use that endpoint and do the rendering.

Creating API Endpoints

So how do you create API endpoints in Symfony? Ok, do you remember what a controller always returns? Yes, a Response! And ya know what? Symfony doesn't care whether that holds HTML, JSON, or a CSV of octopus research data. So actually, this turns out to be really easy.

Create a new controller: I'll call it getNotesAction(). This will return notes for a specific genus. Use @Route("/genus/{genusName}/notes"). We really only want this endpoint to be used for GET requests to this URL. Add @Method("GET"):

... lines 1 - 9
class GenusController extends Controller
{
... lines 12 - 21
/**
* @Route("/genus/{genusName}/notes")
* @Method("GET")
*/
public function getNotesAction($genusName)
{
... lines 28 - 37
}
}

Without this, the route will match a request using any HTTP method, like POST. But with this, the route will only match if you make a GET request to this URL. Did we need to do this? Well no: but it's pretty trendy in API's to think about which HTTP method should be used for each route.

Missing Annotation use Statement

Hmm, it's highlighting the @Method as a missing import. Ah! Don't forget when you use annotations, let PhpStorm autocomplete them for you. That's important because when you do that, PhpStorm adds a use statement at the top of the file that you need:

... lines 1 - 4
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
... lines 6 - 40

If you forget this, you'll get a pretty clear error about it.

Ok, let's see if Symfony sees the route! Head to the console and run debug:router:

php bin/console debug:router

Hey! There's the new route at the bottom, with its method set to GET.

The JSON Controller

Remove the $notes from the other controller: we won't pass that to the template anymore:

... lines 1 - 9
class GenusController extends Controller
{
... lines 12 - 14
public function showAction($genusName)
{
return $this->render('genus/show.html.twig', array(
'name' => $genusName,
));
}
... lines 21 - 38
}

In the new controller, I'll paste a new $notes variable set to some beautiful data:

... lines 1 - 9
class GenusController extends Controller
{
... lines 12 - 25
public function getNotesAction($genusName)
{
$notes = [
['id' => 1, 'username' => 'AquaPelham', 'avatarUri' => '/images/leanna.jpeg', 'note' => 'Octopus asked me a riddle, outsmarted me', 'date' => 'Dec. 10, 2015'],
['id' => 2, 'username' => 'AquaWeaver', 'avatarUri' => '/images/ryan.jpeg', 'note' => 'I counted 8 legs... as they wrapped around me', 'date' => 'Dec. 1, 2015'],
['id' => 3, 'username' => 'AquaPelham', 'avatarUri' => '/images/leanna.jpeg', 'note' => 'Inked!', 'date' => 'Aug. 20, 2015'],
];
... lines 33 - 37
}
}

We're not using a database yet, but you can already see that this kind of looks like it came from one: it has a username, a photo for each avatar, and the actual note. It'll be pretty easy to make this dynamic in the next episode.

Next, create a $data variable, set it to an array, and put the $notes in a notes key inside of that. Don't worry about this: I'm just creating a future JSON structure I like:

... lines 1 - 27
$notes = [
['id' => 1, 'username' => 'AquaPelham', 'avatarUri' => '/images/leanna.jpeg', 'note' => 'Octopus asked me a riddle, outsmarted me', 'date' => 'Dec. 10, 2015'],
['id' => 2, 'username' => 'AquaWeaver', 'avatarUri' => '/images/ryan.jpeg', 'note' => 'I counted 8 legs... as they wrapped around me', 'date' => 'Dec. 1, 2015'],
['id' => 3, 'username' => 'AquaPelham', 'avatarUri' => '/images/leanna.jpeg', 'note' => 'Inked!', 'date' => 'Aug. 20, 2015'],
];
$data = [
'notes' => $notes
];
... lines 36 - 40

Now, how do we finally return $data as JSON? Simple: return new Response() and pass it json_encode($data):

... lines 1 - 9
class GenusController extends Controller
{
... lines 12 - 25
public function getNotesAction($genusName)
{
... lines 28 - 36
return new Response(json_encode($data));
}
}

Simple!

Hey, let's see if this works. Copy the existing URL and add /notes at the end. Congratulations, you've just created your first Symfony API endpoint.

JsonResponse

But you know, that could have been easier. Replace the Response with new JsonResponse and pass it $data without the json_encode:

... lines 1 - 7
use Symfony\Component\HttpFoundation\JsonResponse;
... lines 9 - 10
class GenusController extends Controller
{
... lines 13 - 26
public function getNotesAction($genusName)
{
... lines 29 - 37
return new JsonResponse($data);
}
}

This does two things. First, it calls json_encode() for you. Hey thanks! And second, it sets the application/json Content-Type header on the Response, which we could have set manually, but this is easier.

Refresh. It still works perfectly.

Leave a comment!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=5.5.9",
        "symfony/symfony": "3.1.*", // v3.1.4
        "doctrine/orm": "^2.5", // v2.7.2
        "doctrine/doctrine-bundle": "^1.6", // 1.6.4
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.3.0
        "symfony/swiftmailer-bundle": "^2.3", // v2.3.11
        "symfony/monolog-bundle": "^2.8", // 2.11.1
        "symfony/polyfill-apcu": "^1.0", // v1.2.0
        "sensio/distribution-bundle": "^5.0", // v5.0.22
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.16
        "incenteev/composer-parameter-handler": "^2.0" // v2.1.2
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.0.7
        "symfony/phpunit-bridge": "^3.0" // v3.1.3
    }
}