Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This tutorial has a new version, check it out!

Adding a Comment Entity

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

Hey friends! I mean, hello fellow space-traveling developer... friends. Welcome, to part two of our Doctrine tutorial where we talk all about... relationships. Oh, I love relationships, and there are so many beautiful types in the universe! Like, the relationship between two old friends, as they high-five after a grueling trip between solar systems. Or, the complex relationship between a planet and a moon: a perfect gravitational dance between BFF's. And of course, the most incredible type of relationship in all of the galaxy... database relationships.

Sure, we learned a ton about Doctrine in the first tutorial, but we completely avoided this topic! And it turns out, database relationships are pretty darn important if you want to build one of those "real" applications. So let's crush them.

Project Setup

As always, to have the best possible relationship with Doctrine, you should totally code along with me. Download the course code from this page. After you unzip the file, you'll find a start/ directory that will have the same code you see here. Check out the README.md file for setup instructions, and the answer to this KnpU space riddle:

My name sounds white & fluffy, but I'm not! And instead of blocking the sun, I orbit it.

Need to know the answer? Then download the course code! Anyways, the last setup step will be to open a terminal, move into the project directory and run:

php bin/console server:run

to start the built-in web server. Then, celebrate by finding your browser, and loading http://localhost:8000. Hello: The Space Bar! Our hot new app that helps spread real news to curious astronauts across the galaxy.

And thanks to the last tutorial, these articles are being loaded dynamically from the database. But... these comments at the bottom? Yea, those are still hardcoded. We need to fix that! And this will be our first relationship: each Article can have many Comments. But, more about that later.

Creating the Comment Entity

In the src/Entity directory, the only entity we have so far is Article:

... lines 1 - 8
/**
* @ORM\Entity(repositoryClass="App\Repository\ArticleRepository")
*/
class Article
{
... lines 14 - 157
}

So before we can talk about relationships, we first need to build a Comment entity. We could create this by hand, but the generator is so much nicer:

Open a new terminal tab and run:

php bin/console make:entity

Name the entity Comment. Then, for the fields, we need one for the author and one for the actual comment. Add authorName as a string field. And yea, someday, we might have a User table. And then, this could be a relationship to that table. But for now, keep it as a simple string.

Next, add content as a text field, and also say no to nullable. Hit enter one more time to finish up.

Oh, but before we generate the migration, go open the new Comment class:

... lines 1 - 2
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass="App\Repository\CommentRepository")
*/
class Comment
{
/**
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", length=255)
*/
private $authorName;
/**
* @ORM\Column(type="text")
*/
private $content;
public function getId()
{
return $this->id;
}
public function getAuthorName(): ?string
{
return $this->authorName;
}
public function setAuthorName(string $authorName): self
{
$this->authorName = $authorName;
return $this;
}
public function getContent(): ?string
{
return $this->content;
}
public function setContent(string $content): self
{
$this->content = $content;
return $this;
}
}

No surprises: id, authorName, content and some getter & setter methods. At the top of the class, let's add use TimestampableEntity:

... lines 1 - 5
use Gedmo\Timestampable\Traits\TimestampableEntity;
/**
* @ORM\Entity(repositoryClass="App\Repository\CommentRepository")
*/
class Comment
{
use TimestampableEntity;
... lines 14 - 59
}

That will give us $createdAt and $updatedAt fields.

Now head back to your terminal and run:

php bin/console make:migration

When that finishes, go find the new file. We just want to make sure that this doesn't contain any surprises. For example, if you're working on multiple branches, then your database may be out-of-sync before you run make:migration. If that happens, the migration file would contain extra changes that you'll want to remove. In this case, it looks great:

... lines 1 - 2
namespace DoctrineMigrations;
use Doctrine\DBAL\Migrations\AbstractMigration;
use Doctrine\DBAL\Schema\Schema;
/**
* Auto-generated Migration: Please modify to your needs!
*/
class Version20180426184910 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('CREATE TABLE comment (id INT AUTO_INCREMENT NOT NULL, author_name VARCHAR(255) NOT NULL, content LONGTEXT NOT NULL, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
}
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('DROP TABLE comment');
}
}

