Chapters
JSON Responses + Route Generation
Scroll down to the script below, click on any sentence (including terminal blocks) to jump to that spot in the video!
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")
:
Show Lines
|
// ... lines 1 - 9 |
class GenusController extends Controller | |
{ | |
Show Lines
|
// ... lines 12 - 21 |
/** | |
* @Route("/genus/{genusName}/notes") | |
* @Method("GET") | |
*/ | |
public function getNotesAction($genusName) | |
{ | |
Show Lines
|
// ... 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:
Show Lines
|
// ... lines 1 - 4 |
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method; | |
Show Lines
|
// ... 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:
Show Lines
|
// ... lines 1 - 9 |
class GenusController extends Controller | |
{ | |
Show Lines
|
// ... lines 12 - 14 |
public function showAction($genusName) | |
{ | |
return $this->render('genus/show.html.twig', array( | |
'name' => $genusName, | |
)); | |
} | |
Show Lines
|
// ... lines 21 - 38 |
} |
In the new controller, I'll paste a new $notes
variable set to some beautiful data:
Show Lines
|
// ... lines 1 - 9 |
class GenusController extends Controller | |
{ | |
Show Lines
|
// ... 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'], | |
]; | |
Show Lines
|
// ... 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:
Show Lines
|
// ... 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 | |
]; | |
Show Lines
|
// ... lines 36 - 40 |
Now, how do we finally return $data
as JSON? Simple: return new Response()
and pass it json_encode($data)
:
Show Lines
|
// ... lines 1 - 9 |
class GenusController extends Controller | |
{ | |
Show Lines
|
// ... lines 12 - 25 |
public function getNotesAction($genusName) | |
{ | |
Show Lines
|
// ... 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
:
Show Lines
|
// ... lines 1 - 7 |
use Symfony\Component\HttpFoundation\JsonResponse; | |
Show Lines
|
// ... lines 9 - 10 |
class GenusController extends Controller | |
{ | |
Show Lines
|
// ... lines 13 - 26 |
public function getNotesAction($genusName) | |
{ | |
Show Lines
|
// ... 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.
55 Comments
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?
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!
Would've been better to mention it, thought I made a mistake.
Nice pugin. I've already thought that I made a mistake :D
Thanks for asking, i had the same is question (y)
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.
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!
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 :/
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!
It's the same for me. The method is ANY instead of GET.
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!
Just as an fyi, you also need PHP Annotations plugin for the Annotation auto-complete to work in PHPStorm.
Yes, good tip! I completely forgot to mention that in the Phpstorm chapter :)
That tripped me up as well.
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 :/
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!
Hi! How i can set JSON_UNESCAPED_UNICODE and JSON_UNESCAPED_SLASHES to JsonResponse?
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!
Nice! Thank you!
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!
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
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
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));
}
}
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.
Hmm, your routes looks good to me, can you show me the exception and stack trace please ?
Luckly I don't have it anymore because is working now, but the error message was about missing route to genusname/GET
Hey Josk,
Ah, probably the problem was in cache, or just a simple misprint. Anyway, glad you got it working!
Cheers!
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
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!
I have the same problem and it doesn't help me...
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!
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.
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!
Hi, Victor
Excellent! Thank you!
Just minor note, I think cache:clear instead of clear:cache. It works now. Thanks! =)
Ah, yes! it's my fault... Thanks for noticing me, I'll edit my comment )
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 )?
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.
I think this is actually normal, because in the video we deleted the variable called $notes from the showAction method.
Why use $data = ['notes' => $notes]; instead of just returning notes???
Sorry if is a stupid question. I had never usen Json before.
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!
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!
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!
I had same output as hanthuy had too , but after installing JSONView on firefox all is fine!
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
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".
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
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!
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
Yes, awesome! If you have any questions, let me know!
Keep up the good work!
"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
}
}
nice work bro keep up (y) :)