Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Smarter Entity Methods

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

We are on an epic quest to make everything on the question page truly dynamic. In the design, each question can get up and down voted... but this doesn't work yet and the vote count - + 6 - is hardcoded in the template.

To get this working, let's add a new votes property to the Question entity. When a user clicks the up button, we will increase the votes. When they click down, we'll decrease it. In the future, when we have a true user authentication system, we could make this smarter by recording who is voting and preventing someone from voting multiple times. But our simpler plan will work great for now.

Adding the votes Property

Step one: add a new field to the entity. We could do this by hand by copying an existing property, adjusting the options and then adding getter and setter methods for it. But... it's easier just to run make:entity. At your terminal, run:

php bin/console make:entity

Once again, I could use symfony console... and I probably should. But since this command doesn't need the database environment variables, bin/console also works.

This time, enter Question so that we can update the entity. Yea! make:entity can also be used to modify an entity! Add a new field called votes, make it an integer type and set it to not nullable in the database. Hit enter to finish.

Ok! Let's go check out the Question entity. It looks exactly like we expected: a $votes property and, at the bottom, getVotes() and setVotes() methods.

... lines 1 - 10
class Question
{
... lines 13 - 39
/**
* @ORM\Column(type="integer")
*/
private $votes;
... lines 44 - 97
public function getVotes(): ?int
{
return $this->votes;
}
public function setVotes(int $votes): self
{
$this->votes = $votes;
return $this;
}
}

Let's generate the migration for this. Run:

symfony console make:migration

so that the Symfony binary can inject the environment variables. When this finishes, I like to double check the migration to make sure it doesn't contain any surprises.

... lines 1 - 4
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
... lines 10 - 12
final class Version20200708195925 extends AbstractMigration
{
... lines 15 - 19
public function up(Schema $schema) : void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE question ADD votes INT NOT NULL');
}
public function down(Schema $schema) : void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE question DROP votes');
}
}

This looks perfect. Execute it with:

symfony console doctrine:migrations:migrate

Beautiful!

Default Values with Doctrine

But... this did break one little thing. Go to /questions/new - our endpoint to create a new Question. And... woh! There's an exception coming from the database:

Integrity constraint violation: Column 'votes' cannot be null

Hmm, yea: that makes sense. We didn't set the votes property, so it's trying to create a new row with null for that column. What we probably want to do is default votes to be zero. How can we set a default value for a column in Doctrine?

Actually, that's not really the right question to ask. A better question would be: how can we default the value of a property in PHP?

And the answer to that is simple. In Question, just say private $votes = 0

... lines 1 - 10
class Question
{
... lines 13 - 39
/**
* @ORM\Column(type="integer")
*/
private $votes = 0;
... lines 44 - 108
}

It's that easy. Now, when we instantiate a Question object, votes will be zero. And when it saves the database... the votes column will be zero instead of null. There is actually a way inside the @ORM\Column annotation to specifically set the default value of the column in the database, but I've never used it. Setting the default value on the property works beautifully.

Hit the URL again and... it works!

Giving getVotes() a Non-Nullable Return Type

Back in the entity, scroll down to getVotes(). The return type of this method is a nullable integer. It was generated that way because there was no guarantee that the votes property would ever be set: it was possible for votes to be null in PHP. But thanks to the change we just made, we can now remove the question mark: we know that this will always be an integer.

... lines 1 - 10
class Question
{
... lines 13 - 97
public function getVotes(): int
{
return $this->votes;
}
... lines 102 - 108
}

Rendering the Vote

Before we hook up the voting functionality, let's render the vote count. To make this more interesting - because all of the questions in the database right now have zero votes - let's set a random vote number for new questions. In QuestionController, scroll up to the new() action. Near the bottom, add $question->setVotes() and pass a random number from negative 20 to 50.

... lines 1 - 13
class QuestionController extends AbstractController
{
... lines 16 - 40
public function new(EntityManagerInterface $entityManager)
{
... lines 43 - 62
$question->setVotes(rand(-20, 50));
$entityManager->persist($question);
$entityManager->flush();
... lines 67 - 72
}
... lines 74 - 94
}

Back on the browser, I'll refresh /questions/new a few times to get some fresh data. Copy the new slug and put that into the address bar to view the new Question.

