Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

JSON API Endpoint

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.

In a future tutorial, we're going to create a database to manage songs, genres, and the mixed vinyl records that our users are creating. Right now, we're working entirely with hardcoded data... but our controllers - and - especially templates won't feel that much different once we make this all dynamic.

So here's our new goal: I want to create an API endpoint that will return the data for a single song as JSON. We're going to use this in a few minutes to bring this play button to life. At the moment, none of these buttons do anything, but they do look pretty.

Creating the JSON Controller

The two steps to creating an API endpoint are... exactly the same as creating an HTML page: we need a route and a controller. Since this API endpoint will be returning song data, instead of adding another method inside of VinylController, let's create a totally new controller class. How you organize this stuff is entirely up to you.

Create a new PHP class called SongController... or SongApiController would also be a good name. Inside, this will start like any other controller, by extending AbstractController. Remember: that's optional... but it gives us shortcut methods with no downside.

Next, create a public function called, how about, getSong(). Add the route... and hit tab to auto-complete this so that PhpStorm adds the use statement on top. Set the URL to /api/songs/{id}, where id will eventually be the database id of the song.

And because we have a wildcard in the route, we are allowed to have an $id argument. Finally, even though we don't need to do this, because we know that our controller will return a Response object, we can set that as the return type. Make sure to auto-complete the one from Symfony's HttpFoundation component.

Inside the method, to start, dd($id)... just to see if everything is working.

<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class SongController extends AbstractController
{
#[Route('/api/songs/{id}')]
public function getSong($id): Response
{
dd($id);
}
}

Let's do this! Head over to /api/songs/5 and... got it! Another new page.

Back in that controller, I'm going to paste in some song data: eventually, this will come from the database. You can copy this from the code block on this page. Our job is to return this as JSON.

So how do we return JSON in Symfony? By returning a new JsonResponse and passing it the data.

<?php
... lines 3 - 5
use Symfony\Component\HttpFoundation\JsonResponse;
... lines 7 - 9
class SongController extends AbstractController
{
#[Route('/api/songs/{id}')]
public function getSong($id): Response
{
// TODO query the database
$song = [
'id' => $id,
'name' => 'Waterfalls',
'url' => 'https://symfonycasts.s3.amazonaws.com/sample.mp3',
];
return new JsonResponse($song);
}
}

I know... too easy! Refresh and... hello JSON! Now you might be thinking:

Ryan! You've been telling us - repeatedly - that a controller must all always return a Symfony Response object, which is what render() returns. Now you're returning some other type of Response object?

Ok, fair... but this works because JsonResponse is a Response. Let me explain. Sometimes it's useful to jump into core classes to see how they work. To do that, in PHPStorm - if you're on a Mac hold command, otherwise hold control - and then click the class name that you want to jump into. And... surprise! JsonResponse extends Response. Yea, we're still returning a Response. But this sub-class is nice because it automatically JSON encodes our data and sets the Content-Type header to application/json.

The ->json() Shortcut Method

Oh, and back in our controller, we can be even lazier by saying return $this->json($song)... where json() is another shortcut method that comes from AbstractController.

<?php
... lines 3 - 9
class SongController extends AbstractController
{
#[Route('/api/songs/{id}')]
public function getSong($id): Response
{
... lines 15 - 20
return $this->json($song);
}
}

Doing this makes absolutely no difference because this is just a shortcut to return ... a JsonResponse!

If you're building a serious API, Symfony has a serializer component that's really good at turning objects into JSON... and then JSON back into objects. We talk a lot about it in our API Platform tutorial, which is a powerful library for creating APIs in Symfony.

Next, let's learn how to make our routes smarter, like by making a wildcard only match a number, instead of matching anything.

Leave a comment!

23
Login or Register to join the conversation
FHMJ Avatar

Decided to have a look at the example url for the JSON endpoint...

I'm not even mad

4 Reply

I'm glad you did :D

1 Reply
Albert-W Avatar

Same here

Reply
Artun Avatar

I am getting the Json response like this:
{"id":"5","name":"Waterfalls","url":"https:\/\/symfonycasts.s3.amazonaws.com\/sample.mp3"}

Even when I copy and paste entire controller, it doesn't change

Reply

Hey @Artun!

Actually, that looks perfect to me! It looks a bit different than what you see in the video because I have a browser plugin that makes it look fancy (my bad - I should have mentioned that). In JSON, having quotes around the keys = like "id" is correct. In the video, that browser plugin is hiding those quotes to make it look prettier. But your response is exactly what we want :).

Cheers!

1 Reply
Artun Avatar

Hey @weaverryan!
Thanks for your reply.So I am assuming the backslaches in the url is also expected? instead of "https://symfonycasts.s3.amazonaws.com/sample.mp3&quot;
it shows as
"https:\/\/symfonycasts.s3.amazonaws.com\/sample.mp3"

Reply

Yes @Artun, the "JSON encode" function takes care of escaping any reserved character, for example, if you have double quotes in your data, those will be escaped too

Cheers!

Reply
Benanamen Avatar
Benanamen Avatar Benanamen | posted 5 months ago

This lesson did not work for me as instructed. I needed to add the following in order for it to work.

use Symfony\Component\HttpFoundation\JsonResponse;

Reply

Thank you once again, I just fixed the code block in the script. Cheers!

Reply

good Tutorials, I come from Laravel and everything make sense its more lighter then laravel or lumen but when I saw you how you created Controller file I thought there should be easier way something like command "php artisan make:controller" to do, what I found there is "php bin/console make:crontroller" it requires "maker" you should show this too it makes your life easier

Reply

Hey Engazan,

