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

Saving Relations

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.

Our Comment entity has an article property and an article_id column in the database:

... lines 1 - 10
class Comment
{
... lines 13 - 31
/**
* @ORM\ManyToOne(targetEntity="App\Entity\Article", inversedBy="comments")
* @ORM\JoinColumn(nullable=false)
*/
private $article;
... lines 37 - 77
}

So, the question now is: how do we actually populate that column? How can we relate a Comment to an Article?

The answer is both very easy, and also, quite possibly, at first, weird! Open up the ArticleFixtures class. Let's hack in a new comment object near the bottom: $comment1 = new Comment():

... lines 1 - 5
use App\Entity\Comment;
... lines 7 - 8
class ArticleFixtures extends BaseFixture
{
... lines 11 - 27
public function loadData(ObjectManager $manager)
{
$this->createMany(Article::class, 10, function(Article $article, $count) use ($manager) {
... lines 31 - 56
$article->setAuthor($this->faker->randomElement(self::$articleAuthors))
... lines 58 - 59
;
$comment1 = new Comment();
... lines 63 - 65
});
... lines 67 - 68
}
}

Then, $comment1->setAuthorName(), and we'll go copy our favorite, always-excited astronaut commenter: Mike Ferengi:

... lines 1 - 8
class ArticleFixtures extends BaseFixture
{
... lines 11 - 27
public function loadData(ObjectManager $manager)
{
$this->createMany(Article::class, 10, function(Article $article, $count) use ($manager) {
... lines 31 - 61
$comment1 = new Comment();
$comment1->setAuthorName('Mike Ferengi');
... lines 64 - 65
});
... lines 67 - 68
}
}

Then, $comment1->setContent(), and use one of our hardcoded comments:

... lines 1 - 8
class ArticleFixtures extends BaseFixture
{
... lines 11 - 27
public function loadData(ObjectManager $manager)
{
$this->createMany(Article::class, 10, function(Article $article, $count) use ($manager) {
... lines 31 - 61
$comment1 = new Comment();
$comment1->setAuthorName('Mike Ferengi');
$comment1->setContent('I ate a normal rock once. It did NOT taste like bacon!');
... line 65
});
... lines 67 - 68
}
}

Perfect! Because we're creating this manually, we need to persist it to Doctrine. At the top, use the $manager variable:

... lines 1 - 8
class ArticleFixtures extends BaseFixture
{
... lines 11 - 27
public function loadData(ObjectManager $manager)
{
$this->createMany(Article::class, 10, function(Article $article, $count) use ($manager) {
... lines 31 - 65
});
... lines 67 - 68
}
}

Then, $manager->persist($comment1):

... lines 1 - 8
class ArticleFixtures extends BaseFixture
{
... lines 11 - 27
public function loadData(ObjectManager $manager)
{
$this->createMany(Article::class, 10, function(Article $article, $count) use ($manager) {
... lines 31 - 61
$comment1 = new Comment();
$comment1->setAuthorName('Mike Ferengi');
$comment1->setContent('I ate a normal rock once. It did NOT taste like bacon!');
$manager->persist($comment1);
});
... lines 67 - 68
}
}

If we stop here, this is a valid Comment... but it is NOT related to any article. In fact, go to your terminal, and try the fixtures:

php bin/console doctrine:fixtures:load

JoinColumn & Required Foreign Key Columns

Boom! It fails with an integrity constraint violation:

Column article_id cannot be null

It is trying to create the Comment, but, because we have not set the relation, it doesn't have a value for article_id:

... lines 1 - 10
class Comment
{
... lines 13 - 31
/**
* @ORM\ManyToOne(targetEntity="App\Entity\Article", inversedBy="comments")
* @ORM\JoinColumn(nullable=false)
*/
private $article;
... lines 37 - 77
}

Oh, and also, in Comment, see this JoinColumn with nullable=false? That's the same as having nullable=false on a property: it makes the article_id column required in the database. Oh, but, for whatever reason, a column defaults to nullable=false, and JoinColumn defaults to the opposite: nullable=true.

Setting the Article on the Comment

ANYways, how can we relate this Comment to the Article? By calling $comment1->setArticle($article):