Rendering the true vote count should be easy. Open up templates/question/show.html.twig. Find the vote number... + 6 and replace it with {{ question.votes }}

... lines 1 - 5
<div class="container">
<div class="row">
<div class="col-12">
<h2 class="my-4">Question:</h2>
<div style="box-shadow: 2px 3px 9px 4px rgba(0,0,0,0.04);">
<div class="q-container-show p-4">
<div class="row">
<div class="col-2 text-center">
... line 14
<div class="mt-3">
... lines 16 - 24
<div class="vote-arrows vote-arrows-alt flex-fill pt-2" style="min-width: 90px;">
... lines 26 - 27
<span>{{ question.votes }}</span>
</div>
</div>
</div>
... lines 32 - 39
</div>
</div>
</div>
</div>
</div>
... lines 45 - 72
</div>
... lines 74 - 75

That's good boring code. Back at the browser, when we refresh... nice! This has minus 10 votes... it must not be a great question.

Adding the + / - Sign

Because the vote is negative, it naturally has a "minus" sign next to it. But that won't be there for a positive number. Let me create another Question that will hopefully have a positive vote number. Yes! When it's positive, it's just 10, not + 10.

But... our designer actually does want positive vote numbers to have a plus sign. No problem. We could add some extra Twig logic: if the number is positive, then add a plus sign before printing the votes.

There's nothing wrong with having simple logic like this in Twig. But if there is another place that we could put that logic, that's usually better. In this case, we could add a new method to the Question entity itself: a method that returns the string representation of the vote count - complete with the + and - signs. That would keep the logic out of Twig and even make that code reusable. Heck! We could also unit test it!

Check it out: inside the Question entity - it doesn't matter where, but I'll put it right after getVotes() so that it's next to related methods - add public function getVotesString() with a string return type. Inside, I'll paste some logic.

... lines 1 - 10
class Question
{
... lines 13 - 102
public function getVotesString(): string
{
... lines 105 - 107
}
... lines 109 - 115
}

This first determines the "prefix" - the plus or minus sign - and then adds that before the number - using the abs() function to avoid two minus signs for negative numbers. In other words, this returns the exact string we want. How nice is that? Easy to read & reusable.

... lines 1 - 10
class Question
{
... lines 13 - 102
public function getVotesString(): string
{
$prefix = $this->getVotes() >=0 ? '+' : '-';
return sprintf('%s %d', $prefix, abs($this->getVotes()));
}
... lines 109 - 115
}

To use it in Twig, we can say question.votesString.

... lines 1 - 5
<div class="container">
<div class="row">
<div class="col-12">
<h2 class="my-4">Question:</h2>
<div style="box-shadow: 2px 3px 9px 4px rgba(0,0,0,0.04);">
<div class="q-container-show p-4">
<div class="row">
<div class="col-2 text-center">
... line 14
<div class="mt-3">
... lines 16 - 24
<div class="vote-arrows vote-arrows-alt flex-fill pt-2" style="min-width: 90px;">
... lines 26 - 27
<span>{{ question.votesString }}</span>
</div>
</div>
</div>
... lines 32 - 39
</div>
</div>
</div>
</div>
</div>
... lines 45 - 72
</div>
... lines 74 - 75

That's it. Let's try it! Over on the browser, refresh and... there it is! + 10!

The cool thing about this is that we said question.votesString. But... there is no $votesString property inside of Question! And... that's fine! When we say question.votesString, Twig is smart enough to call the getVotesString() method.

Now that we're printing the vote number, let's make it possible to click these up and down vote buttons. This will be the first time we execute an update query and we'll get to talk more about "smart" entity methods. That's all next.

Leave a comment!

24
Login or Register to join the conversation
davidmintz Avatar
davidmintz Avatar davidmintz | posted 11 months ago

minor issue here: I don't have any DATABASE_URL environment variable explicitly defined in .env or anywhere else. I've been letting the symfony binary figure it out from docker-compose.yml and set it for me, as we learned a few chapters ago. Consequently, when I run composer commands and it tries to run the auto-scripts' cache:clear, I get a fatal 'Environment variable not found: "DATABASE_URL".' If I prefix the composer command with DATABASE_URL=whatever it works -- literally, any old string works. So, not a show-stopper, but I do wonder what the recommended practice is.

