Buy

Awesome Random Fixtures

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

Login Subscribe

Look at ArticleFixtures: we created 10 articles. So, the system has references from 0 to 9:

66 lines src/DataFixtures/ArticleFixtures.php
... 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
}
}

In CommentFixture, spice things up: replace the 0 with $this->faker->numberBetween(0, 9):

26 lines src/DataFixtures/CommentFixture.php
... lines 1 - 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.'_'.$this->faker->numberBetween(0, 9)));
});
... lines 22 - 23
}
}

Try the fixtures again:

php bin/console doctrine:fixtures:load

No errors! And... check the database:

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

That is much better! Just like that, each comment is related to a random article!

Making this Random Reference System Reusable

I really like this idea, where we can fetch random objects in our fixtures. So, let's make it easier! In BaseFixture, add a new private property on top called $referencesIndex. Set that to an empty array:

62 lines src/DataFixtures/BaseFixture.php
... lines 1 - 9
abstract class BaseFixture extends Fixture
{
... lines 12 - 17
private $referencesIndex = [];
... lines 19 - 60
}

I'm adding this because, at the bottom of this class, I'm going to paste in a new, method that I prepared. It's a little ugly, but this new getRandomReference() does exactly what its name says: you pass it a class, like the Article class, and it will find a random Article for you:

62 lines src/DataFixtures/BaseFixture.php
... lines 1 - 9
abstract class BaseFixture extends Fixture
{
... lines 12 - 17
private $referencesIndex = [];
... lines 19 - 41
protected function getRandomReference(string $className) {
if (!isset($this->referencesIndex[$className])) {
$this->referencesIndex[$className] = [];
foreach ($this->referenceRepository->getReferences() as $key => $ref) {
if (strpos($key, $className.'_') === 0) {
$this->referencesIndex[$className][] = $key;
}
}
}
if (empty($this->referencesIndex[$className])) {
throw new \Exception(sprintf('Cannot find any references for class "%s"', $className));
}
$randomReferenceKey = $this->faker->randomElement($this->referencesIndex[$className]);
return $this->getReference($randomReferenceKey);
}
}

That's super friendly!

In CommentFixture, use it: $comment->setArticle() with $this->getRandomReference(Article::class):

26 lines src/DataFixtures/CommentFixture.php
... lines 1 - 8
class CommentFixture extends BaseFixture
{
protected function loadData(ObjectManager $manager)
{
$this->createMany(Comment::class, 100, function(Comment $comment) {
... lines 14 - 19
$comment->setArticle($this->getRandomReference(Article::class));
});
... lines 22 - 23
}
}

To make sure my function works, try the fixtures one last time:

php bin/console doctrine:fixtures:load

And, query for the comments:

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

Brilliant!

Fixture Ordering

There is one last minor problem with our fixtures... they only work due to pure luck. Check this out: I'll right click on CommentFixture and rename the class to A0CommentFixture:

32 lines src/DataFixtures/A0CommentFixture.php
... lines 1 - 9
class A0CommentFixture extends BaseFixture implements DependentFixtureInterface
{
... lines 12 - 30
}

Also allow PhpStorm to rename the file. Some of you might already see the problem. Try the fixtures now:

php bin/console doctrine:fixtures:load

Bah! Explosion!

Cannot find any references to App\Entity\Article

The error comes from BaseFixture and it basically means that no articles have been set into the reference system yet!

62 lines src/DataFixtures/BaseFixture.php
... lines 1 - 9
abstract class BaseFixture extends Fixture
{
... lines 12 - 41
protected function getRandomReference(string $className) {
... lines 43 - 52
if (empty($this->referencesIndex[$className])) {
throw new \Exception(sprintf('Cannot find any references for class "%s"', $className));
}
... lines 56 - 59
}
}

You can see the problem in the file tree. We have not been thinking at all about what order each fixture class is executed. By default, it loads them alphabetically. But now, this is a problem! The A0CommentFixture class is being loaded before ArticleFixtures... which totally ruins our cool system!

You can also see this in the terminal: it loaded A0CommentFixture first.

DependentFixtureInterface

The solution is pretty cool. As soon as you have a fixture class that is dependent on another fixture class, you need to implement an interface called DependentFixtureInterface:

32 lines src/DataFixtures/A0CommentFixture.php
... lines 1 - 6
use Doctrine\Common\DataFixtures\DependentFixtureInterface;
... lines 8 - 9
class A0CommentFixture extends BaseFixture implements DependentFixtureInterface
{
... lines 12 - 30
}

This will require you to have one method. Move to the bottom, then, go to the "Code" -> "Generate" menu, or Command + N on a Mac, select "Implement Methods" and choose getDependencies(). I'll add the public before the function:

32 lines src/DataFixtures/A0CommentFixture.php
... lines 1 - 9
class A0CommentFixture extends BaseFixture implements DependentFixtureInterface
{
... lines 12 - 26
public function getDependencies()
{
... line 29
}
}

