Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Update Query & Rich vs Anemic Models

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

On the show page, we can now up vote or down vote the question... mostly. In the controller, we read the direction POST parameter to know which button was clicked and change the vote count. This doesn't save to the database yet, but we'll do that in a few minutes.

Adding upVote and downVote Methods

Before we do, we have another opportunity to improve our code. The logic inside the controller to increase or decrease the vote isn't complex, but it could be simpler and more descriptive.

In Question, at the bottom, add a new public function called upVote(). I'm going make this return self.

... lines 1 - 10
class Question
{
... lines 13 - 116
public function upVote(): self
{
... lines 119 - 121
}
... lines 123 - 129
}

Inside, say $this->votes++. Then, return $this... just because that allows method chaining. All of the setter methods return $this.

... lines 1 - 10
class Question
{
... lines 13 - 116
public function upVote(): self
{
$this->votes++;
return $this;
}
... lines 123 - 129
}

Copy this, paste, and create another called downVote() that will do $this->votes--.

... lines 1 - 10
class Question
{
... lines 13 - 123
public function downVote(): self
{
$this->votes--;
return $this;
}
}

I'm not going to bother adding any PHP documentation above these, because... their names are already so descriptive: upVote() and downVote()!

I love doing this because it makes the code in our controller so nice. If the direction is up, $question->upVote(). If it's down, $question->downVote().

... lines 1 - 14
class QuestionController extends AbstractController
{
... lines 17 - 99
public function questionVote(Question $question, Request $request)
{
$direction = $request->request->get('direction');
if ($direction === 'up') {
$question->upVote();
} elseif ($direction === 'down') {
$question->downVote();
}
dd($question);
}
}

How beautiful is that? And when we move over to try it... we're still good!

Rich vs Anemic Models

We've now added three custom methods to Question: upVote(), downVote() and getVotesString(). And this touches on a somewhat controversial topic related to entities. Notice that every property in our entity has a getter and setter method. This makes the entity super flexible: you can get or set any field you want.

But sometimes you might not need - or even want - a getter or setter method. For example, do we really want a setVotes() method? Should anything in our app be able to set the vote directly to any number? Probably not. Probably we will always want to use upVote() or downVote().

Now, I will keep this method... but only because we're using it in QuestionController. In the new() method... we're using it to set the fake data.

But this touches on a really interesting idea: by removing any unnecessary getter or setter methods in your entity and replacing them with more descriptive methods that fit your business logic - like upVote() and downVote() - you can, little by little, give your entities more clarity. upVote(), and downVote() are very clear & descriptive. Someone calling these doesn't even need to know or care how they work internally.

Tip

Generally-speaking, an "anemic" model is a class where you can directly modify and access its properties (e.g. via getter/setter methods). A "rich" model is where you, instead, create methods specific to your business logic - like upVote().

Some people take this to an extreme and have almost zero getter and setter methods on their entities. Here at Symfonycasts, we tend to be more pragmatic. We usually have getters and setters method, but we always look for ways to be more descriptive - like upVote() and downVote().

Updating an Entity in the Database

Okay, let's finish this! In our controller, back down in questionVote(), how can we execute an update query to save the new vote count to the database? Well, no surprise, whenever we need to save something in Doctrine, we need the entity manager.

Add another argument: EntityManagerInterface $entityManager.

... lines 1 - 14
class QuestionController extends AbstractController
{
... lines 17 - 99
public function questionVote(Question $question, Request $request, EntityManagerInterface $entityManager)
{
... lines 102 - 114
}
}

Then, below, replace the dd($question) with $entityManager->flush().

... lines 1 - 14
class QuestionController extends AbstractController
{
... lines 17 - 99
public function questionVote(Question $question, Request $request, EntityManagerInterface $entityManager)
{
$direction = $request->request->get('direction');
if ($direction === 'up') {
$question->upVote();
} elseif ($direction === 'down') {
$question->downVote();
}
$entityManager->flush();
... lines 111 - 114
}
}

Done! Seriously! Doctrine is smart enough to realize that the Question object already exists in the database and make an update query instead of an insert. We don't need to worry about "is this an insert or an update" at all? Doctrine has that covered.

No persist() on Update?

But wait, didn't I forget the persist() call? Up in the new() action, we learned that to insert something, we need to get the entity manager and then call persist() and flush().

This time, we could have added persist(), but we don't need to. Scroll back up to new(). Remember: the point of persist() is to make Doctrine aware of your object so that when you call flush(), it knows to check that object and execute whatever query it needs to save that into the database, whether that is an INSERT or UPDATE query.

Down in questionVote(), because Doctrine was used to query for this Question object... it's already aware of it! When we call flush(), it already knows to check the Question object for changes and performs an UPDATE query. Doctrine is smart.