Go back to your terminal and, migrate!

php bin/console doctrine:migrations:migrate

Perfect! We have an article table and now a comment table. But, they are not friends yet. Time to add a relation!

Leave a comment!

34
Login or Register to join the conversation
Julio N. Avatar
Julio N. Avatar Julio N. | posted 3 years ago

Hello,

When I try running php bin/console doctrine:migrations:migrate using the code on the /start folder provided here I get the following error:

PHP Fatal error: Class 'Doctrine\DBAL\Migrations\AbstractMigration' not found in /spacebar_relations/src/Migrations/Version20180413174059.php on line 11
17:41:11 CRITICAL [php] Fatal Error: Class 'Doctrine\DBAL\Migrations\AbstractMigration' not found ["exception" => Symfony\Component\Debug\Exception\FatalErrorException { …}]

Attempted to load class "AbstractMigration" from namespace "Doctrine\DBAL\Migrations".
Did you forget a "use" statement for "Doctrine\Migrations\AbstractMigration"?

Please note this was after running composer install and composer update as well for good measure. It may be worth noting I am running this inside a docker container with a mount point.

Thank you.

1 Reply
Denis N. Avatar

I have the same problem. Did you solve it?

Reply
Julio N. Avatar

I have not, will keep trying today and update in case I am able to.

Reply

Hi guys!

The doctrine/migrations library recently released a version 2.0. My guess is that you are getting this new version, which has some backwards compatibility breaks that are breaking the migrations. There are a few options:

1) Don't update the dependencies - it should work if you use the starting code without updating
2) If you DO update, change the use statement in all of the migrations classes to use Doctrine\Migrations\AbstractMigration;. You may need other changes - we're checking into it. But you can see the changes described here: https://github.com/doctrine...

Please let us know if you have any other problems, or if you have success! Sorry about the problems - new library versions always mess things up :).

Cheers!

1 Reply
Denis N. Avatar

In my case work option 1.

But when I tried:
php bin/console make:migration

I had this message:
Command "make:migration" is not defined.

To generate migration I used:
php bin/console doctrine:migrations:generate

And for migrate:
php bin/console doctrine:migrations:migrate

Thanks for the help.

Reply

Hey Dennis,

Yes, that's because we use MakerBundle v.1.0.2 in the code, but "make:migration" command was added in v1.1, see https://github.com/symfony/... . If you need this command, you can update only maker-bundle dependency with:

$ composer up symfony/maker-bundle

And then you will have this "make:migration" command.

Cheers!

Reply
Denis N. Avatar

Ok, thank you for your help.

Reply

Hey Dennis,

Actually, that was a bug. Now I see we use this command in the screencast, sorry! We messed up with our dependencies, but now it should be fixed.

Cheers!

Reply
Julio N. Avatar

Confirming option 1) did work! thanks a lot for the help :)

Reply
Abelardo Avatar
Abelardo Avatar Abelardo | posted 1 year ago

Hi there!

How to manage a bunch of comments with this approaching? A cache-based system? How could I manage nested comments up to three levels, for example? Is it preferable to connect to Disqus API instead?

Reply

Hey AbelardoLG,

It's a bit more complex, but in theory you would just need a new relation, e.g. Comment::$parent fields that will be OneToMany to itself. And so, the inverse side will be Comment::$children field for example. When you will render a comment, you will also need to render all its child comments if exist. It will give you an infinite level of nesting. About performance - you would better need to profile this solution and see your bottle-necks.

Or yes, you can use Disqus API instead, it's already a ready-to-use solution and requires almost zero coding your your side, but it may be not that flexible as custom solution. So, it depends on how you will need to sue those comments, etc.

Cheers!

1 Reply
Abelardo Avatar

Hi Victor.
First of all, thanks for replying asap.
Secondly, the relationship is 1:N, where 1 parent holds several children comments. More levels of nesting, worst will be the treatment to solve this complicated world of comments.
Yes, you are right! :)

Cheers and, again, thanks for replying to my question! Best regards.

Reply

Hey AbelardoLG,

You're welcome! And another tip for you: If you want to solve that problem in a more advanced way - take a look at Tree behaviour from StofDoctrineExtensionsBundle or KnpLabs/DoctrineBehaviors bundle - that behaviour fits your needs I think :)