btw these tutorials are marvelous. really enjoyable and extremely clear.

Reply

Hey Elliott,

First of all, thank you for kind words about this tutorials! :)

What exactly command did you run? If you want to base on Docker configuration, you should execute "symfony composer install" instead of just "composer install", otherwise the variable won't be found. I guess the problem was in the command you ran :)

Cheers!

Reply
davidmintz Avatar

of course! I didn't realize you could run both composer as well as bin/console through the symfony command. it almost -- but not quite -- occurred to me that I might try that (-: thanks.

Reply

Hey Elliott,

Yeah, that's a little know fact probably, and in most cases there's literally no difference unless you start using Docker ;)

Cheers!

Reply
Hanane K. Avatar
Hanane K. Avatar Hanane K. | posted 11 months ago

Hello,
First of all, I want to say that I love the way you explain things Ryan, you make them easy to understand, so big thanks to u and to the Team.

I was surprised when I noticed that it's not mandatory to have a "get" word at the beginning of your method to be able to use it in the TWIG (ex : getVotesString can be votesString ) , which means that when creating custom methods it's better to make them protected if we don"t want them to be accessible outside (twig).

I tried to use the doctrine annotation to set a default value, but still I get the error saying that this field (votes) can't be null, I had to set the property to Zero to make it work. Here is the syntax * @ORM\Column(type="integer",options={"default":0}) Am I missing smtg
Thanks in advance for your response.

Reply

Hey Hanane K.!

Thanks for for the nice message ❤️❤️❤️

> I was surprised when I noticed that it's not mandatory to have a "get" word at the beginning of your method to be able to use it in the TWIG (ex : getVotesString can be votesString ) , which means that when creating custom methods it's better to make them protected if we don"t want them to be accessible outside (twig).

Definitely. Better, make them private. If you don't need to access a method from outside of the class (via Twig or any other way), then make it private. I always start with private... then only make something public when I NEED to. Though, I'm a bit liberal with my getter/setter methods in my entities (I usually start with a public getter/setter for every property... and sometimes I remove methods that I don't use... but not always - I'm not strict about it).

> I tried to use the doctrine annotation to set a default value, but still I get the error saying that this field (votes) can't be null, I had to set the property to Zero to make it work. Here is the syntax * @ORM\Column(type="integer",options={"default":0}) Am I missing smtg

I always set my properties to their default value anyways - I've never actually used the options={"default":0} way of doing things. There's nothing wrong it it (well, except that it's not working for you for some reason!), it's just not really needed. If you only used the options way of doing this, then it's a bit weird because your property might be null in php... but then when you save it, it will save asa 0. It's just a bit cleaner and more consistent (in my opinion) to set the property to 0... and then naturally when you save, it will save 0 to the database. So I don't know why the "options" isn't working, but I don't recommend bothering with it anyways :).

Cheers!

1 Reply
Tomasz P. Avatar
Tomasz P. Avatar Tomasz P. | posted 11 months ago

Small note, getVotesString should be something like this:
public function getVotesString(): string
{
$prefix = ($this->votes == 0) ? '' : (($this->votes >=0) ? '+' : '-');
return sprintf('%s %d', $prefix, abs($this->votes));
}

If not you will get +0 :)

Reply

Hey Tomasz P.

Good catch =) Yeah of course that part of code can be much better and that's all is up to you to make it perfect!

Cheers!

Reply
Nick-F Avatar

Why do you use $this->getVotes() and not just $this->votes in the getVotesString() function?

Reply

Hey Name->Nick,

Fairly speaking, it's not that much important, and probably just matter of habit, but yeah, you're right, calling the getter is redundant in this case. Moreover, sometimes you even don't need a getter, so it's pointless to have it only to reference from within the class where the property is defined. Though sometimes you may have a bit extra logic in the method, and you do want to reuse it. But most of the cases, I'm for calling properties directly instead via getters within the class :)

Cheers!

Reply
Nick-F Avatar

Cool just making sure there wasn't some more complicated purpose behind it like doctrine having to query the database or entity persistence or something

Reply

Hey Name->Nick,

Good thing to think about, yeah :) But in this case it's much simpler, the value is just an int type ;)