Redirecting

Ok, now that this is saving... what should our controller return? Well, usually after a form submit, we will redirect somewhere. Let's do that. How? return $this->redirectToRoute() and then pass the name of the route that we want to redirect to. Let's use app_question_show to redirect to the show page and then pass any wildcard values as the second argument: slug set to $question->getSlug().

... lines 1 - 14
class QuestionController extends AbstractController
{
... lines 17 - 99
public function questionVote(Question $question, Request $request, EntityManagerInterface $entityManager)
{
$direction = $request->request->get('direction');
if ($direction === 'up') {
$question->upVote();
} elseif ($direction === 'down') {
$question->downVote();
}
$entityManager->flush();
return $this->redirectToRoute('app_question_show', [
'slug' => $question->getSlug()
]);
}
}

Two things about this. First, until now, we've only generated URLs from inside of Twig, by using the {{ path() }} function. We pass the same arguments to redirectToRoute() because, internally, it generates a URL just like path() does.

And second... more of a question. On a high level... what is a redirect? When a server wants to redirect you to another page, how does it do that?

A redirect is nothing more than a special type of response. It's a response that has a 301 or 302 status code and a Location header that tells your browser where to go.

Let's do some digging and find out how redirectToRoute() does this. Hold Command or Ctrl and click redirectToRoute() to jump to that method inside of AbstractController. This apparently calls another method: redirect(). Hold Command or Ctrl again to jump to that.

Ah, here's the answer: this returns a RedirectResponse object. Hold Command or Ctrl one more time to jump into this class.

RedirectResponse live deep in the core of Symfony and it extends Response! Yes this is just a special subclass of Response that's really good at creating redirect responses.

Let's close all of these core classes. The point is: the redirectToRoute() method doesn't do anything magical: it simply returns a Response object that's really good at redirecting.

Ok: testing time! Spin over to your browser and go back to the show page. Right now this has 10 votes. Hit "up vote" and... 11! Do it again: 12! Then... 13! Downvote... 12. We got it!

Like I said earlier, in a real app, when we have user authentication, we might prevent someone from voting multiple times. But, we can worry about that later.

Next: we have created a way to load dummy data into our database via the /questions/new page. But... it's pretty hacky.

Let's replace this with a proper fixtures system.

Leave a comment!

12
Login or Register to join the conversation
Rob T. Avatar

Hey Ryan!
How do you decide how much logic to place into an entity method vs making a new service, i.e. QuestionService and placing all of your voting etc into there?

I ask because I have worked on past projects where almost all the logic related to an entity was placed into the entity which eventually led to so much tight coupling it was hell to work with. As a result, I've always been a bit too scared to touch the Entity class with anything more than the basic getter and setter.

1 Reply

Hey Rob T.!

That's a really good question... and kind of tough to answer - it's a judgement call. But, at least in our company, we tend to put logic into a service more often than we tweak the entity. We would usually change the entity when we have some pure method whose only job is to manipulate the data on that entity (e.g. upVote() or getFullName() which concatenates first and last name). Normally it's kind of hard (at least in my experience) to put too much logic into you entity because your entity does not have access to any services.

For example, suppose that each time a Question gets an up vote, you need to send an email to someone. That logic cannot go into the entity... because the entity doesn't have access to the mailer service. You could pass it into a method - e.g. public function upVote(MailerInterface $mailer) then pass the mailer in whenever you call that. DON'T do that :). You probably already know, but services should never go into your entities.

Anyways, if you don't pass services into your entity, then your entity is pretty limited. In the above example, I would need to have some sort of QuestionVoteHelper with an upVote(Question $question) method. That method would be responsible for incrementing the vote on the Question object AND sending the email. You could still choose to have an upVote() method on your entity. The QuestionVoteHelper would call it... and its job would simply be to increment the vote count (the helper service would still send the email).

That answer got... lengthy 😆. Let me know if it helped!

Cheers!

Reply
Kevin Avatar

Hi Ryan,

Why is it considered bad practice to add services to entities? Is it because methods of an entity should strictly be used/created to modify its own data and nothing more?

Reply

Hey Ccc123,

Yes, using services inside entities is a bad practice. And yes, that's because entities are just simple data objects.

If you need to do some heavy calculations inside and its logic is moved to a separate service - rewrite that service so that you pass the entity object to the service and it do all the necessary heavy calculation and set the final simple value on that entity instead of passing the service to the entity.

Cheers!

Reply
Rob T. Avatar

You say don't pass service vars into the entity, but thats exactly what some past devs had done to the code base I mentioned... *shudders*!!!

That's brilliant, thanks very much for taking the time to reply. Makes a lot of sense and has definitely helped :)

Reply