Just return an array with ArticleFixtures::class:

32 lines src/DataFixtures/A0CommentFixture.php
... lines 1 - 9
class A0CommentFixture extends BaseFixture implements DependentFixtureInterface
{
... lines 12 - 26
public function getDependencies()
{
return [ArticleFixture::class];
}
}

That's it! Load them again:

php bin/console doctrine:fixtures:load

Bye bye error! It loaded ArticleFixtures first and then the comments below that. The fixtures library looks at all of the dependencies and figures out an order that makes sense.

With that fixed, let's rename the class back from this ridiculous name to CommentFixture:

32 lines src/DataFixtures/CommentFixture.php
... lines 1 - 9
class CommentFixture extends BaseFixture implements DependentFixtureInterface
{
... lines 12 - 30
}

To celebrate, move over, refresh and... awesome! 8, random comments. We rock!

Next, let's learn about some tricks to control how Doctrine fetches the comments for an article, like, their order.

Leave a comment!

  • 2018-07-27 Diego Aguiar

    Ohh, you were trying to get the reference of a reference... yeah, it sound's like the system may behave weird

  • 2018-07-27 Angelos Koulouris

    Hi.. I am getting this in th console when i use in my fixture the when i use the

    getRandomReference(Article::calss). (edit: Woooorks!!)

    Booooooo!!!!!
    -------------------------------------------------------------------------------
    In ReferenceRepository.php line 167: Warning:

    Illegal offset type in isset or empty

    --------------------------------------------------------------------------------
    using the getReference(Article::class . '_' . $this->faker->numberBetween(0, 9) Woooorks!!

    I'm on symfony 4.1.2. upgraded today...
    because I had a Deprecation Log Message in the Profiler:
    ----------------------------------------------------------------------------------------
    User Deprecated: Doctrine\Common\ClassLoader is deprecated.
    ----------------------------------------------------------------------------------------

    Am I the only I see it???
    What may I do wrong...?
    _______________________________________________________________
    edit:
    I found what i did..
    geReference( getRandomReference() )

    -- NEVER do that --

  • 2018-07-16 weaverryan

    Hey Luis Mas!

    Yea, I think this certainly makes sense :). I was really "working around" the fact that the normal addReference() method doesn't give us any "hook" or easy way to "fetch" references out (it's just one big array). But, we could set referencesIndex() from inside of createMany() to avoid needing to build this array later :). I wouldn't worry too much about a method that sets & gets data - the referencesIndex() is more of an "internal cache" to help us out. However, certainly your suggestion makes sense!

    Cheers!

  • 2018-07-14 Luis Mas

    Hi!!!

    For me is confusing a method that sets and gets data. Wouldn't be clearer to have two methods?

    1. protected function pushToReferencesIndex(string $className, $entity): void that would call to addReference and add the key to the reference index:


    {
    $className.'_'.$i, $entity;
    $this->addReference($key);
    if (!isset($this->referencesIndex[$className])) {
    $this->referencesIndex[$className] = [];
    }
    $this->referencesIndex[$className][] = $key;
    }

    2. protected function getRandomReference(string $className): object that would only have this lines:


    {
    if (empty($this->referencesIndex[$className])) {
    throw new \Exception(sprintf('Cannot find any references for class "%s"', $className));
    }
    $randomReferenceKey = $this->faker->randomElement($this->referencesIndex[$className]);
    return $this->getReference($randomReferenceKey);
    }

    Or maybe just leave things equal and not do some over-engineering?

  • 2018-05-11 Matt Johnson

    Thank you!

  • 2018-05-10 Matt Johnson

    Thank you!

  • 2018-05-10 weaverryan

    Haha, hey again Matt Johnson! I'm also happy to post our version, fwiw. It looks like this:


    abstract class BaseFixture extends Fixture
    {
    // ...
    private $referencesIndex = [];

    // ...

    protected function getRandomReference(string $className) {
    if (!isset($this->referencesIndex[$className])) {
    $this->referencesIndex[$className] = [];

    foreach ($this->referenceRepository->getReferences() as $key => $ref) {
    if (strpos($key, $className.'_') === 0) {
    $this->referencesIndex[$className][] = $key;
    }
    }
    }

    if (empty($this->referencesIndex[$className])) {
    throw new \Exception(sprintf('Cannot find any references for class "%s"', $className));
    }

    $randomReferenceKey = $this->faker->randomElement($this->referencesIndex[$className]);

    return $this->getReference($randomReferenceKey);
    }
    }

    Cheers!

  • 2018-05-10 Matt Johnson

    If anyone's like me and going through this before it's finished and would like an example for the getRandomReference function, here you go:

    protected function getRandomReference($class)
    {
    $all = $this->manager->getRepository($class)->findAll();
    return $all[array_rand($all)];
    }