Buy Access to Course
09.

Giving the Comments an isDeleted Flag

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

I want to show you a really cool, really powerful feature. But, to do that, we need to give our app a bit more depth. We need to make it possible to mark comments as deleted. Because, honestly, not all comments on the Internet are as insightful and amazing as the ones that you all add to KnpUniversity. You all are seriously the best! But, instead of actually deleting them, we want to keep a record of deleted comments, just in case.

Adding Comment.isDeleted Field

Here's the setup: go to your terminal and run:

php bin/console make:entity

We're going to add a new field to the Comment entity called isDeleted. This will be a boolean type and set it to not nullable in the database:

96 lines | src/Entity/Comment.php
// ... lines 1 - 10
class Comment
{
// ... lines 13 - 37
/**
* @ORM\Column(type="boolean")
*/
private $isDeleted;
// ... lines 42 - 83
public function getIsDeleted(): ?bool
{
return $this->isDeleted;
}
public function setIsDeleted(bool $isDeleted): self
{
$this->isDeleted = $isDeleted;
return $this;
}
}

When that finishes, make the migration:

php bin/console make:migration

And, you know the drill: open that migration to make sure it doesn't contain any surprises:

29 lines | src/Migrations/Version20180430194518.php
// ... lines 1 - 2
namespace DoctrineMigrations;
use Doctrine\DBAL\Migrations\AbstractMigration;
use Doctrine\DBAL\Schema\Schema;
/**
* Auto-generated Migration: Please modify to your needs!
*/
class Version20180430194518 extends AbstractMigration
{
public function up(Schema $schema)
{
// this up() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.');
$this->addSql('ALTER TABLE comment ADD is_deleted TINYINT(1) NOT NULL');
}
public function down(Schema $schema)
{
// this down() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.');
$this->addSql('ALTER TABLE comment DROP is_deleted');
}
}

Oh, this is cool: when you use a boolean type in Doctrine, the value on your entity will be true or false, but in the database, it stores as a tiny int with a zero or one.

This looks good, so move back and.... migrate!

php bin/console doctrine:migrations:migrate

Updating the Fixtures

We're not going to create an admin interface to delete comments, at least, not yet. Instead, let's update our fixtures so that it loads some "deleted" comments. But first, inside Comment, find the new field and... default isDeleted to false:

96 lines | src/Entity/Comment.php
// ... lines 1 - 10
class Comment
{
// ... lines 13 - 37
/**
* @ORM\Column(type="boolean")
*/
private $isDeleted = false;
// ... lines 42 - 94
}

Any new comments will not be deleted.

Next, in CommentFixture, let's say $comment->setIsDeleted() with $this->faker->boolean(20):

33 lines | src/DataFixtures/CommentFixture.php
// ... lines 1 - 9
class CommentFixture extends BaseFixture implements DependentFixtureInterface
{
protected function loadData(ObjectManager $manager)
{
$this->createMany(Comment::class, 100, function(Comment $comment) {
// ... lines 15 - 20
$comment->setIsDeleted($this->faker->boolean(20));
// ... line 22
});
// ... lines 24 - 25
}
// ... lines 27 - 31
}

So, out of the 100 comments, approximately 20 of them will be marked as deleted.

Then, to make this a little bit obvious on the front-end, for now, open show.html.twig and, right after the date, add an if statement: if comment.isDeleted, then, add a close, "X", icon and say "deleted":

90 lines | templates/article/show.html.twig
// ... lines 1 - 6
<div class="container">
<div class="row">
<div class="col-sm-12">
<div class="show-article-container p-3 mt-4">
// ... lines 11 - 39
<div class="row">
<div class="col-sm-12">
// ... lines 42 - 57
{% for comment in article.comments %}
<div class="row">
<div class="col-sm-12">
// ... line 61
<div class="comment-container d-inline-block pl-3 align-top">
// ... line 63
<small>about {{ comment.createdAt|ago }}</small>
{% if comment.isDeleted %}
<span class="fa fa-close"></span> deleted
{% endif %}
// ... lines 68 - 70
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
</div>
// ... lines 82 - 90

Find your terminal and freshen up your fixtures:

php bin/console doctrine:fixtures:load

When that finishes, move back, refresh... then scroll down. Let's see... yea! Here's one: this article has one deleted comment.

Hiding Deleted Comments

We printed this "deleted" note mostly for our own benefit while developing. Because, what we really want to do is, of course, not show the deleted comments at all!

But... hmm. The problem is that, to get the comments, we're calling article.comments:

90 lines | templates/article/show.html.twig
// ... lines 1 - 6
<div class="container">
<div class="row">
<div class="col-sm-12">
<div class="show-article-container p-3 mt-4">
// ... lines 11 - 39
<div class="row">
<div class="col-sm-12">
// ... lines 42 - 57
{% for comment in article.comments %}
// ... lines 59 - 73
{% endfor %}
</div>
</div>
</div>
</div>
</div>
</div>
// ... lines 82 - 90

Which means we're calling Article::getComments():

203 lines | src/Entity/Article.php
// ... lines 1 - 13
class Article
{
// ... lines 16 - 171
/**
* @return Collection|Comment[]
*/
public function getComments(): Collection
{
return $this->comments;
}
// ... lines 179 - 201
}

This is our super-handy, super-lazy shortcut method that returns all of the comments. Dang! Now we need a way to return only the non-deleted comments. Is that possible?

Yes! One option is super simple. Instead of using article.comments, we could go into ArticleController, find the show() action, create a custom query for the Comment objects we need, pass those into the template, then use that new variable. When the shortcut methods don't work, always remember that you don't need to use them.

But, there is another option, it's a bit lazier, and a bit more fun.

Creating Article::getNonDeletedComments()

Open Article and find the getComments() method. Copy it, paste, and rename to getNonDeletedComments(). But, for now, just return all of the comments:

211 lines | src/Entity/Article.php
// ... lines 1 - 13
class Article
{
// ... lines 16 - 179
/**
* @return Collection|Comment[]
*/
public function getNonDeletedComments(): Collection
{
return $this->comments;
}
// ... lines 187 - 209
}

Then, in the show template, use this new field: in the loop, article.nonDeletedComments:

90 lines | templates/article/show.html.twig
// ... 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">
// ... lines 11 - 39
<div class="row">
<div class="col-sm-12">
// ... lines 42 - 57
{% for comment in article.nonDeletedComments %}
// ... lines 59 - 73
{% endfor %}
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
// ... lines 84 - 90

And, further up, when we count them, also use article.nonDeletedComments:

90 lines | templates/article/show.html.twig
// ... 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">
// ... lines 11 - 39
<div class="row">
<div class="col-sm-12">
<h3><i class="pr-3 fa fa-comment"></i>{{ article.nonDeletedComments|length }} Comments</h3>
// ... lines 43 - 75
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
// ... lines 84 - 90

Let's refresh to make sure this works so far. No errors, but, of course, we are still showing all of the comments.

Filtering Deleted Comments in Article::getNonDeletedComments()

Back in Article, how can we change this method to filter out the deleted comments? Well, there is a lazy way, which is sometimes good enough. And an awesome way! The lazy way would be to, for example, create a new $comments array, loop over $this->getComments(), check if the comment is deleted, and add it to the array if it is not. Then, at the bottom, return a new ArrayCollection of those comments:

$comments = [];

foreach ($this->getComments() as $comment) {
    if (!$comment->getIsDeleted()) {
        $comments[] = $comment;
    }
}

return new ArrayCollection($comments);

Simple! But... this solution has a drawback... performance! Let's talk about that next, and, the awesome fix.