Fixtures: Seeding Dummy Data!
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 SubscribeWe're creating our dummy article data in a really... uh... dummy way: with a, sort of, "secret" endpoint that creates an almost-identical article, each time we go there:
// ... lines 1 - 10 | |
class ArticleAdminController extends AbstractController | |
{ | |
/** | |
* @Route("/admin/article/new") | |
*/ | |
public function new(EntityManagerInterface $em) | |
{ | |
$article = new Article(); | |
$article->setTitle('Why Asteroids Taste Like Bacon') | |
->setSlug('why-asteroids-taste-like-bacon-'.rand(100, 999)) | |
->setContent(<<<EOF | |
Spicy **jalapeno bacon** ipsum dolor amet veniam shank in dolore. Ham hock nisi landjaeger cow, | |
lorem proident [beef ribs](https://baconipsum.com/) aute enim veniam ut cillum pork chuck picanha. Dolore reprehenderit | |
labore minim pork belly spare ribs cupim short loin in. Elit exercitation eiusmod dolore cow | |
**turkey** shank eu pork belly meatball non cupim. | |
Laboris beef ribs fatback fugiat eiusmod jowl kielbasa alcatra dolore velit ea ball tip. Pariatur | |
laboris sunt venison, et laborum dolore minim non meatball. Shankle eu flank aliqua shoulder, | |
capicola biltong frankfurter boudin cupim officia. Exercitation fugiat consectetur ham. Adipisicing | |
picanha shank et filet mignon pork belly ut ullamco. Irure velit turducken ground round doner incididunt | |
occaecat lorem meatball prosciutto quis strip steak. | |
Meatball adipisicing ribeye bacon strip steak eu. Consectetur ham hock pork hamburger enim strip steak | |
mollit quis officia meatloaf tri-tip swine. Cow ut reprehenderit, buffalo incididunt in filet mignon | |
strip steak pork belly aliquip capicola officia. Labore deserunt esse chicken lorem shoulder tail consectetur | |
cow est ribeye adipisicing. Pig hamburger pork belly enim. Do porchetta minim capicola irure pancetta chuck | |
fugiat. | |
EOF | |
); | |
// publish most articles | |
if (rand(1, 10) > 2) { | |
$article->setPublishedAt(new \DateTime(sprintf('-%d days', rand(1, 100)))); | |
} | |
$article->setAuthor('Mike Ferengi') | |
->setHeartCount(rand(5, 100)) | |
->setImageFilename('asteroid.jpeg') | |
; | |
$em->persist($article); | |
$em->flush(); | |
return new Response(sprintf( | |
'Hiya! New Article id: #%d slug: %s', | |
$article->getId(), | |
$article->getSlug() | |
)); | |
} | |
} |
Honestly, it's not great for development: every article on the homepage pretty much looks the same.
Yep, our dummy data sucks. And, that's important! If we could load a rich set of random data easily, we could develop and debug faster.
Installing DoctrineFixturesBundle
To help with this, we'll use a great library called DoctrineFixturesBundle... but with our own spin to make things really fun.
First let's get it installed. Find your terminal and run
composer require orm-fixtures:3.0.2 --dev
And yep, this is a Flex alias, and we're using --dev
because this tool will help us load fake data for development... which is not something we need in our production code. If you've ever accidentally replaced the production database with dummy data... you know what I mean.
Generating Fixture with make:fixtures
Perfect! When it finishes, generate our first fixture class by running:
php bin/console make:fixtures
Call it ArticleFixtures
. It's fairly common to have one fixture class per entity, or sometimes, per group of entities. And... done!
Go check it out: src/DataFixtures/ArticleFixtures.php
:
// ... lines 1 - 2 | |
namespace App\DataFixtures; | |
use Doctrine\Bundle\FixturesBundle\Fixture; | |
use Doctrine\Common\Persistence\ObjectManager; | |
class ArticleFixtures extends Fixture | |
{ | |
public function load(ObjectManager $manager) | |
{ | |
// $product = new Product(); | |
// $manager->persist($product); | |
$manager->flush(); | |
} | |
} |
The idea behind fixtures is dead simple: step (1) we write code to create and save objects, and then step (2), we run a new console command that executes all of our fixture classes.
Writing the Fixtures
Open ArticleAdminController
: let's start stealing some code! Copy all of our dummy article code, go back to the fixture class, and paste! We need to re-type the e
on Article
and hit tab so that PhpStorm adds the use
statement for us on top:
// ... lines 1 - 4 | |
use App\Entity\Article; | |
// ... lines 6 - 8 | |
class ArticleFixtures extends Fixture | |
{ | |
public function load(ObjectManager $manager) | |
{ | |
$article = new Article(); | |
// ... lines 14 - 48 | |
} | |
} |
Then, at the bottom, the entity manager variable is called $manager
:
// ... lines 1 - 8 | |
class ArticleFixtures extends Fixture | |
{ | |
public function load(ObjectManager $manager) | |
{ | |
$article = new Article(); | |
// ... lines 14 - 45 | |
$manager->persist($article); | |
$manager->flush(); | |
} | |
} |
Back in the controller, just put a die('todo')
for now:
// ... lines 1 - 10 | |
class ArticleAdminController extends AbstractController | |
{ | |
/** | |
* @Route("/admin/article/new") | |
*/ | |
public function new(EntityManagerInterface $em) | |
{ | |
die('todo'); | |
// ... lines 19 - 24 | |
} | |
} |
Someday, we'll create a proper admin form here.
And... that's it! It's super boring and it only creates one article... but it should work! Try it: find your terminal and run a new console command:
php bin/console doctrine:fixtures:load
This will ask if you want to continue because - important note! - the command will empty the database first, and then load fresh data. Again, not something you want to run on production... not saying I've done that before.
When it finishes, find your browser, and refresh. It works!
Creating Multiple Articles
But... come on! We're going to need more than one article! How can we create multiple? First, we're going to do it the easy... but, kinda boring way. A for
loop! Say: for $i = 0; $i < 10;
$i++. And, all the way at the bottom, add the ending curly brace. We need to call persist()
in the loop, but we only need to call flush()
once at the end.
Cool! Try it again:
php bin/console doctrine:fixtures:load
Then, refresh! Awesome! Except that the articles are still totally boring and identical... we'll talk about that in the next chapter.
BaseFixture Class for Cooler Looping
But first, let me show you a cooler way to create multiple articles. In the DataFixtures
directory, create a new class called BaseFixture
. Make it abstract, and extend the normal class that all fixtures extend... so... Fixture
:
// ... lines 1 - 2 | |
namespace App\DataFixtures; | |
use Doctrine\Bundle\FixturesBundle\Fixture; | |
// ... lines 6 - 7 | |
abstract class BaseFixture extends Fixture | |
{ | |
// ... lines 10 - 20 | |
} |
Here's the idea: this will not be a fixture class that the bundle will execute. Instead, it will be a base class with some cool helper methods. To start, copy the load()
method and implement it here. Re-type ObjectManager
to get its use
statement:
// ... lines 1 - 2 | |
namespace App\DataFixtures; | |
// ... lines 4 - 5 | |
use Doctrine\Common\Persistence\ObjectManager; | |
abstract class BaseFixture extends Fixture | |
{ | |
// ... lines 10 - 14 | |
public function load(ObjectManager $manager) | |
{ | |
// ... lines 17 - 19 | |
} | |
} |
Oh, ObjectManager
is an interface implemented by EntityManager
, it's not too important: just think "this is the entity manager".
Next, and this won't make sense yet, create a private $manager
property, and set it inside the load()
method:
// ... lines 1 - 7 | |
abstract class BaseFixture extends Fixture | |
{ | |
// ... line 10 | |
private $manager; | |
// ... lines 12 - 14 | |
public function load(ObjectManager $manager) | |
{ | |
$this->manager = $manager; | |
// ... lines 18 - 19 | |
} | |
} |
Finally, create an abstract protected function
called loadData()
with that same ObjectManager
argument:
// ... lines 1 - 7 | |
abstract class BaseFixture extends Fixture | |
{ | |
// ... lines 10 - 12 | |
abstract protected function loadData(ObjectManager $manager); | |
// ... lines 14 - 20 | |
} |
Back in load()
, call this: $this->loadData($manager)
:
// ... lines 1 - 7 | |
abstract class BaseFixture extends Fixture | |
{ | |
// ... lines 10 - 14 | |
public function load(ObjectManager $manager) | |
{ | |
// ... lines 17 - 18 | |
$this->loadData($manager); | |
} | |
} |
So far, this doesn't do anything special. Back in ArticleFixtures
, extend the new BaseFixture
instead. I'll also cleanup the extra use
statement:
// ... lines 1 - 4 | |
use App\Entity\Article; | |
use Doctrine\Common\Persistence\ObjectManager; | |
class ArticleFixtures extends BaseFixture | |
{ | |
// ... lines 10 - 48 | |
} |
Now, instead of implementing load()
, implement loadData()
and make it protected:
// ... lines 1 - 7 | |
class ArticleFixtures extends BaseFixture | |
{ | |
public function loadData(ObjectManager $manager) | |
{ | |
// ... lines 12 - 47 | |
} | |
} |
And... yea! The fixture system will call load()
on BaseFixture
, that will call loadData()
on ArticleFixtures
and... well... everything will work exactly like before.
Adding the createMany Method
So... why did we just do this? Go back to the BaseFixture
class and, at the bottom, I'm going to paste in a little method that I created:
// ... lines 1 - 7 | |
abstract class BaseFixture extends Fixture | |
{ | |
// ... lines 10 - 21 | |
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); | |
} | |
} | |
} |
Oh, and to make PhpStorm happy, at the top, add some PHPDoc that the $manager
property is an ObjectManager
instance:
// ... lines 1 - 5 | |
use Doctrine\Common\Persistence\ObjectManager; | |
abstract class BaseFixture extends Fixture | |
{ | |
/** @var ObjectManager */ | |
private $manager; | |
// ... lines 12 - 32 | |
} |
Anyways, say hello to createMany()
! A simple method that we can call to create multiple instances of an object. Here's the idea: we call createMany()
and pass it the class we want to create, how many we want to create, and a callback method that will be called each time an object is created. That'll be our chance to load that object with data.
Basically, it does the for
loop for us... which is not a huge deal, except for two nice things. First, it calls persist()
for us, so we don't have to:
// ... lines 1 - 7 | |
abstract class BaseFixture extends Fixture | |
{ | |
// ... lines 10 - 21 | |
protected function createMany(string $className, int $count, callable $factory) | |
{ | |
for ($i = 0; $i < $count; $i++) { | |
// ... lines 25 - 27 | |
$this->manager->persist($entity); | |
// ... lines 29 - 30 | |
} | |
} | |
} |
Ok, cool, but not amazing. But, this last line is cool:
// ... lines 1 - 7 | |
abstract class BaseFixture extends Fixture | |
{ | |
// ... lines 10 - 21 | |
protected function createMany(string $className, int $count, callable $factory) | |
{ | |
for ($i = 0; $i < $count; $i++) { | |
// ... lines 25 - 28 | |
// store for usage later as App\Entity\ClassName_#COUNT# | |
$this->addReference($className . '_' . $i, $entity); | |
} | |
} | |
} |
It won't matter yet, but in a future tutorial, we will have multiple fixtures classes. When we do, we will need to be able to reference objects created in one fixture class from other fixture classes. By calling addReference()
, all of our objects are automatically stored and ready to be fetched with a key that's their class name plus the index number.
The point is: this is going to save us some serious work... but not until the next tutorial.
Back in ArticleFixtures
, use the new method: $this->createMany()
passing it Article::class
, 10, and a function:
// ... lines 1 - 7 | |
class ArticleFixtures extends BaseFixture | |
{ | |
public function loadData(ObjectManager $manager) | |
{ | |
$this->createMany(Article::class, 10, function(Article $article, $count) { | |
// ... lines 13 - 43 | |
}); | |
// ... lines 45 - 46 | |
} | |
} |
This will have two args: the Article
that was just created and a count of which article this is. Inside the method, we can remove the $article = new Article()
, and instead of a random number on the slug, we can use $count
:
// ... lines 1 - 7 | |
class ArticleFixtures extends BaseFixture | |
{ | |
public function loadData(ObjectManager $manager) | |
{ | |
$this->createMany(Article::class, 10, function(Article $article, $count) { | |
$article->setTitle('Why Asteroids Taste Like Bacon') | |
->setSlug('why-asteroids-taste-like-bacon-'.$count) | |
->setContent(<<<EOF | |
Spicy **jalapeno bacon** ipsum dolor amet veniam shank in dolore. Ham hock nisi landjaeger cow, | |
lorem proident [beef ribs](https://baconipsum.com/) aute enim veniam ut cillum pork chuck picanha. Dolore reprehenderit | |
labore minim pork belly spare ribs cupim short loin in. Elit exercitation eiusmod dolore cow | |
**turkey** shank eu pork belly meatball non cupim. | |
Laboris beef ribs fatback fugiat eiusmod jowl kielbasa alcatra dolore velit ea ball tip. Pariatur | |
laboris sunt venison, et laborum dolore minim non meatball. Shankle eu flank aliqua shoulder, | |
capicola biltong frankfurter boudin cupim officia. Exercitation fugiat consectetur ham. Adipisicing | |
picanha shank et filet mignon pork belly ut ullamco. Irure velit turducken ground round doner incididunt | |
occaecat lorem meatball prosciutto quis strip steak. | |
Meatball adipisicing ribeye bacon strip steak eu. Consectetur ham hock pork hamburger enim strip steak | |
mollit quis officia meatloaf tri-tip swine. Cow ut reprehenderit, buffalo incididunt in filet mignon | |
strip steak pork belly aliquip capicola officia. Labore deserunt esse chicken lorem shoulder tail consectetur | |
cow est ribeye adipisicing. Pig hamburger pork belly enim. Do porchetta minim capicola irure pancetta chuck | |
fugiat. | |
EOF | |
); | |
// publish most articles | |
if (rand(1, 10) > 2) { | |
$article->setPublishedAt(new \DateTime(sprintf('-%d days', rand(1, 100)))); | |
} | |
$article->setAuthor('Mike Ferengi') | |
->setHeartCount(rand(5, 100)) | |
->setImageFilename('asteroid.jpeg') | |
; | |
}); | |
// ... lines 45 - 46 | |
} | |
} |
At the bottom, the persist isn't hurting anything, but it's not needed anymore.
Finish the end with a closing parenthesis and a semicolon:
// ... lines 1 - 7 | |
class ArticleFixtures extends BaseFixture | |
{ | |
public function loadData(ObjectManager $manager) | |
{ | |
$this->createMany(Article::class, 10, function(Article $article, $count) { | |
$article->setTitle('Why Asteroids Taste Like Bacon') | |
->setSlug('why-asteroids-taste-like-bacon-'.$count) | |
->setContent(<<<EOF | |
Spicy **jalapeno bacon** ipsum dolor amet veniam shank in dolore. Ham hock nisi landjaeger cow, | |
lorem proident [beef ribs](https://baconipsum.com/) aute enim veniam ut cillum pork chuck picanha. Dolore reprehenderit | |
labore minim pork belly spare ribs cupim short loin in. Elit exercitation eiusmod dolore cow | |
**turkey** shank eu pork belly meatball non cupim. | |
Laboris beef ribs fatback fugiat eiusmod jowl kielbasa alcatra dolore velit ea ball tip. Pariatur | |
laboris sunt venison, et laborum dolore minim non meatball. Shankle eu flank aliqua shoulder, | |
capicola biltong frankfurter boudin cupim officia. Exercitation fugiat consectetur ham. Adipisicing | |
picanha shank et filet mignon pork belly ut ullamco. Irure velit turducken ground round doner incididunt | |
occaecat lorem meatball prosciutto quis strip steak. | |
Meatball adipisicing ribeye bacon strip steak eu. Consectetur ham hock pork hamburger enim strip steak | |
mollit quis officia meatloaf tri-tip swine. Cow ut reprehenderit, buffalo incididunt in filet mignon | |
strip steak pork belly aliquip capicola officia. Labore deserunt esse chicken lorem shoulder tail consectetur | |
cow est ribeye adipisicing. Pig hamburger pork belly enim. Do porchetta minim capicola irure pancetta chuck | |
fugiat. | |
EOF | |
); | |
// publish most articles | |
if (rand(1, 10) > 2) { | |
$article->setPublishedAt(new \DateTime(sprintf('-%d days', rand(1, 100)))); | |
} | |
$article->setAuthor('Mike Ferengi') | |
->setHeartCount(rand(5, 100)) | |
->setImageFilename('asteroid.jpeg') | |
; | |
}); | |
$manager->flush(); | |
} | |
} |
So, it's a little bit fancier, and it'll save that important reference for us. Let's try it! Reload the fixtures again:
php bin/console doctrine:fixtures:load
No errors! Refresh the homepage: ah, our same, boring list of 10 identical articles. In the next chapter, let's use an awesome library called Faker to give each article rich, unique, realistic data.
what's the difference between
use Doctrine\Persistence\ObjectManager; and use Doctrine\Common\Persistence\ObjectManager;