If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeI 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.
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:
... 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:
... 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
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
:
... 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)
:
... 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":
... 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.
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
:
... 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()
:
... 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.
Open Article
and find the getComments()
method. Copy it, paste, and rename to getNonDeletedComments()
. But, for now, just return all of the comments:
... 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
:
... 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
:
... 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.
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.
Hey Dominik,
First of all, because this screencast is not about StofDoctrineExtenstion but about Doctrine relations. StofDoctrineExtenstion has many useful behaviors we didn't use here, but that's because we just don't want to stretch this course' time. We just wanted to show how you can install and start using the bundle, but as I already said we didn't have an aim to show all its features. So, if you need softdeletable behavior - go for it :)
I hope this helps.
Cheers!
Hello, guys,
In this video while I was coding along with you and tried to perform "symfony console doctrine:fixtures:load" command, I got this error: "Warning: Invalid argument supplied for foreach() " . I tried to add -vvv at the end and I got this:
"Exception trace:
at /Users/tautvydas/PhpstormProjects/Lesson_3/vendor/doctrine/data-fixtures/lib/Doctrine/Common/DataFixtures/Loader.php:170
Doctrine\Common\DataFixtures\Loader->addFixture() at /Users/tautvydas/PhpstormProjects/Lesson_3/vendor/symfony/doctrine-bridge/DataFixtures/ContainerAwareLoader.php:44
Symfony\Bridge\Doctrine\DataFixtures\ContainerAwareLoader->addFixture() at /Users/tautvydas/PhpstormProjects/Lesson_3/vendor/doctrine/doctrine-fixtures-bundle/Loader/SymfonyFixturesLoader.php:60
Doctrine\Bundle\FixturesBundle\Loader\SymfonyFixturesLoader->addFixture() at /Users/tautvydas/PhpstormProjects/Lesson_3/vendor/doctrine/doctrine-fixtures-bundle/Loader/SymfonyFixturesLoader.php:44
Doctrine\Bundle\FixturesBundle\Loader\SymfonyFixturesLoader->addFixtures() at /Users/tautvydas/PhpstormProjects/Lesson_3/var/cache/dev/ContainerPiTxZ4K/getDoctrine_FixturesLoadCommandService.php:40
ContainerPiTxZ4K\getDoctrine_FixturesLoadCommandService::do() at /Users/tautvydas/PhpstormProjects/Lesson_3/var/cache/dev/ContainerPiTxZ4K/App_KernelDevDebugContainer.php:364
ContainerPiTxZ4K\App_KernelDevDebugContainer->load() at /Users/tautvydas/PhpstormProjects/Lesson_3/vendor/symfony/dependency-injection/Container.php:441
Symfony\Component\DependencyInjection\Container->getService() at /Users/tautvydas/PhpstormProjects/Lesson_3/vendor/symfony/dependency-injection/Argument/ServiceLocator.php:40
Symfony\Component\DependencyInjection\Argument\ServiceLocator->get() at /Users/tautvydas/PhpstormProjects/Lesson_3/vendor/symfony/console/CommandLoader/ContainerCommandLoader.php:45
Symfony\Component\Console\CommandLoader\ContainerCommandLoader->get() at /Users/tautvydas/PhpstormProjects/Lesson_3/vendor/symfony/console/Application.php:551
Symfony\Component\Console\Application->has() at /Users/tautvydas/PhpstormProjects/Lesson_3/vendor/symfony/console/Application.php:640
Symfony\Component\Console\Application->find() at /Users/tautvydas/PhpstormProjects/Lesson_3/vendor/symfony/framework-bundle/Console/Application.php:116
Symfony\Bundle\FrameworkBundle\Console\Application->find() at /Users/tautvydas/PhpstormProjects/Lesson_3/vendor/symfony/console/Application.php:254
Symfony\Component\Console\Application->doRun() at /Users/tautvydas/PhpstormProjects/Lesson_3/vendor/symfony/framework-bundle/Console/Application.php:82
Symfony\Bundle\FrameworkBundle\Console\Application->doRun() at /Users/tautvydas/PhpstormProjects/Lesson_3/vendor/symfony/console/Application.php:166
Symfony\Component\Console\Application->run() at /Users/tautvydas/PhpstormProjects/Lesson_3/bin/console:43
exit status 1"
I tried to remove the code from this lesson entirely and perform fixtures load command, but I still receive the same error. The problem is, I can't figure out what causes it. Is there a way to perform a command or smth and find out exactly where this problem arises?
Thank you very much in advance!
Hey Tautvydas!
Nice work adding the -vvv to get the stacktrace :). The error does, "sort of", show you were it's happening: there is apparently a foreach on line 170 of this file: /Users/tautvydas/PhpstormProjects/Lesson_3/vendor/doctrine/data-fixtures/lib/Doctrine/Common/DataFixtures/Loader.php. I would check that out... I can't see exactly what that is, because I'm not sure what the precise version is of this library that you're using.
My best guess is that it is this: https://github.com/doctrine/data-fixtures/blob/39e9777c9089351a468f780b01cffa3cb0a42907/lib/Doctrine/Common/DataFixtures/Loader.php#L170
And if I'm correct, then apparently one of your fixtures classes has a getDependencies()
method, which is not returning an array.
Let me know if that's what it is!
Cheers!
Hello, Ryan!
Indeed, I made a mistake in "getDependencies()", because I forgot to add the square brackets in the return statement. Thank you for your help!
Btw, I really enjoy learning from you, guys!
Hi, found little typo in getDependencies() should be plural ArticleFixtures ie.: return [ArticleFixture::class];
Hey Jindrich,
Thank you for reporting it! Could you clarify where exactly did you find that type? I mean, in the video? On what time? Or do you find it in the scripts below video? Or maybe in downloaded code? More context will help me to figure out where the problem is and I fix it.
Cheers!
Uh, oh. wrong thread.
It belongs to scripts bellow the video https://symfonycasts.com/sc...
Sorry for confusion.
Hey Jindrich,
Ah, now I see! Thank you for letting us know! Yes, we had fixed it in the code but forgot to fix it in code blocks. Not it's fixed there too in https://github.com/knpunive...
Cheers!
Hey there!
For me this part did not work:
if (!$comment->isDeleted()) {
$comments[] = $comment;
}
Schouldn't it be
$comment->getIsDeleted()
there?
Hey Andreas,
Thank you for this report! You're correct, it should be getIsDeleted() in PHP code. I fixed it in https://github.com/knpunive...
Cheers!
Hey Chris!
Woooops, my fault! Thank you for this report! I fixed it in https://github.com/knpunive...
Cheers!
// 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-paginator-bundle": "^2.7", // v2.7.2
"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
"twig/extensions": "^1.5" // v1.5.1
},
"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
}
}
Hello guys! Why didn't you use softdeletable behaviour from StofDoctrineExtenstion? Is there any reason?