Cheers!

1 Reply

When I started and follow the readme I get "An exception occurred in driver: SQLSTATE[HY000] [2002] Connection refused" when running "php bin/console doctrine:database:create
" from readme file

Reply

Hey Axel,

Look like you have incorrect database credentials in .env files. First of all, in this course we use MySQL database, do you have it installed? If so - great, you probably just need to tweak your DB credentials. You can do it in .env.local files. First, open the .env file and find the DATABASE_URL line, copy/paste it into .env.local and tweak it to match *your* DB credentials and try to create the DB with that "php bin/console doctrine:database:create" command again. Does it work? Btw, you can modify .env file directly, but keep in mind that that file will be committed to your repo.

I hope this helps!

Cheers!

Reply
Default user avatar
Default user avatar Paul Rijke | posted 3 years ago | edited

Hi there great explanation. I was wondering how to solve the use case if a Comment can be a child of different entities. For example if a comment could be a child of Article (as in your example) AND a comment can be set on a User (or any other entity).

How should that be resolved then?

Reply

Hey Paul Rijke

Do you will have different kind of comments? or only one base class? If it's the last one, then what you can do is to create a Comment entity agnostic of its relationships. In other words the Article entity will hold the relationship to Comments (At database level, the Article table will store the comment id). Does it makes any sense to you?

Cheers!

Reply
Default user avatar

That could be indeed. When I think a little ahead, it might be handy to determine to which parent a comment belongs.

I.e.. when I make a notification to a user on a comment (ideally with @mentions if amybody knows a bundle) then the notified person should be able to see to which parent a comment belongs to understand the context.

For example I have a Insurance Policy with a Claim child entity. When I comment on that Claim something like @user1 can you pick this one up.

Also on the parent entity (the Insurance or even the grand parent the Policy Holder) it needs to be possible to aggregate the comments down the hierarchy.

Does this make sense as well?

Reply

Oh, I think I understand, you really need to have a field like $owner on the comment entity. In that case what you can do (kind of ugly) is to have a field per each possible owner, in other words, the Comment entity would have a field related to Article and another one to User. This is an easy and pragmatic way to achieve what you want but I would only recommend it if you will have at most 3 or 4 parents.

Another approach that might work is to use Doctrine Inheritance mapping. It allows you to manage hierarchies. Docs: https://www.doctrine-projec...

Or, you can just create a separate table per each kind of comment, like for example UserComment, or ArticleComment

There might be other solutions but any solution has its own drawbacks, so, as always, it depends on your application needs :)

Cheers!

Reply
Default user avatar

Yeah, in the meantime I studied the codebase of SuiteCRM on github. Their Notes and Task module can be attached to different entities as well. They do it by creating uuid's for the entities and set parent_id to that uuid with an extra field parent_type (which contains Invoice or Quote or whatever entity). I think you're suggestiong more or less the same?

Studied yesterday the Single Type Inherritance and Class Type Inheritance, but I did not understand it enough to envision it for my use case. Hoped that this was a "standard" problem with a "standard" solution. Even looked into the FOSCommentBundle, but that links to (twig) pages instead of entities.

I think the first approach is maybe the best (or someone can point me to the holy grail). Next step is to figure it out using doctrine.

Really appreciate thinking with me Diego (and others)

Reply

> They do it by creating uuid's for the entities and set parent_id to that uuid with an extra field parent_type (which contains Invoice or Quote or whatever entity). I think you're suggestiong more or less the same?

Yeah! That's another way to go. But as I said, you have to consider the pros and cons of the solution and choose the best one for your application. Usually, I try to don't overthink the solution and just be pragmatic, buuut you need to add tests because you may want in the future to change your current implementation

Reply
Sergey Avatar

Cannot download script of this course. Nettwork error.

Reply

Hey Sergey!

Sorry about that! Temporary server issue - should be better now!

Cheers!

Reply
Yaroslav Y. Avatar
Yaroslav Y. Avatar Yaroslav Y. | posted 3 years ago

I believe you should update the course codebase - because when I run

./bin/console make:entity