Cheers!

Reply
Niclas H. Avatar
Niclas H. Avatar Niclas H. | posted 1 year ago

Heads up for those using PostgreSQL - the migration execution isn't going to work based on the auto-generated migration PHP files alone, assuming you already have some existing Questions saved in your database.

When executing the migration, it will attempt to add the 'votes' field to your existing Questions with the default value 'null'. But you specified that 'votes' cannot be null, so the migration fails.

It's an easy fix - update the migration file's "up" function to use 0 as the default value instead.

Before:
$this->addSql('ALTER TABLE question ADD votes INT NOT NULL');

After:
$this->addSql('ALTER TABLE question ADD votes INT NOT NULL DEFAULT 0');

Reply
maki_mono Avatar

In your migration:

public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE question ADD votes INT');
$this->addSql('UPDATE question SET votes = 0');
$this->addSql('ALTER TABLE question ALTER COLUMN votes SET NOT NULL');
}

Reply

Hey Piotyras,

Yes, you're right, migrations generated for MySQL won't work on PostgreSQL. We usually use MySQL DB in our screencasts, so consider to generate a different migration that will work for your DB porting our migration generated for MySQL, or if you don't care about the data lose - you can execute bin/console doctrine:schema:update --force, but this trick won't fit production of course, as you do not want to lose prod data.

Thanks for sharing your solution with others!

Cheers!

Reply
CloudCreators Avatar
CloudCreators Avatar CloudCreators | posted 1 year ago

In AbstractMySQLDriver.php line 115:

An exception occurred in driver: SQLSTATE[HY000] [2006] MySQL server has go
ne away

In Exception.php line 18:

SQLSTATE[HY000] [2006] MySQL server has gone away

In PDOConnection.php line 39:

SQLSTATE[HY000] [2006] MySQL server has gone away

In PDOConnection.php line 39:

PDO::__construct(): MySQL server has gone away
I added the votes property then I wanted to make migration but It states this error? any help

Reply

Hey Shubham,

It sounds like you have a connection problem to the DB server. I'd suggest you to restart your MySQL server, and try to stop and start Symfony server as well just in case. Does it help?

Also, double check your DB schema, make sure it's valid and in sync. To check it, you can run:

$ bin/console doctrine:schema:validate

Both Mapping and Database sections are green? If it's a dev project with no real data, and you don't care about any potential data lose locally - you can try to execute:

$ bin/console doctrine:schema:update --force

To sync the DB schema and try again to see if it solves the issue. But you probably may want to write a correct migration for those executed queries to avoid data losing on production.

If nothing above helps, what exactly are you doing when see this error? Just refresh the page? What's page URL exactly?

Cheers!

Reply
CloudCreators Avatar
CloudCreators Avatar CloudCreators | victor | posted 1 year ago

Yeah it worked I restarted docker!!

Reply

Hey Shubham,

I'm happy to hear it, thanks for confirming that helps!

Cheers!

Reply
Antoine R. Avatar
Antoine R. Avatar Antoine R. | posted 1 year ago

Hello,

I was surprised to see that the migration did not break when adding a not nullable field to our data. It appears it's thanks to MySQL, but I didn't find informations about this behavior :
After a few tests, I noticed that when adding a int or double not null without providing a default value, mysql puts the value 0 in this field for existing datas.

On the other hand, I was confused by symfony console dbal:run-sql "SELECT * FROM question" showing string(1) "0" for my new INT fields. But it's just a weird display from the command. In fact it is indeed an int. For example, a double 1.1 is displayed string(3) "1.1".

Reply

Hey Antoine R.!

> I was surprised to see that the migration did not break when adding a not nullable field to our data. It appears it's thanks to MySQL, but I didn't find informations about this behavior

Yup, you're correct, I also don't know the specifics. But I do know, for example, if you added a new "not null datetime" field, that WOULD cause a problem with MySQL.

> On the other hand, I was confused by symfony console dbal:run-sql "SELECT * FROM question" showing string(1) "0" for my new INT fields. But it's just a weird display from the command. In fact it is indeed an int. For example, a double 1.1 is displayed string(3) "1.1".

Good debugging. As i was reading this, I was also thinking "I wonder if that's just due to how it's displayed" :).

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