... lines 1 - 8
class ArticleFixtures extends BaseFixture
{
... lines 11 - 27
public function loadData(ObjectManager $manager)
{
$this->createMany(Article::class, 10, function(Article $article, $count) use ($manager) {
... lines 31 - 61
$comment1 = new Comment();
$comment1->setAuthorName('Mike Ferengi');
$comment1->setContent('I ate a normal rock once. It did NOT taste like bacon!');
$comment1->setArticle($article);
$manager->persist($comment1);
});
... lines 68 - 69
}
}

And that's it! This is both the most wonderful and strangest thing about Doctrine relations! We do not say setArticle() and pass it $article->getId(). Sure, it will ultimately use the id in the database, but in PHP, we only think about objects: relate the Article object to the Comment object.

Once again, Doctrine wants you to pretend like there is no database behind the scenes. Instead, all you care about is that a Comment object is related to an Article object. You expect Doctrine to figure out how to save that.

Copy that entire block, paste, and use it to create a second comment to make things a bit more interesting: $comment2. Copy a different dummy comment and paste that for the content:

... lines 1 - 8
class ArticleFixtures extends BaseFixture
{
... lines 11 - 27
public function loadData(ObjectManager $manager)
{
$this->createMany(Article::class, 10, function(Article $article, $count) use ($manager) {
... lines 31 - 61
$comment1 = new Comment();
$comment1->setAuthorName('Mike Ferengi');
$comment1->setContent('I ate a normal rock once. It did NOT taste like bacon!');
$comment1->setArticle($article);
$manager->persist($comment1);
$comment2 = new Comment();
$comment2->setAuthorName('Mike Ferengi');
$comment2->setContent('Woohoo! I\'m going on an all-asteroid diet!');
$comment2->setArticle($article);
$manager->persist($comment2);
});
... lines 74 - 75
}
}

And now, let's see if it works! Reload the fixtures:

php bin/console doctrine:fixtures:load

No errors! Great sign! Let's dig into the database:

php bin/console doctrine:query:sql 'SELECT * FROM comment'

There it is! We have 20 comments: 2 for each article. And the article_id for each row is set!

This is the beauty of Doctrine: we relate objects in PHP, never worrying about the foreign key columns. But of course, when we save, it stores things exactly like it should.

Next, let's learn how to fetch related data, to get all of the comments for a specific Article.

Leave a comment!

23
Login or Register to join the conversation

Hey @Rayen, hey there ,

Doctrine encourage to take over and control transaction demarcation ourself! So how to deal with it? Can you tell me a "good" use-case when to you use it?

Big thanks!

Reply

Hey Houssem!

Are you referring to database transactions? If so, excellent question!

First, Doctrine automatically wraps its operations in a transaction. You can actually see this in one of our videos - https://symfonycasts.com/sc... - just hit "play" and you'll see how Doctrine has wrapped the INSERT query in a transaction. So if you're inserting 2 related objects at once, and one fails, both will be rolled back. You don't even need to think about it.

But, you *can* manually start and commit a transaction if you want. That's useful if, for example, you need to save something to the database AND send some data to an external API... and if the API call fails, you want to "roll back" your database query. We don't have any videos on this, but it's fairly straightforward: https://www.doctrine-projec...

Let me know if that helps!

Cheers!

Reply
Default user avatar
Default user avatar A Mastou | posted 2 years ago

Hey guys, I'm a little bit confused about two things. Why we persist $comment and not persist $article ? The second stuff is about this use($manager) I didn't know this syntax, where can I have more details about this use inside a method .

Reply

Hey Mastou,

Why persist($comment)? Because we just created that Comment object. So, basically, if you create a new entity, to say Doctrine to store that object into DB you need to call persist on that object first, otherwise that object just won't be stored in the DB, Doctrine will just ignore them. So, the strategy is the next: create a new entity object, fill in some data on it, them call persist() on it to say Doctrine to save this object on next flush() call, and actually call flush() so doctrine execute queries and save everything that was modified into the DB.

To know more about that "use($manager)" syntax - google anonymous PHP functions, or just see this link to PHP docs: https://www.php.net/manual/... . Basically, it helps to pass some context to that anonymous function, i.e. in our case we allow to use that $manager object *inside* that function.

I hope this is clearer for you now!

Cheers!

1 Reply
Brent Avatar

I have a user and user_account tables, the id of the user is a foreign key in the user_account, basically a join table (one user to many user accounts). I have mapped the relationships in my two entities (user, useraccount), correctly I believe. Before persisting, in my current setup a user id does not yet exist. What I'm trying to do is create the user then create the useraccount with the id of the user just created in one persist/flush. I keep getting an error that id is null when persisting the useraccount. If my these entities are related properly can I create both at the same time? I hoping what I am asking makes sense. Thank you!

