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!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-iconv": "*",
        "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.2.7
        "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
    }
}