JSON Responses + Route Generation

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"):

40 lines src/AppBundle/Controller/GenusController.php
... 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:

40 lines src/AppBundle/Controller/GenusController.php
... 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:

40 lines src/AppBundle/Controller/GenusController.php
... 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:

40 lines src/AppBundle/Controller/GenusController.php
... 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:

40 lines src/AppBundle/Controller/GenusController.php
... 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):

40 lines src/AppBundle/Controller/GenusController.php
... 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:

41 lines src/AppBundle/Controller/GenusController.php
... lines 1 - 37
return new JsonResponse($data);
... lines 39 - 41

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!

  • 2016-11-16 Zuhayer Tahir

    Thanks for asking, i had the same is question (y)

  • 2016-11-15 Yurii Romanov

    Nice! Thank you!

  • 2016-11-15 weaverryan

    Hey Yuri!

    It should be like this:


    $response = new JsonResponse(...);
    $response->setEncodingOptions(JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);

    Here's some more info, above the encodingOptions property in that class: https://github.com/symfony/sym...

    Hope that helps!

  • 2016-11-15 Yurii Romanov

    Hi! How i can set JSON_UNESCAPED_UNICODE and JSON_UNESCAPED_SLASHES to JsonResponse?

  • 2016-11-08 weaverryan

    Yes, awesome! If you have any questions, let me know!

    Keep up the good work!

  • 2016-11-08 Nobuyuki Fujioka

    Yo, Ryan

    Thank you for your reply. I struggled for many hours, but yes I used the nelmio cors bundle thingy, and now it is working. Still not understanding 100% how I made it work with the nelmio settings, but good enough for now.

    Cheers,
    Noby

  • 2016-11-07 weaverryan

    Hey Noby!

    Sure! I typically use https://github.com/nelmio/Nelm.... It works really nicely to return the necessary headers from your API so that JavaScript can get passed the CORS issue. Of course, another solution (which just may not be possible in your situation) is to keep the two sites under the same domain - then CORS isn't a problem.

    Cheers!

  • 2016-11-07 Nobuyuki Fujioka

    Hi, KnpUniversity

    I am trying to make a quick api with symfony as backend, and a completely separate front end (angular1) talking to the symfony api.
    I am not sure how to get rid of the cross origin issue. Could you give me some pointer on how to set up the cors so that I can use the api from the front end app (javascript. no php, no twig) in another domain?

    Thanks,
    Noby

  • 2016-11-03 Victor Bocharsky

    Ah, yes! it's my fault... Thanks for noticing me, I'll edit my comment )

  • 2016-11-03 Nobuyuki Fujioka

    Hi, Victor
    Excellent! Thank you!
    Just minor note, I think cache:clear instead of clear:cache. It works now. Thanks! =)

  • 2016-11-03 Victor Bocharsky

    Hey Nobuyuki,

    Yes, Symfony has a console command to clear its cache - execute the next command in your terminal:


    # to clear the cache for dev environment:
    $ bin/console cache:clear

    # or to clear the cache for prod environment (or any other environment you have, just type it instead of prod):
    $ bin/console cache:clear --env=prod

    Cheers!

  • 2016-11-03 Nobuyuki Fujioka

    Hi, Victor

    I had a similar issue as others. Symfony was not able to find route /genus/{genusName} anymore. notes route was working.
    I just continue to follow your instruction in the next chapter and the issue resolved by itself. I wonder if it is a cache issue as you mentioned above.

    Is there any special way to clear cash in phpStorm or in symfony?

    Your help is appreciated.

  • 2016-08-18 Oscar Galván

    I think this is actually normal, because in the video we deleted the variable called $notes from the showAction method.

  • 2016-08-04 Victor Bocharsky

    Oh, did you confuse due to my answer? Or was this tutorial hard for you? Tell us what was the hardest part for you in this course.

  • 2016-08-03 Zoé Belleton

    Thanks ! I feel very lost... Is there any other lessons that a beginner should follow before this course (except the PHP POO part that I have already did )?

  • 2016-08-03 Victor Bocharsky

    Hey Zoe,

    Ah, let's debug this thing! PhpStorm could help with it, right click on the project, choose "Find in path" item in drop down menu and search for "show.html.twig". PhpStorm finds all places where you use this template. Then ensure you pass that "notes" variable to this template in all places where you render / use it. Of course, you should exclude cache directory. BTW, don't forget to clear cache as well after any changes, especially for prod env.

    Cheers!

  • 2016-08-03 Zoé Belleton

    I have the same problem and it doesn't help me...

  • 2016-07-11 Victor Bocharsky

    Hey Viktor,

    That's bad. It means you try to use variable `notes` didn't passed into the template. Probably you forgot to pass it in a controller's action which render this template like:

    public function someAction()
    {
    // some code

    return $this->render('genus/show.html.twig', [
    'notes' => $notes,
    // other variables
    ]);
    }

    Check that and let me know if it helped.

    Cheers!

  • 2016-07-09 Viktor

    Is it okay if when trying to access my http://localhost:8000/genus/octopus I get error saying: Variable "notes" does not exist in genus/show.html.twig at line 25

  • 2016-05-11 zs Zoubair

    nice work bro keep up (y) :)

  • 2016-04-29 weaverryan

    Not a stupid question :). There is no reason - both would work just fine. Typically, when I am returning a "list" of things (like a list of notes), I put them under a key. This would allow you later to add other properties more easily, in case you want to return notes and something else. But technically speaking, there's no reason - but it's a good idea!

  • 2016-04-29 BondashMaster

    Why use $data = ['notes' => $notes]; instead of just returning notes???
    Sorry if is a stupid question. I had never usen Json before.

  • 2016-04-28 weaverryan

    Hi Konrad!

    Yep, it's ok :). Technically, my output also has no spaces, but I have a plugin for my browser - JSONView - which formats it "pretty" for me.

    Cheers!

  • 2016-04-27 Konrad Zając

    Hi,
    I went through the tutorial and it showes me the correct data, but not formated, like:
    {"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"}]}
    All in one line, is it ok?

  • 2016-03-30 Brian Morris

    They are talking about the formatting of the output not the output itself. For anyone else with the issue, install a browser extension (or plug in). Just search for your browser + "json formatter".

  • 2016-03-08 Andrew Grudin

    I had same output as hanthuy had too , but after installing JSONView on firefox all is fine!

  • 2016-03-03 weaverryan

    Hey there!

    I think I know the issue :). But first, it's *more* important that you only enable "dev" bundles in the dev/test environments in AppKernel than adding libraries to require-dev. Making sure a bundle doesn't get loaded in "prod" if it's not needed actually makes your container smaller and gives you a smaller memory footprint. But require-dev basically just makes your filesystem footprint smaller - which doesn't have much impact on performance.

    Now, I'm guessing that your deploy explodes on "composer install" - is that correct? If you *don't* install SensioGeneratorBundle (--no-dev), then you *must* run all app/console commands with --env=prod. You cannot use the "dev" environment at all anymore - as SensioGeneratorBundle needs to be included in AppKernel in dev. When you run composer install, it runs a few app/console commands *for* you, and it runs them in the dev environment by default. To make these commands run in the "prod" environment, you can run "export SYMFONY_ENV=prod" to set an environment variable. More details here: http://symfony.com/doc/current...

    For me, this is enough of a headache that I usually *do* install dev dependencies on the production server.

    I hope that helps!

  • 2016-03-03 docLommy

    Hi, weaverryan ! Principally I'd like to organize my dependencies in a more meaningful way by using the require-dev key in composer.json for dev-only dependencies. As I see in the tutorial code you place the sensio/generator-bundle under require-dev and it's fine. However, if I try this in my project, my deployment explodes with errors, although I'd expect it to work, as composer install has the --dev option as default. So it should install all dependencies under require AND require-dev anyway. I guess I've just not understood it completely :/

  • 2016-02-15 Lacy Moore

    That tripped me up as well.

  • 2016-01-19 hanthuy

    Yeah, your result is really nice with line by line, but I'm not. Is there any differences? Thanks,
    {
    - note: [
    xxxxxx
    ]
    }

    Updated: I also think that's the reason why my notes doesn't appear in the next video although I added the {% block javascripts %} to show.html.twig

  • 2016-01-19 weaverryan

    Hi!

    Actually, this JSON output looks perfect! You *should* be getting exactly this after returning new Response(json_encode($data)) or new JsonResponse($data).

    Is there an issue I'm missing?

    Cheers!

  • 2016-01-19 hanthuy

    Hi,

    I don't what happened, but my output is still the same after using json_encode:

    {"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"}]}

    Any recommendations, thanks!

  • 2016-01-16 weaverryan

    Yes, good tip! I completely forgot to mention that in the Phpstorm chapter :)

  • 2016-01-16 Matt W.

    Just as an fyi, you also need PHP Annotations plugin for the Annotation auto-complete to work in PHPStorm.