Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
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!

55
Login or Register to join the conversation
Default user avatar
Default user avatar zs Zoubair | posted 5 years ago

nice work bro keep up (y) :)

3 Reply
Default user avatar
Default user avatar Konrad Zając | posted 5 years ago

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?

2 Reply

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!

7 Reply
Default user avatar

Would've been better to mention it, thought I made a mistake.

Reply
Default user avatar

Nice pugin. I've already thought that I made a mistake :D

Reply
Default user avatar

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

Reply
Default user avatar
Default user avatar Antonio Iba | posted 5 years ago

To work just fine, I had to add after the namespace "use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;" and "use Symfony\Component\HttpFoundation\JsonResponse;".
I suppose it's the fault of my IDE but everthing else was working just fine.

1 Reply

Hey Antonio,

I suppose you're right, it's due to the IDE. You have to allow your IDE *autocomplete* the class name, i.e. you need to write a part of the class name and then choose it from the drop-down list. Then your IDE should add this namespace after the "namespace" declaration for you.

Cheers!

Reply
Sammy F. Avatar
Sammy F. Avatar Sammy F. | posted 4 years ago

can it be that this track isn't valid for 3.4?

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;

is deprecated, and bin/console Debug:router doesn't show GET as method for the route :/

Reply

Hey Sammy F.

This course is totally valid for Symfony 3.4 but you will see more deprecated messages.

> bin/console Debug:router doesn't show GET as method for the route
What do you see when executing it?

Cheers!

Reply
Default user avatar

It's the same for me. The method is ANY instead of GET.

Reply

Hey Jon,

Try to set the method on "@Route" annotation, like:


/**
 * @Route("/your-route-path", name="your_route_name", methods={"GET"})
 */

Then, clear the cache and rerun the same command:

$ bin/console debug:router

Does it works for you? Do you see the GET instead of ANY now?

Cheers!

1 Reply
Default user avatar
Default user avatar Matt W. | posted 5 years ago

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

Reply

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

1 Reply
Default user avatar

That tripped me up as well.

Reply
Default user avatar
Default user avatar docLommy | posted 5 years ago

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 :/

Reply

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/curr...

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

I hope that helps!

1 Reply
Default user avatar
Default user avatar Yurii Romanov | posted 5 years ago

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

Reply

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/symfony/blob/1b6b08cf0babd52390ed50f95648f1cf58f8f67d/src/Symfony/Component/HttpFoundation/JsonResponse.php#L32

Hope that helps!

1 Reply
Default user avatar
Default user avatar Yurii Romanov | weaverryan | posted 5 years ago

Nice! Thank you!

Reply
weaverryan Avatar weaverryan | SFCASTS | posted 5 years ago | edited

Yo Dan!

Welcome to Symfony :). I can definitely help you to start debugging this guy! This error means that Symfony looked at all of your routes (i.e. your @Route annotations), but could not find a route that matched the URL /genus/octopus/notes. So probably, there is a problem with the @Route annotation that we added here: https://knpuniversity.com/screencast/symfony/json-api#creating-api-endpoints

Also, it might be helpful to see what routes Symfony does know of. Try running this command:


php bin/console debug:router

Do you see a route whose path is /genus/octopus/notes (probably not, based on the error). This output might help you know what routes Symfony is (and is not) seeing!

Let us know if this helps! Cheers!

Reply
Default user avatar

Hi, after the modification for creating the second page, when I go to the first page I get this error
$message .= sprintf(' (from "%s")', $referer); } throw new NotFoundHttpException($message, $e); } catch (MethodNotAllowedException $e) { $message = sprintf('No route found for "%s %s": Method Not Allowed (Allow: %s)', $request->getMethod(), $request->getPathInfo(), implode(', ', $e->getAllowedMethods()));

I did the command to clear the cache, but It's still not working.
No error for the second page with json

Reply

Hey Josk

When a "MethodNotAllowedException" is thrown it's because you are trying to access a route with an invalid method, like trying to do a POST request to the "/genus/{genusName}/notes" route
Can you show me how looks like your GenusController ?

Have a nice day

Reply
Default user avatar

Ok, below there is the whole code of the file GenusController.php. Thanks in advance.

namespace AppBundle\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;

class GenusController extends Controller
{
/**
* @Route("/genus/{genusName}")
*/
public function showAction($genusName)
{

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

/**
* @Route("/genus/{genusName}/notes")
* @Method("GET")
*/

public function getNotesAction()
{
$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,
];

return new Response(json_encode($data));

}

}

Reply
Default user avatar

I don't know why, but now is working. I've just cut the section notes in the show.html.twig. I did the same before, but the previous time I deleted also the empty row between the two section tag (as shown in the video). The empty row is the only thing I made different, but maybe I made some other errors when deleting that row.
Now it's fine, thanks for the quick reply.

1 Reply

Hmm, your routes looks good to me, can you show me the exception and stack trace please ?

Reply
Default user avatar

Luckly I don't have it anymore because is working now, but the error message was about missing route to genusname/GET

Reply

Hey Josk,

Ah, probably the problem was in cache, or just a simple misprint. Anyway, glad you got it working!

Cheers!

1 Reply
Default user avatar

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

Reply
Victor Avatar Victor | SFCASTS | Viktor | posted 5 years ago | edited

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!

1 Reply
Default user avatar
Default user avatar Zoé Belleton | Victor | posted 5 years ago

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

1 Reply

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!

4 Reply
Default user avatar
Default user avatar Nobuyuki Fujioka | Victor | posted 5 years ago

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.

Reply

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!

2 Reply
Default user avatar
Default user avatar Nobuyuki Fujioka | Victor | posted 5 years ago

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

Reply

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

Reply
Default user avatar
Default user avatar Zoé Belleton | Victor | posted 5 years ago

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 )?

Reply

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.

Reply
Default user avatar
Default user avatar Oscar Galván | Victor | posted 5 years ago

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

2 Reply
Default user avatar
Default user avatar BondashMaster | posted 5 years ago

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

Reply

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!

1 Reply
Default user avatar
Default user avatar Dale Nguyen | posted 5 years ago

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!

Reply

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!

Reply
Default user avatar
Default user avatar Andrew Grudin | weaverryan | posted 5 years ago

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

Reply
Default user avatar

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

Reply
Default user avatar

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".

Reply
Default user avatar
Default user avatar Nobuyuki Fujioka | posted 5 years ago

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

Reply

Hey Noby!

Sure! I typically use https://github.com/nelmio/N.... 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!

Reply
Default user avatar
Default user avatar Nobuyuki Fujioka | weaverryan | posted 5 years ago

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

Reply

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

Keep up the good work!

Reply
Cat in space

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

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
        "composer/package-versions-deprecated": "^1.11" // 1.11.99
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.0.7
        "symfony/phpunit-bridge": "^3.0" // v3.1.3
    }
}
userVoice