When we click the heart icon, we need to send an AJAX request to the server that will, eventually, update something in a database to show that the we liked this article. That API endpoint also needs to return the new number of hearts to show on the page... ya know... in case 10 other people liked it since we opened the page.

In ArticleController, make a new public function toggleArticleHeart():

47 lines src/Controller/ArticleController.php
... lines 1 - 9
class ArticleController extends AbstractController
{
... lines 12 - 39
public function toggleArticleHeart($slug)
{
... lines 42 - 44
}
}

Then add the route above: @Route("/news/{slug}") - to match the show URL - then /heart. Give it a name immediately: article_toggle_heart:

47 lines src/Controller/ArticleController.php
... lines 1 - 9
class ArticleController extends AbstractController
{
... lines 12 - 36
/**
* @Route("/news/{slug}/heart", name="article_toggle_heart")
*/
public function toggleArticleHeart($slug)
{
... lines 42 - 44
}
}

I included the {slug} wildcard in the route so that we know which article is being liked. We could also use an {id} wildcard once we have a database.

Add the corresponding $slug argument. But since we don't have a database yet, I'll add a TODO: "actually heart/unheart the article!":

47 lines src/Controller/ArticleController.php
... lines 1 - 9
class ArticleController extends AbstractController
{
... lines 12 - 36
/**
* @Route("/news/{slug}/heart", name="article_toggle_heart")
*/
public function toggleArticleHeart($slug)
{
// TODO - actually heart/unheart the article!
... lines 43 - 44
}
}

Returning JSON

We want this API endpoint to return JSON... and remember: the only rule for a Symfony controller is that it must return a Symfony Response object. So we could literally say return new Response(json_encode(['hearts' => 5])).

But that's too much work! Instead say return new JsonResponse(['hearts' => rand(5, 100)]:

47 lines src/Controller/ArticleController.php
... lines 1 - 6
use Symfony\Component\HttpFoundation\JsonResponse;
... lines 8 - 9
class ArticleController extends AbstractController
{
... lines 12 - 36
/**
* @Route("/news/{slug}/heart", name="article_toggle_heart")
*/
public function toggleArticleHeart($slug)
{
// TODO - actually heart/unheart the article!
return new JsonResponse(['hearts' => rand(5, 100)]);
}
}

Tip

Or use the controller shortcut!

return $this->json(['hearts' => rand(5, 100)]);

There's nothing special here: JsonResponse is a sub-class of Response. It calls json_encode() for you, and also sets the Content-Type header to application/json, which helps your JavaScript understand things.

Let's try this in the browser first. Go back and add /heart to the URL. Yes! Our first API endpoint!

Tip

My JSON looks pretty thanks to the JSONView extension for Chrome!

Making the Route POST-Only

Eventually, this endpoint will modify something on the server - it will "like" the article. So as a best-practice, we should not be able to make a GET request to it. Let's make this route only match when a POST request is made. How? Add another option to the route: methods={"POST"}:

47 lines src/Controller/ArticleController.php
... lines 1 - 9
class ArticleController extends AbstractController
{
... lines 12 - 36
/**
* @Route("/news/{slug}/heart", name="article_toggle_heart", methods={"POST"})
*/
public function toggleArticleHeart($slug)
{
... lines 42 - 44
}
}

As soon as we do that, we can no longer make a GET request in the browser: it does not match the route anymore! Run:

./bin/console debug:router

And you'll see that the new route only responds to POST requests. Pretty cool. By the way, Symfony has a lot more tools for creating API endpoints - this is just the beginning. In future tutorials, we'll go further!

Hooking up the JavaScript & API

Our API endpoint is ready! Copy the route name and go back to article_show.js. But wait... if we want to make an AJAX request to the new route... how can we generate the URL? This is a pure JS file... so we can't use the Twig path() function!

Actually, there is a really cool bundle called FOSJsRoutingBundle that does allow you to generate routes in JavaScript. But, I'm going to show you another, simple way.

Back in the template, find the heart section. Let's just... fill in the href on the link! Add path(), paste the route name, and pass the slug wildcard set to a slug variable:

109 lines templates/article/show.html.twig
... lines 1 - 4
{% block body %}
<div class="container">
<div class="row">
<div class="col-sm-12">
<div class="show-article-container p-3 mt-4">
<div class="row">
<div class="col-sm-12">
... line 13
<div class="show-article-title-container d-inline-block pl-3 align-middle">
... lines 15 - 18
<span class="pl-2 article-details">
... line 20
<a href="{{ path('article_toggle_heart', {slug: slug}) }}" class="fa fa-heart-o like-article js-like-article"></a>
</span>
</div>
</div>
</div>
... lines 26 - 97
</div>
</div>
</div>
</div>
{% endblock %}
... lines 104 - 109

Actually... there is not a slug variable in this template yet. If you look at ArticleController, we're only passing two variables. Add a third: slug set to $slug:

48 lines src/Controller/ArticleController.php
... lines 1 - 9
class ArticleController extends AbstractController
{
... lines 12 - 22
public function show($slug)
{
... lines 25 - 30
return $this->render('article/show.html.twig', [
... line 32
'slug' => $slug,
... line 34
]);
}
... lines 37 - 46
}

That should at least set the URL on the link. Go back to the show page in your browser and refresh. Yep! The heart link is hooked up.

Why did we do this? Because now we can get that URL really easily in JavaScript. Add $.ajax({}) and pass method: 'POST' and url set to $link.attr('href'):

16 lines public/js/article_show.js
$(document).ready(function() {
$('.js-like-article').on('click', function(e) {
... lines 3 - 5
$link.toggleClass('fa-heart-o').toggleClass('fa-heart');
$.ajax({
method: 'POST',
url: $link.attr('href')
... lines 11 - 12
})
});
});

That's it! At the end, add .done() with a callback that has a data argument:

16 lines public/js/article_show.js
$(document).ready(function() {
$('.js-like-article').on('click', function(e) {
... lines 3 - 7
$.ajax({
method: 'POST',
url: $link.attr('href')
}).done(function(data) {
... line 12
})
});
});

The data will be whatever our API endpoint sends back. That means that we can move the article count HTML line into this, and set it to data.heart:

16 lines public/js/article_show.js
$(document).ready(function() {
$('.js-like-article').on('click', function(e) {
... lines 3 - 7
$.ajax({
... lines 9 - 10
}).done(function(data) {
$('.js-like-article-count').html(data.hearts);
})
});
});

Oh, and if you're not familiar with the .done() function or Promises, I'd highly recommend checking out our JavaScript Track. It's not beginner stuff: it's meant to take your JS up to the next level.

Anyways... let's try it already! Refresh! And... click! It works!

And... I have a surprise! See this little arrow icon in the web debug toolbar? This showed up as soon as we made the first AJAX request. Actually, every time we make an AJAX request, it's added to the top of this list! That's awesome because - remember the profiler? - you can click to view the profiler for any AJAX request. Yep, you now have all the performance and debugging tools at your fingertips... even for AJAX calls.

Oh, and if there were an error, you would see it in all its beautiful, styled glory on the Exception tab. Being able to load the profiler for an AJAX call is kind of an easter egg: not everyone knows about it. But you should.

I think it's time to talk about the most important part of Symfony: Fabien. I mean, services.

