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

Fixture References & Relating Objects

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $10.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Having just one fixture class that loads articles and comments... and eventually other stuff, is not super great for organization:

... 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();
... lines 63 - 67
$comment2 = new Comment();
... lines 69 - 72
});
... lines 74 - 75
}
}

Let's give the comments their own home. First, delete the comment code from here:

... 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 - 60
});
... lines 62 - 63
}
}

Then, find your terminal and run:

php bin/console make:fixture

Call it CommentFixture.

Flip back to your editor and open that file!

... lines 1 - 2
namespace App\DataFixtures;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Common\Persistence\ObjectManager;
class CommentFixture extends Fixture
{
public function load(ObjectManager $manager)
{
// $product = new Product();
// $manager->persist($product);
$manager->flush();
}
}

In the last tutorial, we made a cool base class with some extra shortcuts. Extend BaseFixture:

... lines 1 - 6
class CommentFixture extends BaseFixture
{
... lines 9 - 15
}

Then, instead of load, we now need loadData(), and it should be protected. Remove the extra use statement on top:

... lines 1 - 4
use Doctrine\Common\Persistence\ObjectManager;
class CommentFixture extends BaseFixture
{
protected function loadData(ObjectManager $manager)
{
... lines 11 - 14
}
}

Thanks to our custom base class, we can create a bunch of comments easily with $this->createMany(), passing it Comment::class, 100, and then a callback that will receive each 100 Comment objects:

... lines 1 - 4
use App\Entity\Comment;
... lines 6 - 7
class CommentFixture extends BaseFixture
{
protected function loadData(ObjectManager $manager)
{
$this->createMany(Comment::class, 100, function(Comment $comment) {
... lines 13 - 18
});
$manager->flush();
}
}

Inside, let's use Faker - which we also setup in the last tutorial - to give us awesome, fake data. Start with $comment->setContent(). I'll use multiple lines:

... lines 1 - 7
class CommentFixture extends BaseFixture
{
protected function loadData(ObjectManager $manager)
{
$this->createMany(Comment::class, 100, function(Comment $comment) {
$comment->setContent(
... line 14
);
... lines 16 - 18
});
... lines 20 - 21
}
}

Now, if $this->faker->boolean, which will be a random true or false, then either generate a random paragraph: $this->faker->paragraph, or generate two random sentences. Pass true to get this as text, not an array:

... lines 1 - 7
class CommentFixture extends BaseFixture
{
protected function loadData(ObjectManager $manager)
{
$this->createMany(Comment::class, 100, function(Comment $comment) {
$comment->setContent(
$this->faker->boolean ? $this->faker->paragraph : $this->faker->sentences(2, true)
);
... lines 16 - 18
});
... lines 20 - 21
}
}

Cool! Next, for the author, we can use $comment->setAuthor() with $this->faker->name, to get a random person's name:

... lines 1 - 7
class CommentFixture extends BaseFixture
{
protected function loadData(ObjectManager $manager)
{
$this->createMany(Comment::class, 100, function(Comment $comment) {
$comment->setContent(
$this->faker->boolean ? $this->faker->paragraph : $this->faker->sentences(2, true)
);
$comment->setAuthorName($this->faker->name);
... line 18
});
... lines 20 - 21
}
}

By the way, all of these faker functions are covered really well in their docs. I'm seriously not just making them up.

Finally, add $comment->setCreatedAt() with $this->faker->dateTimeBetween() from -1 months to -1 seconds:

... lines 1 - 7
class CommentFixture extends BaseFixture
{
protected function loadData(ObjectManager $manager)
{
$this->createMany(Comment::class, 100, function(Comment $comment) {
$comment->setContent(
$this->faker->boolean ? $this->faker->paragraph : $this->faker->sentences(2, true)
);
$comment->setAuthorName($this->faker->name);
$comment->setCreatedAt($this->faker->dateTimeBetween('-1 months', '-1 seconds'));
});
... lines 20 - 21
}
}

That'll give us much more interesting data.

Using the Reference System

At this point, this is a valid Comment object... we just haven't related it to an Article yet. We know how to do this, but... the problem is that all of the articles are created in a totally different fixture class. How can we get access to them here?

Well, one solution would be to use the entity manager, get the ArticleRepository, and run some queries to fetch out the articles.

But, that's kinda lame. So, there's an easier way. Look again at the BaseFixture class, specifically, the createMany() method:

... lines 1 - 9
abstract class BaseFixture extends Fixture
{
... lines 12 - 27
protected function createMany(string $className, int $count, callable $factory)
{
for ($i = 0; $i < $count; $i++) {
$entity = new $className();
$factory($entity, $i);
$this->manager->persist($entity);
// store for usage later as App\Entity\ClassName_#COUNT#
$this->addReference($className . '_' . $i, $entity);
}
}
}

It's fairly simple, but it does have one piece of magic: it calls $this->addReference() with a key, which is the entity class name, an underscore, then an integer that starts at zero and counts up for each loop. For the second argument, it passes the object itself.

This reference system is a little "extra" built into Doctrine's fixtures library. When you add a "reference" from one fixture class, you can fetch it out in another class. It's super handy when you need to relate entities. And hey, that's exactly what we're trying to do!

Inside CommentFixture, add $comment->setArticle(), with $this->getReference() and pass it one of those keys: Article::class, then _0:

... lines 1 - 4
use App\Entity\Article;
... lines 6 - 8
class CommentFixture extends BaseFixture
{
protected function loadData(ObjectManager $manager)
{
$this->createMany(Comment::class, 100, function(Comment $comment) {
... lines 14 - 19
$comment->setArticle($this->getReference(Article::class.'_0'));
});
... lines 22 - 23
}
}

PhpStorm is complaining about a type-mismatch, but this will totally work. Try it! Find your terminal and run:

php bin/console doctrine:fixtures:load

No errors! That's a great sign! Check out the database:

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

Yes! 100 comments, and each is related to the exact same article.

Relating to Random Articles

So, success! Except that this isn't very interesting yet. All our comments are related to the same one article? Come on!

Let's spice things up by relating each comment to a random article. And, learn about when we need to implement a DependentFixtureInterface.

Leave a comment!

24
Login or Register to join the conversation

When I run "php bin/console doctrine:fixtures:load", I get:

In ReferenceRepository.php line 141:

Reference to: (App\DataFixtures\Article_0) does not exist

However, doing some tiny debugging, I found out that if I change this line:
$comment->setArticle($this->getReference(Article::class.'_0'));
to:
$comment->setArticle($this->getReference('App\Entity\Article_0'));

It totally works. Am I missing something?

3 Reply

Hey Daniel,

Yeah, you missed the namespace, add "use App\Entity\Article;" line in the beginning of the src/DataFixtures/CommentFixture.php file.

Cheers!

5 Reply
Christian S. Avatar
Christian S. Avatar Christian S. | Victor | posted 4 years ago

I added the namespace but still receive the same error as Daniel (also Daniels solution doesn't work for me).
However, I renamed my "Article" class to "Drops" (as "Drop" wasn't allowed with doctrine), so might this be an order-issue (CommentFixture being executed first while no related Drops exist yet)?

Update: I got the answer from the next tutorial and it was as I expected the order of the names :)

1 Reply

Though I managed, I cant see how to make the thing random, did anyone managed to randomize that _0 so it loops through the entity related, and get random ids?

1 Reply

Hey JLChafardet,

Haha, well done! :)

Cheers!

1 Reply

I'm facing a different hurdle though now,

I'm playing with an idea thats just for the heck of it but, nontheless, I'm the kind that likes to even make my time-spenders to work properly.

I do write (amateurish at best) fiction work, so I decided to write a small app, that will allow me to store fictions.

so I have for now 3 tables in my db, 3 entities, Author, Novel, Chapter.

One Author can have written many novels, One novel can have many chapters. Fairly straightforward.

the complication comes here in "Chapters", at least when it comes to fixtures. as every chapter has a "number" (Chapter 1, 2, 3, 4 etc), so 3 novels, can have a chapter 1, or a chapter 300.

that "number" has to be incremental but not automatically. every new record, it should "get the last inserted value for "number" and increment it by one.

do you got an idea on how to do this through fixtures? by form is somewhat simpler, as all I need is to bring that information when the form is rendered, but in fixtures (to test) is a whole different story.

Reply

Hey JLChafardet,

I think I got it. So, it should be pretty straightforward with fixtures I think. Just iterate over all Novels and in a loop create as many chapters as you want. Like for every new Novel you will have a new loop that will start from 1 so all chapters will start from 1 to some number, you can use faker to generate a random number that will be the last chapter. Does it sound good for you? :)

Cheers!

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

Ctrl + Opt + O
removes unused 'use' statements

but I prefer
Cmd + Opt + L
as it also auto-formats the code along the way

just FYI

1 Reply
Akavir S. Avatar
Akavir S. Avatar Akavir S. | posted 3 years ago

Hello,

I just dont understand how 'references' from the function 'create many' are stored. What is the entity which is actually owning them, i cant find any parameter call reference where they could be stored.

Reply