Actually, there's an easier way for controller, you need to have Maker bundle installed and then as you said you can call:

bin/console make:controller

To be able to use the Maker bundle (if you don't have one) - first, you need to execute:

composer require maker

We wanted to show the strightforawrd way focusing the attention on no magic, you just need to create a simple PHP class in the specific folder. We will show the way with Maker in future tutorials too. ;)

Cheers!

1 Reply

Hi again, Ryan.

I just ran into another issue, and this time I made sure that the code in my controller doesn't miss anything.

It looks like my Symfony is somehow ignoring the fact that I created a brand-new route, because upon trying to open it in the browser, the framework is throwing a 404 Exception: No route found for "GET https://127.0.0.1:8000/api/songs/5".

Moreover, by issuing the bin/console debug:router command, the new route isn't showing up. Clearing up the caches and restarting the server didn't make any difference either.

Here's my SongController code:


namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class SongController extends AbstractController
{
    #[Route('/api/songs/{id}')]

    public function getSong($id): Response
    {
        dd($id);
    }

What's happening? Any ideas?

Thank you so much.

Reply

Hey @roenfeldt!

Hmm.

Moreover, by issuing the bin/console debug:router command, the new route isn't showing up

That was a great thing to try for debugging. And while you shouldn't need to clear the cache ever, when you hit a super weird issue it's always a good thing to try as well. So nice job there.

So, why isn't this working? First, remove that line break between the #[Route] and the public function. I don't think that's the issue (I had never tried it before, but having a line break doesn't seem to cause any issues) - but better to remove it. To other things to check, which will sound crazy, but there is very little else that could be going wrong: (A) make sure you have the <?php at the start of your file (I have seen this many times and it's super hard to debug) and (B) triple-check that you're inside of the src/Controller/ directory.

The mechanism for loading these attribute routes is pretty simple: in your config/routes.yaml directory, there is a line there that says

look at all php files in src/Controller and parse their #[Route] attributes.

It's these 3 lines - https://github.com/symfony/recipes/blob/main/symfony/routing/6.1/config/routes.yaml#L1-L3 . So, there's not a lot that can go wrong as long as you have a valid php file in the src/Controller/ directory (and the contents of your class - apart from the potential missing <?php look perfect).

Let me know if any of this helps :).

Cheers!

1 Reply

Hi Ryan,

First off, thank you for the fast reply, and thank you for the very nice explanation!

With the help of your suggestions, I realized that PhpStorm created the file into the wrong folder. My SongController was created inside src/App/Controller. How did that happen? Well, the error was, once again, on the exterior of the screen, of course :)

Because I learned my lesson from the previous time (the Controller's namespace was missing the App\ part), in order to prevent that from happening again, I used PhpStorm's New > PHP Class dialog (just like you did in the video), but as an extra step, I updated the Namespace field by prepending App\ to the existing Controller value. And then I pressed Return, of course without realizing that PhpStorm will create the wrongful folder structure based on what I chose in there.

If I didn't do that, the path for the new Controller would have been correct, but then the namespace inside it would have caused Symfony to throw the Controller (...) has no container set, did you forget to define it as a service subscriber? 500 Exception.

To conclude, your suggestions put me back on track, for which I am grateful. But my question now is, how would you suggest I'd handle this problem, because I need PhpStorm to automatically prepend App\ to the namespace of any new Controller I'd be creating, just like it does in your video?

Thank you so much, Ryan! :)

Reply

Hey @roenfeldt!

Yea, the "New -> PHP Class" dialog is great - and it is definitely NOT working correctly for you. In order for that to work, PhpStorm needs to know which namespace lives in which directory. In a Symfony app, the App\ namespace lives in src. This convention isn't hardcoded into Symfony - it's actually a setting in your composer.json file: https://github.com/symfony/skeleton/blob/948939fb8ac6b1ab38547bff6f386bca3a782ed4/composer.json#L35-L39

For the last 2-ish years, PHPStorm should automatically read this setting and use that when you create classes. For some reason that's not happening, or it's reading it incorrectly. Or, that feature just isn't activated for some reason. Go into your PHPStorm settings. Inside, go to PHP -> Composer and look for a check box that says Synchronize IDE Settings with composer.json - I believe this needs to be activated.

Let me know if that helps :).

Cheers!

1 Reply

Hey Ryan,

Enabling the Synchronize IDE Settings with composer.jsoncheckbox did the trick. Yay! :D

Thank you for clarifying this out for me!

Cheers! :)

Reply
Cat in space

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

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.0.2",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "symfony/asset": "6.0.*", // v6.0.3
        "symfony/console": "6.0.*", // v6.0.3
        "symfony/dotenv": "6.0.*", // v6.0.3
        "symfony/flex": "^2", // v2.1.5
        "symfony/framework-bundle": "6.0.*", // v6.0.4
        "symfony/monolog-bundle": "^3.0", // v3.7.1
        "symfony/runtime": "6.0.*", // v6.0.3
        "symfony/twig-bundle": "6.0.*", // v6.0.3
        "symfony/ux-turbo": "^2.0", // v2.0.1
        "symfony/webpack-encore-bundle": "^1.13", // v1.13.2
        "symfony/yaml": "6.0.*", // v6.0.3
        "twig/extra-bundle": "^2.12|^3.0", // v3.3.8
        "twig/twig": "^2.12|^3.0" // v3.3.8
    },
    "require-dev": {
        "symfony/debug-bundle": "6.0.*", // v6.0.3
        "symfony/stopwatch": "6.0.*", // v6.0.3
        "symfony/web-profiler-bundle": "6.0.*" // v6.0.3
    }
}
userVoice