Reply

Hey brentmtc,

First of all, you can check if your mapping is correct with "bin/console doctrine:schema:validate" command. Suppose, it is correct, then it should just work. You just need to persist both entities, i.e. call "->persist($user)" and "->persist($userAccount)" and only then call one "->flush()" that will save both entities to the DB.

If you still have this problem, hm, then what exactly id is null in the error? UserAccount::id or UserAccount::userId? Please, check your id property declaration, does it looks like this in both User and UserAccount entities?


/**
* @var integer
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;

Cheers!

Reply
Brent Avatar

Thank you. I was able to get this working by adding a user_id field to my user_account table.

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

I believe you should update the codebase you provide for this tutorial - folder 'start'

ArticleFixtures::loadData() does not use faker for generating article content, but uses hard-coded text instead. This might be by purpose, but using faker for all properties would be much nicer, if you ask me.

Not critical, but still confusing a bit.

Reply

Hey again Yaroslav Y.!

Can you explain a bit more? Do you just mean that it's a bit inconsistent to use Faker for many of the properties, but use hardcoded text for the article content field? If so, you're right - but it was also done on purpose - we include some markdown formatting in that string, so we can see that markdown processing is working, and also include a few words that we check for later for a feature. Basically, we wanted to control the article content... but the downside is that (you're right) it's not random anymore.

Cheers!

1 Reply
Symfonybeard Avatar
Symfonybeard Avatar Symfonybeard | posted 4 years ago

This tutorial worked well. But there is one small issue by selecting the data from the database with php bin/console doctrine:query:sql 'SELECT * FROM comment'
I am getting this error: Too many arguments, expected arguments "command" "sql".

Reply

Hey Symfonybeard ,

I suppose you're on Windows OS. Try to use double quotes instead, i.e.:

php bin/console doctrine:query:sql "SELECT * FROM comment"

Does it help? :)

Cheers!

Reply
Symfonybeard Avatar
Symfonybeard Avatar Symfonybeard | victor | posted 4 years ago

Great, this works! Thanks for your help. :)))

Reply

Glad it works! Yeah, a tricky thing about Windows console I learned from other users :)

Cheers!

Reply
Will H. Avatar
Will H. Avatar Will H. | posted 4 years ago

I really appreciate how the end of each video kind of teases the subject of the next video. It piques my curiosity and motivates me to continue to the next topic. It's a nice touch!

Reply

Ha! Perfect! That's why we do it... but it's nice to have feedback that it's working! Keep going!!!! :D

Cheers!

Reply

Hello guys,

do you also get the deprecation log message "User Deprecated: Doctrine\Common\ClassLoader is deprecated." ?

Cheers

Reply

Hey fahani!

Yep! It's nothing to worry about. There is some deprecation things happening right now... which should go away when doctrine/orm releases their next version. You can happily ignore this ;).

Cheers!

2 Reply
Default user avatar
Default user avatar toporovvv | posted 4 years ago

$this->createMany(Article::class, 10, function(Article $article, $count) use ($manager) {


Variable $count here is not used and can be removed.

Reply

Hey toporovvv ,

You're totally right! Thanks for letting us know. Probably not a big deal, we've kept it so you can easily modify our example and see that there's an extra $count argument which you can use if you need.

Cheers!

Reply
GDIBass Avatar
GDIBass Avatar GDIBass | posted 4 years ago

Anyone trying this... I had to add a use to my callback function:

> $this->createMany(Article::class, 10, function(Article $article, $count) use ($manager) {

Reply

Hey GDIBass

If you have to use a local variable inside a callback function, then you have to specify it by doing what you did. The compiler requires to keep track of the reference of that variable some how :)

Cheers!

Reply
GDIBass Avatar

Yup. I didn't see another way of adding comments referencing the article while also having $manager to persist.

Reply

I know it's not obvious yet, because (at this moment) the video & code blocks aren't released yet, but yes, we do this *exact* thing - the script contains:

> At the top, use the $manager variable

So, you're doing it totally right :). Later, we'll cleanup things a bit and put Comments into their own fixture class. But for right now, that use is needed to get the $manager variable in scope so we can persist the comments.

Cheres!

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