Buy

Updating an Entity

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

In the previous tutorial, we created our heart feature! You click on the heart, it makes an Ajax request back to the server, and returns the new number of hearts. It's all very cute. In theory... when we click the heart, it would update the number of "hearts" for this article somewhere in the database.

But actually, instead of updating the database... well... it does nothing, and returns a new, random number of hearts. Lame!

Look in the public/js directory: open article_show.js:

16 lines public/js/article_show.js
$(document).ready(function() {
$('.js-like-article').on('click', function(e) {
e.preventDefault();
var $link = $(e.currentTarget);
$link.toggleClass('fa-heart-o').toggleClass('fa-heart');
$.ajax({
method: 'POST',
url: $link.attr('href')
}).done(function(data) {
$('.js-like-article-count').html(data.hearts);
})
});
});

In that tutorial, we wrote some simple JavaScript that said: when the "like" link is clicked, toggle the styling on the heart, and then send a POST request to the URL that's in the href of the link. Then, when the AJAX call finishes, read the new number of hearts from the JSON response and update the page.

The href that we're reading lives in show.html.twig. Here it is:

86 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 - 20
<span class="pl-2 article-details">
... line 22
<a href="{{ path('article_toggle_heart', {slug: article.slug}) }}" class="fa fa-heart-o like-article js-like-article"></a>
</span>
</div>
</div>
</div>
... lines 28 - 73
</div>
</div>
</div>
</div>
{% endblock %}
... lines 80 - 86

It's a URL to some route called article_toggle_heart. And we're sending the article slug to that endpoint.

Open up ArticleController, and scroll down to find that route: it's toggleArticleHeart():

74 lines src/Controller/ArticleController.php
... lines 1 - 16
class ArticleController extends AbstractController
{
... lines 19 - 61
/**
* @Route("/news/{slug}/heart", name="article_toggle_heart", methods={"POST"})
*/
public function toggleArticleHeart($slug, LoggerInterface $logger)
{
// TODO - actually heart/unheart the article!
$logger->info('Article is being hearted!');
return new JsonResponse(['hearts' => rand(5, 100)]);
}
}

And, as you can see... this endpoint doesn't actually do anything! Other than return JSON with a random number, which our JavaScript uses to update the page:

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);
})
});
});

Updating the heartCount

It's time to implement this feature correctly! Or, at least, more correctly. And, for the first time, we will update an existing row in the database.

Back in ArticleController, we need to use the slug to query for the Article object. But, remember, there's a shortcut for this: replace the $slug argument with Article $article:

75 lines src/Controller/ArticleController.php
... lines 1 - 4
use App\Entity\Article;
... lines 6 - 16
class ArticleController extends AbstractController
{
... lines 19 - 61
/**
* @Route("/news/{slug}/heart", name="article_toggle_heart", methods={"POST"})
*/
public function toggleArticleHeart(Article $article, LoggerInterface $logger, EntityManagerInterface $em)
{
... lines 67 - 72
}
}

Thanks to the type-hint, Symfony will automatically try to find an Article with this slug.

Then, to update the heartCount, just $article->setHeartCount() and then $article->getHeartCount() + 1:

75 lines src/Controller/ArticleController.php
... lines 1 - 16
class ArticleController extends AbstractController
{
... lines 19 - 61
/**
* @Route("/news/{slug}/heart", name="article_toggle_heart", methods={"POST"})
*/
public function toggleArticleHeart(Article $article, LoggerInterface $logger, EntityManagerInterface $em)
{
$article->setHeartCount($article->getHeartCount() + 1);
... lines 68 - 72
}
}

Side note, it's not important for this tutorial, but in a high-traffic system, this could introduce a race condition. Between the time this article is queried for, and when it saves, 10 other people might have also liked the article. And that would mean that this would actually save the old, wrong number, effectively removing the 10 hearts that occurred during those microseconds.

Anyways, at the bottom, instead of the random number, use $article->getHeartCount():

75 lines src/Controller/ArticleController.php
... lines 1 - 16
class ArticleController extends AbstractController
{
... lines 19 - 61
/**
* @Route("/news/{slug}/heart", name="article_toggle_heart", methods={"POST"})
*/
public function toggleArticleHeart(Article $article, LoggerInterface $logger, EntityManagerInterface $em)
{
$article->setHeartCount($article->getHeartCount() + 1);
... lines 68 - 71
return new JsonResponse(['hearts' => $article->getHeartCount()]);
}
}

