Awesome Random Fixtures
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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeLook at ArticleFixtures
: we created 10 articles. So, the system has references from 0 to 9:
// ... 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)
:
// ... 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:
// ... 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:
// ... 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)
:
// ... 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
:
// ... 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!
// ... 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
:
// ... 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:
// ... lines 1 - 9 | |
class A0CommentFixture extends BaseFixture implements DependentFixtureInterface | |
{ | |
// ... lines 12 - 26 | |
public function getDependencies() | |
{ | |
// ... line 29 | |
} | |
} |
Just return an array with ArticleFixtures::class
:
// ... lines 1 - 9 | |
class A0CommentFixture extends BaseFixture implements DependentFixtureInterface | |
{ | |
// ... lines 12 - 26 | |
public function getDependencies() | |
{ | |
return [ArticleFixtures::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
:
// ... 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.
Hey Denis,
Could you explain exactly what you renamed to what? Like name and new name... It's important to know so we could be able to help you. Most probably you renamed it bad, e.g. class name does not match file name, etc. Or probably you renamed the class, but still reference the old name somewhere.
Cheers!