Ha! I was afraid of that! 😆I've seen it too

Anyways, I'm glad this was helpful!

Cheers!

Reply
Niclas H. Avatar
Niclas H. Avatar Niclas H. | posted 2 years ago

The title of this video is "Rich vs Anemic Models", yet these terms aren't mentioned even once in the video. I know it can be Googled, but perhaps the video could be updated with a short description.

Reply

Hey Niclas H.!

Ah, good catch! Yes, we'll add a note to the video to help - thanks!

Cheers!

Reply
Nayte91 Avatar
Nayte91 Avatar Nayte91 | posted 2 years ago

Hello, and thank you for this awesome tutorial,
I was wondering for this questionVote() controller method you present here : does flush send a db request only if the object changed ? It seems so.

But does flush take time to compare every objects in order to know if it needs to send request or not ? Does this flush() method consume some resources ? I'm thinking about what Ryan said about "someone who is messing with this route".. Is it better to put the flush() in the if and else statements ? Or to guard pattern the whole at beginning ?
Not criticizing the video, it's perfect; I was just wondering how to to this "the best way possible" during it :)
cheers !

Reply

Hey Nayte,

Yes, you'er right, the flush() will send the query to your DB only when the object is changed - that part is completely handled by Doctrine ORM, Doctrine track all the entities and if any have changes - the corresponding queries will be sent on flush() and only on flush() :)

And yes, it consumes some resources for this, but that's the price of this developer experience you have with Doctrine ORM. And it's not that much resources when you do thing right, so don't think too much about this. But if you're really curious, you can always use a profiler tool like Blackfire.io (we even have a course about it here: https://symfonycasts.com/sc... ) and check how heavy that part of code is in your application ;)

Cheers!

Reply
Farshad Avatar
Farshad Avatar Farshad | posted 2 years ago

Do you guys have any tips of how to structure the directories for big projects? A website with many sub parts. For example: Games, Videos, Blog, Articles.
I want to keep everything organized. So I was thinking to put every Sub Category Controller in a different folder. For example:
src / Controller / Games / GamesController.php
src / Controller / Videos / VideosController.php

But if Symfony is automatically making files in src / Entities and src / Repository directories, then those 2 directories will be filled up with 50 or more .php files and will become messy. How do I keep everything in those directories organised?

Reply

Hey Farry7,

Yeah, you can orginize it as you want. Good idea about orginizing controllers, if you have several controllers related to "Games" - it makes sense to put them into src/Controller/Games/ folder. And same for other services, you may group them with separate folders.

About entities, take a look at "config/packages/doctrine.yaml" config file, in particular, "doctrine.orm.mappings". In theory you can put your entities in different folders, but you would need to configure paths to them.

About repositories - you don't care about them, you can put them in whatever folder you want too, but you will need to specify correct FQCNs in your entities.

I hope this helps!

Cheers!

Reply
Cat in space

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

This tutorial also works great for Symfony 6!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.4.1",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "doctrine/doctrine-bundle": "^2.1", // 2.1.1
        "doctrine/doctrine-migrations-bundle": "^3.0", // 3.0.2
        "doctrine/orm": "^2.7", // 2.8.2
        "knplabs/knp-markdown-bundle": "^1.8", // 1.9.0
        "knplabs/knp-time-bundle": "^1.11", // v1.16.0
        "sensio/framework-extra-bundle": "^6.0", // v6.2.1
        "sentry/sentry-symfony": "^4.0", // 4.0.3
        "stof/doctrine-extensions-bundle": "^1.4", // v1.5.0
        "symfony/asset": "5.1.*", // v5.1.2
        "symfony/console": "5.1.*", // v5.1.2
        "symfony/dotenv": "5.1.*", // v5.1.2
        "symfony/flex": "^1.3.1", // v1.17.5
        "symfony/framework-bundle": "5.1.*", // v5.1.2
        "symfony/monolog-bundle": "^3.0", // v3.5.0
        "symfony/stopwatch": "5.1.*", // v5.1.2
        "symfony/twig-bundle": "5.1.*", // v5.1.2
        "symfony/webpack-encore-bundle": "^1.7", // v1.8.0
        "symfony/yaml": "5.1.*", // v5.1.2
        "twig/extra-bundle": "^2.12|^3.0", // v3.0.4
        "twig/twig": "^2.12|^3.0" // v3.0.4
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.3", // 3.4.0
        "symfony/debug-bundle": "5.1.*", // v5.1.2
        "symfony/maker-bundle": "^1.15", // v1.23.0
        "symfony/var-dumper": "5.1.*", // v5.1.2
        "symfony/web-profiler-bundle": "5.1.*", // v5.1.2
        "zenstruck/foundry": "^1.1" // v1.5.0
    }
}
userVoice