gstreamer0.10-ffmpeg
gstreamer0.10-plugins-good
packages.
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.
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.
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 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.
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.
Hey Jack,
Of course! If you don't use that class directly in your controller - you're free to remove it completely. If that namespace is used in that "json()" method - it should be imported in that file instead, that is already do. So, literally just remove it. But of course, it's always to good idea to check if the route is still accessible after it, or even better have an automated tests that cover those routes and will check it for you ;)
Cheers!
OMG - I want to do that now!!
In truth... sometimes I try to think of different things to say - adding this one to my list.
// composer.json
{
"require": {
"php": "^7.3.0 || ^8.0.0",
"ext-ctype": "*",
"ext-iconv": "*",
"easycorp/easy-log-handler": "^1.0.7", // v1.0.9
"sensio/framework-extra-bundle": "^6.0", // v6.2.1
"symfony/asset": "5.0.*", // v5.0.11
"symfony/console": "5.0.*", // v5.0.11
"symfony/debug-bundle": "5.0.*", // v5.0.11
"symfony/dotenv": "5.0.*", // v5.0.11
"symfony/flex": "^1.3.1", // v1.17.5
"symfony/framework-bundle": "5.0.*", // v5.0.11
"symfony/monolog-bundle": "^3.0", // v3.5.0
"symfony/profiler-pack": "*", // v1.0.5
"symfony/routing": "5.1.*", // v5.1.11
"symfony/twig-pack": "^1.0", // v1.0.1
"symfony/var-dumper": "5.0.*", // v5.0.11
"symfony/webpack-encore-bundle": "^1.7", // v1.8.0
"symfony/yaml": "5.0.*" // v5.0.11
},
"require-dev": {
"symfony/profiler-pack": "^1.0" // v1.0.5
}
}
After using
$this→json
to return the JSON, my code editor is sayinguse Symfony\Component\HttpFoundation\JsonResponse;
is declared but not used. Is it safe to take it out?