Hey Akavir S.!

Let me try to give some hints... but let me know if it still doesn't make sense ;)

Check out the code block here - https://symfonycasts.com/screencast/doctrine-relations/awesome-random-fixtures#codeblock-00ae1a5ad6 - you can click to expand the entire code block to see the entire file. The key is this line inside createMany()``

$this->addReference($className . '_' . $i, $entity);



It's that simple. If you create an App\Entity\Article object, then this stores a reference called App\Entity\Article_5 (for example). But... I think this might *not* quite be your question. This "reference" system has nothing to do with Doctrine relations. It's purely a "Doctrine fixtures" feature where you can "place" certain objects you create and then "get" them from that place in other parts of your fixtures. You usually do this so that you can add a relation. On a low-level, it basically allows you to do this:

1) Create an entity
2) Do some other code
3) Ask the fixtures: Hey! Can you give me the object I made in step (1) - I want to relate it to some other object.

That's the idea :). Does that help? Or did I miss your question entirely... very possible ;).

Cheers!
1 Reply
Akavir S. Avatar

Yeah it helped a lot !

Thanks you for taking the time to answer me !

You guys are doing amazing job
I wish you prosperity <3

Reply
javmc Avatar
javmc Avatar javmc | posted 4 years ago | edited

Hello,

I've found this tutorial very useful, as always. However I have a question.

My entity Comment has several properties. The ParentType property could be a Review or a FAQ. How would you handle the fixtures so you create 50 Comment fixtures with ParentType set to Review and 50 Comment fixtures with ParentType set to FAQ? When I try to have 2 different Fixture Classes, like CommentWithReviewParentFixtures and CommentWithFaqParentFixtures one overwrites the other, same if I try to modify your code to have 2 callable factories in it.

Reply

Is the comment record being overwritten or what's been overwritten? If the problem is the reference, then you could move the logic for creating references into a protected method and in your Fixture class you just overwrite it

Cheers!

Reply
akincer Avatar
akincer Avatar akincer | posted 4 years ago

Getting a similar error below which carries over to the next chapter:

Reference to: (App\Entity\Post_0) does not exist

Note: I changed "Article" to "Post" in the very beginning of the first tutorial for reasons unrelated to this discussion.

Reply
 Avatar
 Avatar Diego Aguiar | akincer | posted 4 years ago

Hey Aaron

Did you update the file name as well?

Cheers!

Reply
akincer Avatar

All files were created by hand while following along, I just always replace "Post" in place of "Article" to avoid a name mismatch. I don't think that's it. Just validated there were no name mismatches anywhere.

Reply

Great! I can see in your other comments that you could fix your problem
Cheers & happy learnings!

Reply

Hey Aaron!

Hmm. So, a few things to check. Even after you finish the next chapter, I would double check that your fixtures are being loaded in order. Specifically, we're looking to see if the PostFixture is loaded before you see this error (you should be able to determine this by the console output). That would be possible reason #1 for the error: that the CommentFixture is running before PostFixture has a chance to add in the fixture references.

Thing #2 to check is that the references are being added correctly in PostFixture. The fancy createMany() should handle this, but something might going wrong. To check, at the end of the PostFixture::loadData() method, try:


var_dump($this->getReference('App\Entity\Post_0'));

If everything is working properly, that should return something. If there is a big error on that line (you can see the exact line of an error by adding -vvv at the end of the fixtures command) then we know the problem is somewhere in the PostFixture.

Cheers!

Reply
akincer Avatar
akincer Avatar akincer | weaverryan | posted 4 years ago | edited

Yes, this was the solution for anyone else that runs into a similar issue:

Change your BaseFixture declaration to this:

abstract class BaseFixture extends Fixture implements OrderedFixtureInterface

Then add this function to each fixture class:

`public function getOrder()

{
    // This fixture would be loaded first. For the fixture you want loaded second, add this function and return 2
    // The third would return 3 and so on ...
    return 1;
}`
Reply
akincer Avatar

Actually -- it looks like there's a better way to solve this in the next chapter, but you can use this in the interim if you're following along and just want to make this work for the moment.

Reply
akincer Avatar

Looks like maybe use "implements OrderedFixtureInterface" for BaseFixture and then some special sauce somewhere or another?

Reply
akincer Avatar

Definitely looks like Comment is loading before Post. I presume there's some fancy footwork to get Post to load before Comment.

> purging database
> loading App\DataFixtures\AppFixtures
> loading App\DataFixtures\CommentFixture

BTW -- these training courses are awesome and I REALLY wish I knew about them before I spent an embarrassing amount of time trying to figure out some very basic things.

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