So, now, to the key question: how do we run an UPDATE query in the database? Actually, it's the exact same as inserting a new article. Fetch the entity manager like normal: EntityManagerInterface $em:

75 lines src/Controller/ArticleController.php
... lines 1 - 8
use Doctrine\ORM\EntityManagerInterface;
... lines 10 - 16
class ArticleController extends AbstractController
{
... lines 19 - 61
/**
* @Route("/news/{slug}/heart", name="article_toggle_heart", methods={"POST"})
*/
public function toggleArticleHeart(Article $article, LoggerInterface $logger, EntityManagerInterface $em)
{
... lines 67 - 72
}
}

Then, after updating the object, just call $em->flush():

75 lines src/Controller/ArticleController.php
... lines 1 - 16
class ArticleController extends AbstractController
{
... lines 19 - 61
/**
* @Route("/news/{slug}/heart", name="article_toggle_heart", methods={"POST"})
*/
public function toggleArticleHeart(Article $article, LoggerInterface $logger, EntityManagerInterface $em)
{
$article->setHeartCount($article->getHeartCount() + 1);
$em->flush();
$logger->info('Article is being hearted!');
return new JsonResponse(['hearts' => $article->getHeartCount()]);
}
}

But wait! I did not call $em->persist($article). We could call this... it's just not needed for updates! When you query Doctrine for an object, it already knows that you want that object to be saved to the database when you call flush(). Doctrine is also smart enough to know that it should update the object, instead of inserting a new one.

Ok, go back and refresh! Here is the real heart count for this article: 88. Click the heart and... yea! 89! And if you refresh, it stays! We can do 90, 91, 92, 93, and forever! And yea... this is not quite realistic yet. On a real site, I should only be able to like this article one time. But, we'll need to talk about users and security before we can do that.

Smarter Entity Method

Now that this is working, we can improve it! In the controller, we wrote some code to increment the heart count by one:

75 lines src/Controller/ArticleController.php
... lines 1 - 16
class ArticleController extends AbstractController
{
... lines 19 - 61
/**
* @Route("/news/{slug}/heart", name="article_toggle_heart", methods={"POST"})
*/
public function toggleArticleHeart(Article $article, LoggerInterface $logger, EntityManagerInterface $em)
{
$article->setHeartCount($article->getHeartCount() + 1);
... lines 68 - 72
}
}

But, whenever possible, it's better to move code out of your controller. Usually we do this by creating a new service class and putting the logic there. But, if the logic is simple, it can sometimes live inside your entity class. Check this out: open Article, scroll to the bottom, and add a new method: public function incrementHeartCount(). Give it no arguments and return self, like our other methods:

156 lines src/Entity/Article.php
... lines 1 - 9
class Article
{
... lines 12 - 131
public function incrementHeartCount(): self
... lines 133 - 154
}

Then, $this->heartCount = $this->heartCount + 1:

156 lines src/Entity/Article.php
... lines 1 - 131
public function incrementHeartCount(): self
{
$this->heartCount = $this->heartCount + 1;
return $this;
}
... lines 138 - 156

Back in ArticleController, we can simplify to $article->incrementHeartCount():

75 lines src/Controller/ArticleController.php
... lines 1 - 16
class ArticleController extends AbstractController
{
... lines 19 - 61
/**
* @Route("/news/{slug}/heart", name="article_toggle_heart", methods={"POST"})
*/
public function toggleArticleHeart(Article $article, LoggerInterface $logger, EntityManagerInterface $em)
{
$article->incrementHeartCount();
... lines 68 - 72
}
}

That's so nice. This moves the logic to a better place, and, it reads really well:

Hello Article: I would like you to increment your heart count. Thanks!

Smart Versus Anemic Entities

And... this touches on a somewhat controversial topic related to entities. Notice that every property in the entity has a getter and setter method. This makes our entity super flexible: you can get or set any field you need.

But, sometimes, you might not need, or even want a getter or setter method. For example, do we really want a setHeartCount() method?

154 lines src/Entity/Article.php
... lines 1 - 9
class Article
{
... lines 12 - 124
public function setHeartCount(int $heartCount): self
{
$this->heartCount = $heartCount;
return $this;
}
... lines 131 - 152
}