only entity and its repository get created; the dialog to add fields to the entity is never shown.
Still, after I run

composer update

and then launch again

./bin/console make:entity

the dialog to add fields to the entity shows up, as expected.

Reply

Hey Yaroslav Y.!

Ah, thank you for pointing this out! We actually take great care to get all the dependency versions correct and inline with the video. In this case, we did that but then I (literally, it was me) messed something up recently, which caused maker bundle to be set at an earlier version. I've just fixed that.

About the doctrine/migrations issue, you have the correct fix (thanks for sharing). We'll be adding a note or fix about this soon. The problem is that doctrine/migrations v2 recently came out. If you use the version packaged with this tutorial, you're fine. But if you upgrade to v2, then you hit this issue.

Again, thanks for raising this bug with the code download so we could fix it! If you notice anything else weird, please let us know.

Cheers!

1 Reply
Yaroslav Y. Avatar

...and the update of vendors code, run by the composer, seem to break one of migrations:

13:05:47 CRITICAL [php] Fatal Error: Class 'Doctrine\DBAL\Migrations\AbstractMigration' not found ["exception" => Symfony\Component\Debug\Exception\FatalErrorException { …}]

In Version20180414171443.php line 11:

Attempted to load class "AbstractMigration" from namespace "Doctrine\DBAL\Migrations".
Did you forget a "use" statement for "Doctrine\Migrations\AbstractMigration"?

I looked a bit deeper and discovered this problem touches all existing migrations; to fix it, you should update all existing migration files as follows:

1. replace
use Doctrine\DBAL\Migrations\AbstractMigration;
with
use Doctrine\Migrations\AbstractMigration;

2. add 'void' return type to up() and down() methods of each migration

Should work as expected after that.

Respect.

Reply
Yaroslav Y. Avatar

btw, if I'm not mistaken, the same problem occurs with the codebase of previous tutorial 'Doctrine & The Database' https://symfonycasts.com/sc... but I'm not 100% sure about it

Reply
Default user avatar
Default user avatar Андрей Николаев | posted 4 years ago

Hi!
It is a great command line tool to create Entities and all other tasks for doctrine!
Where I can get it? ...how to install it?
I'm using ZF3 + Doctrine... But as I can see some Symfony modules could be used as well.
Please, help!

Reply

Hi Andrey,

If you're talking about console in general - you can install it even in ZF, see Symfony Console Component: https://github.com/symfony/... . What about that tool for creating entities/migrations we use in this screencast - that's something special to Symfony, because it comes from MakerBundle, see https://github.com/symfony/... - but bundles are kinda special for Symfony because they have some configuration to inject the code into Symfony framework.

Though, for Doctrine Migrations see https://github.com/doctrine... - that's a standalone library that does not relate to Symfony.

Cheers!

1 Reply
Default user avatar
Default user avatar Андрей Николаев | victor | posted 4 years ago

:( thanx.

I can't understand what is the issue to get details through OneToMany.... the collection is empty... The field has type PersistentCollection at the runtime (in the debugger)...

Reply

Hey Андрей Николаев

So you fetched an entity with a OneToMany relationship but its collection is empty? Probably you only have to add some items to the collection. The "PersistentCollection" type is a special type of collection that allows you to remove records (items) from the DB when you do some changes to it and the call $entityManager->flush()

Reply
Dmitriy Avatar
Dmitriy Avatar Dmitriy | posted 4 years ago

Hello. Help me please.

I have the entities - User, FacebookUser, GoogleUser. FacebookUser and GoogleUser must extend User entity.



Is it possible to implement this with the help of Doctrine ORM? How can I extend entities in Symfony?

Reply

Hey Dmitriy,

Yes, it's possible, but this relates to Doctrine not Symfony since we're talking about ORM. There're a few way to do so, each has its own pros and cons, see the docs for more information and examples: https://www.doctrine-projec...

Cheers!

Reply
Dmitriy Avatar

Thank you, Victor. It is very useful.

Reply
Cat in space

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

The course is built on Symfony 4, but the principles still apply perfectly to Symfony 5 - not a lot has changed in the world of relations!

What PHP libraries does this tutorial use?

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