Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This tutorial has a new version, check it out!

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.

Start your All-Access Pass
Buy just this tutorial for $12.00

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:

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

Leave a comment!

16
Login or Register to join the conversation
Yahya E. Avatar
Yahya E. Avatar Yahya E. | posted 5 years ago

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

7 Reply
Default user avatar

It's a bit weird that it's not part of the tutorial since this is so important. I assume you need to write your own query to handle it so that it does an update and reads the current counter in a single query instead of separately, then theoretically there shouldn't be any race condition. You can use Doctrine's DQL for that, though I'm not sure where to put this query, inside the Entity?

Reply

Hey Anon,

Well, that's kinda performance and highload issue, this tutorial is not about it :) And it may depends on the query, so difficult to say. But all your queries are better to put into entity repository.

Cheers!

Reply
Default user avatar
Default user avatar Anon | Victor | posted 3 years ago | edited

The query would be pretty simple, just something like UPDATE App\Entity\Article u SET u.heartCount = u.heartCount + 1 WHERE u.id = :article_id.

But since this is an update query, and all the examples in this tutorial are select queries, and all updates to entities seem to be done within entities I'm not sure what the proper place for an update query is. Maybe something to mention in the tutorial? Cheers for the response, I thought this comment would get lost since the other comments are a year old.

Reply

Hey there!

I think you can just place that query inside the corresponding repository class


// some entity repository
public function someUpdateMethod() 
{
$qb = $this->createQueryBuilder('u');
$qb->update()
        ->set('u.someField', 'new data')
        ->getQuery()
        ->execute();
}

Cheers!

Reply
Mike P. Avatar
Mike P. Avatar Mike P. | posted 3 years ago

One question, I want to log the last 10 "search requests" of a search form from my userbase, to output them on the main homepage.
What would you recommend to output only the newest 10 search requests?

To create an entity and:
1.) Create a db record for every search request and "clear" the table every 24h via a Cronjob?
2.) To use the update function (like in this tutorial) to update the "oldest" (of the 10) dataset in the DB?
3.) Or is there another, better approach?

I'm asking this because it seems to put a lot of stress on the system if a lot of search query get performed in the future simultaneously.

Thank you in advance!

1 Reply

Hey Mike P.

I wouldn't rely on the database for that feature because of what you said. It will increase the resources usage of the server (Unless you don't expect high traffic). The first thing that comes to my mind is to use Google Analytics to track all the searches performed and then consume their API to get a list of the searches, and cache the result for 10 minutes or so.
There might be better tools for what you want to do that I'm not aware of, so I recommend you to investigate a little bit before jumping to the implementation

Cheers!

Reply
Robert Avatar

Hey, after i make the changes and click the heart i get the following error.

No route found for "GET /news/this-is-my-article-10721/heart": Method Not Allowed (Allow: POST)

I'm on Symfony 4.4 , do i need to configure the route ?

Reply

Hey Robert

Looks like your route configured properly, looks like your ajax request not working, probably you may have errors in your browser console, can you check it?

Cheers!

1 Reply
Oleksii V. Avatar
Oleksii V. Avatar Oleksii V. | posted 3 years ago

Hi,
I don't understand how to change property of entity and how to fully delete entity with its migration?

Reply

Hey Oleksii V.

To fully delete an entity what you need to do is first delete the entity class file, and then create a migration that will DROP the related table of the entity
To change a property the flow is basically the same, you have to add a new property to the entity (add its metadata), and then create a migration that will update its table

I hope it helps. Cheers!

1 Reply
Oleksii V. Avatar

Thank You!

1 Reply
Default user avatar
Default user avatar toporovvv | posted 5 years ago | edited

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).

Reply

Hey toporovvv

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

Have a nice day

Reply

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

Reply

Hey abdouniabdelkarim

Thanks for informing us! I already fixed it :)

Have a nice day

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "knplabs/knp-markdown-bundle": "^1.7", // 1.7.0
        "knplabs/knp-time-bundle": "^1.8", // 1.8.0
        "nexylan/slack-bundle": "^2.0,<2.2.0", // v2.0.0
        "php-http/guzzle6-adapter": "^1.1", // v1.1.1
        "sensio/framework-extra-bundle": "^5.1", // v5.1.4
        "stof/doctrine-extensions-bundle": "^1.3", // v1.3.0
        "symfony/asset": "^4.0", // v4.0.4
        "symfony/console": "^4.0", // v4.0.14
        "symfony/flex": "^1.0", // v1.17.6
        "symfony/framework-bundle": "^4.0", // v4.0.14
        "symfony/lts": "^4@dev", // dev-master
        "symfony/orm-pack": "^1.0", // v1.0.6
        "symfony/twig-bundle": "^4.0", // v4.0.4
        "symfony/web-server-bundle": "^4.0", // v4.0.4
        "symfony/yaml": "^4.0" // v4.0.14
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.0", // 3.0.2
        "easycorp/easy-log-handler": "^1.0.2", // v1.0.4
        "fzaninotto/faker": "^1.7", // v1.7.1
        "symfony/debug-bundle": "^3.3|^4.0", // v4.0.4
        "symfony/dotenv": "^4.0", // v4.0.14
        "symfony/maker-bundle": "^1.0", // v1.4.0
        "symfony/monolog-bundle": "^3.0", // v3.1.2
        "symfony/phpunit-bridge": "^3.3|^4.0", // v4.0.4
        "symfony/profiler-pack": "^1.0", // v1.0.3
        "symfony/var-dumper": "^3.3|^4.0" // v4.0.4
    }
}
userVoice