Buy Access to Course
13.

Updating an Entity

Share this awesome video!

|

Keep on Learning!

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:

$(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.