Updating an Entity
Keep on Learning!
If you liked what you've learned so far, dive in! Subscribe to get access to this tutorial plus video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeIn 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:
// ... 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()
:
// ... 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:
$(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
:
// ... 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
:
// ... 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()
:
// ... 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
:
// ... 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()
:
// ... 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:
// ... 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:
// ... lines 1 - 9 | |
class Article | |
{ | |
// ... lines 12 - 131 | |
public function incrementHeartCount(): self | |
// ... lines 133 - 154 | |
} |
Then, $this->heartCount = $this->heartCount + 1
:
// ... 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()
:
// ... 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?
// ... 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:
// ... 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:
// ... 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.
Hey there, now I am curious.. How to handle it in high traffic web projects :) ? At least as conceptual. Thanks.