I mean, should any part of the app ever need to change this? Probably not: they should just call our more descriptive incrementHeartCount() instead:

156 lines src/Entity/Article.php
... lines 1 - 124
public function setHeartCount(int $heartCount): self
{
$this->heartCount = $heartCount;
return $this;
}
... lines 131 - 156

I am going to keep it, because we use it to generate our fake data, but I want you to really think about this point.

By removing unnecessary getter or setter methods, and replacing them with more descriptive methods that fit your business logic, you can, little-by-little, give your entities more clarity. Some people take this to an extreme and have almost zero getters and setters. Here at KnpU, we tend to be more pragmatic: we usually have getters and setters, but we always look for ways to be more descriptive.

Next, our dummy article data is boring, and we're creating it in a hacky way:

61 lines src/Controller/ArticleAdminController.php
... lines 1 - 10
class ArticleAdminController extends AbstractController
{
/**
* @Route("/admin/article/new")
*/
public function new(EntityManagerInterface $em)
{
$article = new Article();
$article->setTitle('Why Asteroids Taste Like Bacon')
->setSlug('why-asteroids-taste-like-bacon-'.rand(100, 999))
->setContent(<<<EOF
Spicy **jalapeno bacon** ipsum dolor amet veniam shank in dolore. Ham hock nisi landjaeger cow,
lorem proident [beef ribs](https://baconipsum.com/) aute enim veniam ut cillum pork chuck picanha. Dolore reprehenderit
labore minim pork belly spare ribs cupim short loin in. Elit exercitation eiusmod dolore cow
**turkey** shank eu pork belly meatball non cupim.
Laboris beef ribs fatback fugiat eiusmod jowl kielbasa alcatra dolore velit ea ball tip. Pariatur
laboris sunt venison, et laborum dolore minim non meatball. Shankle eu flank aliqua shoulder,
capicola biltong frankfurter boudin cupim officia. Exercitation fugiat consectetur ham. Adipisicing
picanha shank et filet mignon pork belly ut ullamco. Irure velit turducken ground round doner incididunt
occaecat lorem meatball prosciutto quis strip steak.
Meatball adipisicing ribeye bacon strip steak eu. Consectetur ham hock pork hamburger enim strip steak
mollit quis officia meatloaf tri-tip swine. Cow ut reprehenderit, buffalo incididunt in filet mignon
strip steak pork belly aliquip capicola officia. Labore deserunt esse chicken lorem shoulder tail consectetur
cow est ribeye adipisicing. Pig hamburger pork belly enim. Do porchetta minim capicola irure pancetta chuck
fugiat.
EOF
);
// publish most articles
if (rand(1, 10) > 2) {
$article->setPublishedAt(new \DateTime(sprintf('-%d days', rand(1, 100))));
}
$article->setAuthor('Mike Ferengi')
->setHeartCount(rand(5, 100))
->setImageFilename('asteroid.jpeg')
;
$em->persist($article);
$em->flush();
return new Response(sprintf(
'Hiya! New Article id: #%d slug: %s',
$article->getId(),
$article->getSlug()
));
}
}

Let's build an awesome fixtures system instead.

Leave a comment!

  • 2018-07-09 Diego Aguiar

    Hey toporovvv

    Nice catch! Those code blocks were totally wrong, I've already fixed them :)

    Have a nice day

  • 2018-07-09 toporovvv

    The code of the function setHeartCount get a heartCount parameter, but then calling in the controller without arguments. I suppose, that in the entity it should be like this:

     
    public function incrementHeartCount(): self
    {
    $this->heartCount = $this->getHeartCount() + 1;

    return $this;
    }

    Ah, it's not about the method, it's about the code highlighted for the video (setHeartCount instead of incrementHeartCount).

  • 2018-06-28 Diego Aguiar

    Hey Abdelkarim Abdouni

