Buy Access to Course
16.

Smarter Entity Methods

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

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.

110 lines | src/Entity/Question.php
// ... 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.

32 lines | migrations/Version20200708195925.php
// ... 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

110 lines | src/Entity/Question.php
// ... 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.

110 lines | src/Entity/Question.php
// ... 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.

96 lines | src/Controller/QuestionController.php
// ... 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 }}

75 lines | templates/question/show.html.twig
// ... 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.

117 lines | src/Entity/Question.php
// ... 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.

117 lines | src/Entity/Question.php
// ... 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.

75 lines | templates/question/show.html.twig
// ... 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.