Leave a comment!

  • 2018-07-30 Dong Kyun Ham

    Missed it. Thanks.

  • 2018-07-30 Victor Bocharsky

    Hey Dong,

    It makes sense, because it was shown on the previous page: https://knpuniversity.com/s... - probably you missed it.

    Cheers!

  • 2018-07-29 Dong Kyun Ham

    Just realized that the line 3-5 of the javascript file is not shown anywhere on the script.
    I had to look back the video to understand why my Ajax was complaining about $link not being defined error.

  • 2018-04-30 weaverryan

    Yep, Victor is right! In fact, this API endpoint will be updated to handle *real* data in chapter 13 of the Doctrine tutorial :) https://knpuniversity.com/s...

    Cheers!

  • 2018-04-30 Victor Bocharsky

    Hey Joseph,

    Kinda difficult to implement this feature right now. This course is introduction into Symfony 4, and we know nothing about Doctrine and relations in it. Well, probably you do, but other users may do not know it well, or do not know at all. So, that's just another topic which isn't covered with this tutorial. But I think we'll implement this feature in the future tutorials when talk about more complex things. Thanks for understanding!

    Cheers!

  • 2018-04-27 Joseph

    Your tutorials are really helpful.In real project we wont make like button with random numbers because we need real data. :) If you make like real project it will be perfect for me.

  • 2018-03-21 Diego Aguiar

    No worries Tess, I like to help ;)

  • 2018-03-21 Tess Hsu

    great, yes that what i though, sorry for this stupid question

  • 2018-03-16 Diego Aguiar

    Hey Tess Hsu

    In this case it is returning a random between 5 and 100 because there is no database yet, so you can't actually know how many hearts have that given slug, in other words, Ryan just faked it.
    In the next chapters you will see how it gets implemented

    Cheers!

  • 2018-03-16 Tess Hsu

    sorry I'm new to Symfony, there is about the this part I confused;
    return new JsonResponse(['hearts' => rand(5, 100)]);

    go to Symfony doc:
    https://api.symfony.com/4.0...
    JsonResponse create(mixed $data = null, int $status = 200, array $headers = array())

    to me for understanding, JsonResponse is a default Symfony class which could return an HTTP response in JSON format

    so in this tutorial, use hearts parametre as data I understand, but for this part
    => rand(5, 100)]

    rand is get random number between 5-100
    how could it be work to get increase count heart ? It should be increase $count ++ as for my understanding ?

    thank you so much your awesome tutorial anyway

  • 2018-03-12 Victor Bocharsky

    Hey Chris,

    On KnpU we don't have too much API, so our routes for AJAX calls live in the same controller, so it's mostly like we show in this screencast, but probably it's happened historically. But you can totally split them, especially if you a have public API which is used by other clients, so it's nice to have something like "/api/" prefix in the URL for all your API calls. And this way you can organize your files as "src/Controller/ProductController.php" and "src/Controller/API/ProductController.php" - I like this way. But anyway, it's up, if you like it and if you're comfortable with it - go for it. Probably on the first stage you'll have just a few controllers, but I'd recommend you to think in advance a bit and imagine how comfortable you will be with chosen way if you have much more controllers. If it's still ok with you - then I think you chose the correct way.

    Well, I'm a bit biased, but probably I don't really know a good resource to learn about OAuth, but I think if you google you can find some good information, probably not a tutorial but some blog posts on this topic.

    If FOSOAuthServerBundle is OK for you - I think you can go with it, anyway it provides some kind of ready-to-use solution to you. Otherwise, I think you can look at low-level https://github.com/thephple... package.

    Cheers!

  • 2018-03-09 Chris

    Just a few question which I always wanted to ask.

    When you create json routes for ajax calls in one of your projects, do you store them in the same controller similiar as here in the ArticleController, where the html representation lives, or do you create a new one, specifically for Ajax calls?

    I consider to create an OAuth system for one of my projects. Therefore the folder structure is kind of an issue for me, yet.
    For api routes in conjunction with Oauth, I would create routes with prefix /api in a newly created folder called Api , e.g. with subfolders ProductController, ... . This location (/src/Controller/Api/ProductController) is only assigned to api requests where client credentials are needed.

    And the json routes used for ajax calls for my homepage and the html representation are alltogether stored in a seperate /src/Controller/ProductController directory? Is this how it should be done, the right way?

    Btw, do you guys know any good resource to learn OAuth, to secure my api requests, where users have to submit client credentials? I already watched your full Rest series and the OAuth tutorial, but I want to build a system like that. In the OAuth tutorial we were on the other side, trying to get access to such a system. Is it possible to create my own OAuth system similiar to your Rest series or shall I simply rely on the FOSOAuthServerBundle?

    Even if it`s a bit off topic, honestly this is my favorite site to learn programming, and I hope you can help me :)

    Cheers

  • 2018-01-25 Mert Simsek

    It sounds good. I see, thank you for replying :)

  • 2018-01-25 weaverryan

    Yo Mert Simsek!

    Great question! Um.... both! ;)

    1) It's no longer needed. Well, even in Symfony 3, it was not needed with annotations, but it was needed if you used YAML routing (there was a workaround, but it was ugly). But in Symfony 4, even in YAML, it's not needed.

    2) Because of that, the Symfony core team decided to stop recommending it and just allow people to have short names. You already can easily know which methods in your controller are "controller actions": all public functions (if you have a public function that you consider to *not* be an action, then it shouldn't live in the controller!).

    I hope that clarifies!

    Cheers!

  • 2018-01-25 Victor Bocharsky

    Hey Ahmed,

    Looks like you have learnt a lot with us so far, well done! ;)

    Cheers, and have a good day too!

  • 2018-01-24 Ahmed Bhs

    Thank you for replying, I thought maybe I missed something, that's why I asked about this video. Again, thank you, I really appreciate, and I really enjoyed learning the trick with symfony 4.

    Cheers, have a good day :)

  • 2018-01-24 Mert Simsek

    Do we do not need to add a controller's method 'action' keyword end of method's name? Or no longer recommended?

  • 2018-01-24 Victor Bocharsky

    Hey Ahmed,

    Are you talking about exact this video? Probably nothing much if you have seen our old tutorials. Well, the same except now we're doing these changes on Symfony 4 and in a different way of course. And if you noticed it - probably nothing much were changed in Symfony 4 in *this* process, right? And I think it's good, like it's still solid. Anyway, we need to make these changes to move forward in this tutorial, but I agree, these changes you can do on Symfony 3.x, and probably even on Symfony 2.x as well, so this process remains the same, good catch!

    Cheers!

  • 2018-01-23 AdFlorin

    now i'm waiting for Advanced APi tutorial :)

  • 2018-01-23 Diego Aguiar

    Hey AdFlorin

    This chapter is released! I thought you would like to know it :)

    Cheers!

  • 2018-01-23 Ahmed Bhs

    Thank you for the tutorial, i really appreciate your hard work,
    What's the new feature introduced here?. I mean creating an endpoint and using fosJsRoutingBundle are already introduced in the old version of Symfony!!

  • 2018-01-23 AdFlorin

    waiting ... chilling :D

  • 2018-01-22 weaverryan

    You're right Stéphane! I've already fixed that - https://github.com/knpunive... - thanks for letting me know!

    Cheers!

  • 2018-01-22 Stéphane

    I thing there is an error with the name of the route. You miss the "s"
    @Route("/news/{slug}")