    Thanks for informing us! I already fixed it :)

    Have a nice day

  • 2018-06-27 Abdelkarim Abdouni

    Hello weaverryan , you just forgot to return $this in the incrementHeartCount method of Article Entity. In the code, not on the video :)
    Cheers

  • 2018-05-29 Victor Bocharsky

    Hey Yahya,

    Good question! One of the possible solutions is do not have a number of hearts at all :) So, you have a ManyToMany relation where each heart is just a new row in the user_article table, and you just write to this table new rows to save hearts... but than to get the amount of current hearts you would like to execute COUNT query on this table. Well, you probably may want to cache this counted value for a while to improve performance if you're ok to have not a real up-to-date value but cached one.

    Cheers!

  • 2018-05-27 Yahya A. Erturan

    Hey there, now I am curious.. How to handle it in high traffic web projects :) ? At least as conceptual. Thanks.

  • 2018-05-20 Shaun

    Thanks Victor Bocharsky and weaverryan , I really appreciate your help :)

  • 2018-05-17 Victor Bocharsky

    Hey Shaun,

    First of all, you better use .data() for getting and even setting values, if you pass the 2nd arg to it - it will set the passed value instead of returning it, i.e. $link.data('liked', data.liked);, see docs for more information and examples: https://api.jquery.com/data/

    And I see another problem, in .done() you mistakenly wrap already jQuery object with another jQuery object, I mean you should not wrap $link with $($link), it's already a jQuery object when you do var $link = $(e.currentTarget);, so try this code:


    $(document).ready(function() {
    $('body').on('click', '.js-like-post', function(e) {
    e.preventDefault();

    var $link = $(e.currentTarget);
    $link.toggleClass('far fa-thumbs-up').toggleClass('fas fa-thumbs-up');

    var liked = $(this).data('liked');

    $.ajax({
    method: 'POST',
    url: $link.attr('href'),
    data: {
    liked: liked
    },
    }).done(function(data) {
    $link.data('liked', data.liked);
    $('.js-like-post-count').text(data.likes);
    })
    });
    });

    Also, it's a good practice to start only those variables with $ that are jQuery object.

    Cheers!

  • 2018-05-16 Shaun

    Thanks Ryan, your reply was really helpful :)

    I have created a data attribute in my twig template called data-liked. When I send a request to the controller, I am able to update the value (for example from 0 to 1), but when I try again, the JS doesn't pick up the new data attribute value, any idea why this might be?

    Here is my JS:


    $(document).ready(function() {
    $('.js-like-post').on('click', function(e) {
    e.preventDefault();

    var $link = $(e.currentTarget);
    $link.toggleClass('far fa-thumbs-up').toggleClass('fas fa-thumbs-up');

    var $liked = $(this).data('liked');

    $.ajax({
    method: 'POST',
    url: $link.attr('href'),
    data: {
    liked: $liked
    },
    }).done(function(data) {
    $($link).attr('data-liked', data.liked);
    $('.js-like-post-count').html(data.likes);
    })
    });
    });
  • 2018-05-14 weaverryan

    Hey Shaun!

    Of course, it always "depends". The front-end DOES know whether or not the user has "liked" the article yet, we use this to style the heart, and we could very easily (e.g. via a data- attribute), make this information available to JavaScript. The "purest" solution would probably be to change the endpoint (/news/{slug}/heart) to be a PUT endpoint so that your AJAX call (from a conceptual standpoint) is effectively "editing" the "heart" field of an article. In this case, you would send whether or not the user wants to "heart" the article in the JSON body of the request (e.g. {"heart": 0}). Or, to be super fancy, you could literally send "0" or "1" in the request body, but that's kinda strange :).

    So, that's probably what I would do. But, the alternative would be to simply keep the endpoint as a POST, and have the JavaScript blindly send a POST request to the endpoint (with no request body, like it is done in this tutorial) and allow the server to figure out if the user has already liked the article (and if they have, "unlike" it). Obviously, whatever you do, as Victor mentioned, on the server-side, you'll need some additional relationships to be setup.

    Does that help? Or did I miss your question entirely? :)

    Cheers!

  • 2018-05-14 Shaun

    Thanks for getting back to me Victor, I am fairly comfortable with the relationship side of things, I was more curious about how the API end point would know whether the user was liking or unliking the article? I presume this would be done with Javascript, if you have any advice on this side that would be much appreciated :)

  • 2018-05-14 Victor Bocharsky

    Hey Shaun,

    Good question! It requires a bit more work and knowledges of ManyToMany Doctrine relation that we do not explain in this course yet. But if do not want to wait but want to go ahead, you'll need to make so that User relates to Article as ManyToMany in Doctrine, i.e. so a user may *heart* many articles and the reverse, an article may be *hearted* by many users :) Then, with some logic and checks you can implement it.

    Cheers!

  • 2018-05-13 Shaun

    How can this be improved so that if the user clicks the heart again the heartCount is decremented?