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.

One of the features on our site... which doesn't work yet... is that you can up and down vote answers to a question. Eventually, when you click up or down, this will make an AJAX request to an API endpoint that we will make. That endpoint will save the vote to the database and respond with JSON that contains the new vote count so that our JavaScript can update this vote number.

We don't have a database in our app yet, but we're ready to build every other part of this feature.

Creating a JSON Endpoint

Let's start by creating a JSON API endpoint that will be hit via AJAX when a user up or down votes an answer.

We could create this in QuestionController as a new method. But since this endpoint really deals with a "comment", let's create a new controller class. Call it CommentController.

Like before, we're going to say extends AbstractController and hit tab so that PhpStorm autocompletes this and adds the use statement on top. Extending this class gives us shortcut methods... and I love shortcuts!

... lines 1 - 2
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
class CommentController extends AbstractController
{
}

Inside, create a public function. This could be called anything... how about commentVote(). Add the route above: /**, then @Route. Auto-complete the one from the Routing component so that PhpStorm adds its use statement.

For the URL, how about /comments/{id} - this will eventually be the id of the specific comment in the database - /vote/{direction}, where {direction} will either be the word up or the word down.

And because we have these two wildcards, we can add two arguments: $id and $direction. I'll start with a comment: the $id will be super important later when we have a database... but we won't use it at all right now.

... lines 1 - 6
use Symfony\Component\Routing\Annotation\Route;
... line 8
class CommentController extends AbstractController
{
/**
* @Route("/comments/{id}/vote/{direction}")
*/
public function commentVote($id, $direction)
{
... lines 16 - 25
}
}

Without a database, we'll kinda fake the logic. If $direction === 'up', then we would normally save this up-vote to the database and query for the new vote count. Instead, say $currentVoteCount = rand(7, 100).

... lines 1 - 8
class CommentController extends AbstractController
{
... lines 11 - 13
public function commentVote($id, $direction)
{
// todo - use id to query the database
// use real logic here to save this to the database
if ($direction === 'up') {
$currentVoteCount = rand(7, 100);
} else {
... line 22
}
... lines 24 - 25
}
}

The vote counts in the template are hardcoded to 6. So this will make the new vote count appear to be some random number higher than that. In the else, do the opposite: a random number between 0 and 5.

... lines 1 - 8
class CommentController extends AbstractController
{
... lines 11 - 13
public function commentVote($id, $direction)
{
// todo - use id to query the database
// use real logic here to save this to the database
if ($direction === 'up') {
$currentVoteCount = rand(7, 100);
} else {
$currentVoteCount = rand(0, 5);
}
... lines 24 - 25
}
}

Yes, this will all be much cooler when we have a database, but it will work great for our purposes.

Returning JSON?

The question now is: after "saving" the vote to the database, what should this controller return? Well it should probably return JSON... and I know that I want to include the new vote count in its data so our JavaScript can use that to update the vote number text.

So... how do we return JSON? Remember: our only job in a controller is to return a Symfony Response object. JSON is nothing more than a response whose body is a JSON string instead of HTML. So we could say: return new Response() with json_encode() of some data.

But! Instead, return new JsonResponse() - auto-complete this so that PhpStorm adds the use statement. Pass this an array with the data we want. How about a votes key set to $currentVoteCount.

... lines 1 - 5
use Symfony\Component\HttpFoundation\JsonResponse;
... lines 7 - 8
class CommentController extends AbstractController
{
... lines 11 - 13
public function commentVote($id, $direction)
{
// todo - use id to query the database
// use real logic here to save this to the database
if ($direction === 'up') {
$currentVoteCount = rand(7, 100);
} else {
$currentVoteCount = rand(0, 5);
}
return new JsonResponse(['votes' => $currentVoteCount]);
}
}

Now... you may be thinking:

Ryan! You keep saying that we must return a Response object... and you just returned something different. This is madness!

Fair point. But! If you hold Command or Ctrl and click the JsonResponse class, you'll learn that JsonResponse extends Response. This class is nothing more than a shortcut for creating JSON responses: it JSON encodes the data we pass to it and makes sure that the Content-Type header is set to application/json, which helps AJAX libraries understand that we're returning JSON data.

So... ah! Let's test out our shiny-new API endpoint! Copy the URL, open a new browser tab, paste and fill in the wildcards: how about 10 for {id} and vote "up". Hit enter. Hello JSON endpoint!

The big takeaway is this: JSON responses are nothing special.

The json() Shortcut Method

The JsonResponse class makes life easier... but we can be even lazier! Instead of new JsonResponse, just say return $this->json().

... lines 1 - 8
class CommentController extends AbstractController
{
... lines 11 - 13
public function commentVote($id, $direction)
{
... lines 16 - 24
return $this->json(['votes' => $currentVoteCount]);
}
}

That changes nothing: it's a shortcut method to create the same JsonResponse object. Easy peasy.

The Symfony Serializer

By the way, one of the "components" in Symfony is called the "Serializer", and it's really good at converting objects into JSON or XML. We don't have it installed yet, but if we did, the $this->json() would start using it to serialize whatever we pass. That wouldn't make any difference in our case with an array, but it means that you could start passing objects to $this->json(). If you want to learn more - or want to build a super-rich API - check out our tutorial about API Platform: an amazing Symfony bundle for building APIs.

Next, let's write some JavaScript that will make an AJAX call to our new endpoint. We'll also learn how to add global Javascript as well as page-specific JavaScript.

Leave a comment!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.2.5",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "easycorp/easy-log-handler": "^1.0.7", // v1.0.9
        "sensio/framework-extra-bundle": "^5.5", // v5.5.3
        "symfony/asset": "5.0.*", // v5.0.5
        "symfony/console": "5.0.*", // v5.0.4
        "symfony/debug-bundle": "5.0.*", // v5.0.4
        "symfony/dotenv": "5.0.*", // v5.0.4
        "symfony/flex": "^1.3.1", // v1.6.2
        "symfony/framework-bundle": "5.0.*", // v5.0.4
        "symfony/monolog-bundle": "^3.0", // v3.5.0
        "symfony/profiler-pack": "*", // v1.0.4
        "symfony/twig-pack": "^1.0", // v1.0.0
        "symfony/var-dumper": "5.0.*", // v5.0.4
        "symfony/webpack-encore-bundle": "^1.7", // v1.7.3
        "symfony/yaml": "5.0.*" // v5.0.4
    },
    "require-dev": {
        "symfony/profiler-pack": "^1.0" // v1.0.4
    }
}