Alien Tech for Fixtures: Foundry & Faker
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 using src/DataFixtures/AppFixtures.php to create fake fixture data. This works fine. But where's the cool and fun? Do we really want to write manual code to add dozens or more entities? Points to you if you answered: hell no!
To take this from tedious to terrific, find your terminal and run:
Installing Foundry and Faker
composer require --dev foundry
Scroll up to see what was installed. The important packages are zenstruck/foundry - as way to create many entities quickly - and fakerphp/faker - a library to make fake data so we don't need to rely on lorem ipsum and our own lack of creativity.
Run
git status
to see what the recipes did: it enabled a bundle and added a config file. That config works well out of the box, so no need to look at it.
Creating a Starship Factory
With Foundry, every entity can have a factory class. To get these going run:
symfony console make:factory
This lists all entities that don't yet have a factory. Choose Starship and... success! It created a new StarshipFactory class. Go check that out: src/Factory/StarshipFactory.php.
This class will be really good at creating Starship objects - handy in case the Borg come back. First, look at this class() method. This tells Foundry which entity class this factory helps with. The defaults() is where we define default values to use when creating starships. I recommend adding defaults for all required fields: it'll make life easier.
Hey! Check out these self::faker() calls! This is how we generate random data. For name, captain and class, it's random text, status, is a random StarshipStatusEnum and arrivedAt defaults to any random date Since time travel still hasn't been invented, replace self::faker()->dateTime() with self::faker()->dateTimeBetween('-1 year', 'now'):
| // ... lines 1 - 11 | |
| final class StarshipFactory extends PersistentProxyObjectFactory | |
| { | |
| // ... lines 14 - 111 | |
| protected function defaults(): array|callable | |
| { | |
| return [ | |
| 'arrivedAt' => \DateTimeImmutable::createFromMutable(self::faker()->dateTimeBetween('-1 year', 'now')), | |
| // ... lines 116 - 119 | |
| ]; | |
| } | |
| // ... lines 122 - 131 | |
| } |
Faker's text() method will give us random text, but not necessarily interesting text. Instead of serving under Captain "apple pie breakfast", in the tutorial/ directory, copy these constants and paste them at the top of the factory class:
| // ... lines 1 - 11 | |
| final class StarshipFactory extends PersistentProxyObjectFactory | |
| { | |
| private const SHIP_NAMES = [ | |
| 'Nebula Drifter', | |
| 'Quantum Voyager', | |
| 'Starlight Nomad', | |
| // ... lines 18 - 44 | |
| ]; | |
| // ... line 46 | |
| private const CLASSES = [ | |
| 'Eclipse', | |
| 'Vanguard', | |
| 'Specter', | |
| // ... lines 51 - 57 | |
| ]; | |
| // ... line 59 | |
| private const CAPTAINS = [ | |
| 'Orion Stark', | |
| 'Lyra Voss', | |
| 'Cassian Drake', | |
| // ... lines 64 - 90 | |
| ]; | |
| // ... lines 92 - 131 | |
| } |
Then, for captain use randomElement(self::CAPTAINS). For class, randomElement(self::CLASSES) and for name, randomElement(self::SHIP_NAMES):
| // ... lines 1 - 11 | |
| final class StarshipFactory extends PersistentProxyObjectFactory | |
| { | |
| // ... lines 14 - 111 | |
| protected function defaults(): array|callable | |
| { | |
| return [ | |
| // ... line 115 | |
| 'captain' => self::faker()->randomElement(self::CAPTAINS), | |
| 'class' => self::faker()->randomElement(self::CLASSES), | |
| 'name' => self::faker()->randomElement(self::SHIP_NAMES), | |
| // ... line 119 | |
| ]; | |
| } | |
| // ... lines 122 - 131 | |
| } |
Using the Starship Factory
Time to use this factory! In src/DataFixtures/AppFixtures.php, in load(), write StarshipFactory::createOne(). Pass this an array of property values for the first ship: copy these from the existing code: name, class, captain, status and arrivedAt:
| // ... lines 1 - 9 | |
| class AppFixtures extends Fixture | |
| { | |
| public function load(ObjectManager $manager): void | |
| { | |
| StarshipFactory::createOne([ | |
| 'name' => 'USS LeafyCruiser (NCC-0001)', | |
| 'class' => 'Garden', | |
| 'captain' => 'Jean-Luc Pickles', | |
| 'status' => StarshipStatusEnum::IN_PROGRESS, | |
| 'arrivedAt' => new \DateTimeImmutable('-1 day'), | |
| ]); | |
| // ... lines 21 - 36 | |
| } | |
| } |
I'll paste the other two... and remove the old code:
| // ... lines 1 - 9 | |
| class AppFixtures extends Fixture | |
| { | |
| public function load(ObjectManager $manager): void | |
| { | |
| // ... lines 14 - 21 | |
| StarshipFactory::createOne([ | |
| 'name' => 'USS Espresso (NCC-1234-C)', | |
| 'class' => 'Latte', | |
| 'captain' => 'James T. Quick!', | |
| 'status' => StarshipStatusEnum::COMPLETED, | |
| 'arrivedAt' => new \DateTimeImmutable('-1 week'), | |
| ]); | |
| StarshipFactory::createOne([ | |
| 'name' => 'USS Wanderlust (NCC-2024-W)', | |
| 'class' => 'Delta Tourist', | |
| 'captain' => 'Kathryn Journeyway', | |
| 'status' => StarshipStatusEnum::WAITING, | |
| 'arrivedAt' => new \DateTimeImmutable('-1 month'), | |
| ]); | |
| } | |
| } |
Bonus! Remove the persist() and flush() calls: Foundry handles that for us!
Let's see what this does! Reload the fixtures:
symfony console doctrine:fixtures:load
Choose yes and... success! Back over, refresh and... it looks the same. That's a good sign! Now, let's create a fleet of ships!
Creating Many Starships
For the first three, we passed an array of values... but we didn't need to do that. If we don't pass a value, it'll use the StarshipFactory::defaults() method. Watch how dangerous this makes us: a Borg cube just showed up? Whip up 20 new ships with StarshipFactory::createMany(20):
| // ... lines 1 - 9 | |
| class AppFixtures extends Fixture | |
| { | |
| public function load(ObjectManager $manager): void | |
| { | |
| // ... lines 14 - 37 | |
| StarshipFactory::createMany(20); | |
| } | |
| } |
Back in the terminal, load the fixtures again:
symfony console doctrine:fixtures:load
And over in the app, refresh and... check it out! A whole fleet of ships here now, and yep, they all have random data!
Now that the fake data is looking more real, it makes me wonder: what if our app was running on a huge star base with hundreds or thousands of ships? This would be a long page. Next, we'll paginate these results into smaller chunks.
11 Comments
Hi,
Let's say we have two tables such that User and Lablist. And we created two factory for these tables.
How will we send data to these tables by using AppFixtures without affection both of them? If I add LablistFactory under UserFactory in below code, and if we load, all tables will be refreshed?
Hey @Mahmut-A,
Factory relations is what you're looking for. There are a couple ways to achieve but using your code above, it could be something like this:
I hope this helps!
--Kevin
Hi Kevin,
Maybe I explained wrong. There are two tables. one is user, second is lablist. I want to load their data to tables. As far as I understand, I can use only AppFixtures to load data. However, if I use UserFactory and LablistFactory in AppFixtures, then if I load by using fixtures:load command, both tables will be refreshed at each run.
I want to only load lablist, since user is alread loaded , I do not want to refresh it.
I want to add something. I created two Fixtures. AppFixtures and LablistFixtures by using command "symfony console make:fixtures LablistFixtures."
then, I am applying "php bin/console doctrine:fixtures:load --group=LablistFixtures" command to load data to lablist table. however, when I apply this, user table is being deleted.
if I use "php bin/console doctrine:fixtures:load --group=LablistFixtures --group=AppFixtures, all tables are refreshed and IDs are regenerated.
I want to only load to spesific tables not all of them.
Ok, I understand. You can have multiple fixture files: perhaps in your case, a
UserFixturesandLablistFixtures. By default, both will by loaded, but you can group them so they can be loadedindependently. Check the "Splitting Fixtures into Separate Files" documentation to see your options.
Hi,
Actually,splitting also refreshing. But I fixed my problem as below:
php bin/console doctrine:fixtures:load --group=LablistFixtures --appendwith this command, User tables is not refreshing.
However, we should be careful to not duplicate data in Lablist since we used --append. To prevent this, I added below lines in LabList.php class.
so now everything is perfect. thank you
`use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
#[UniqueEntity('labname')]
`
Glad to hear you have it working!
I think adjusting the purging behaviour could be used as an alternative to
--append.Hello,
I want to ask that how can I add new mail address to existing user table. As you see, there are mahmut.aydin mail adress and random 10 mail address. I want to add new one withouth deleting old data.
thank you
Hey Mahmut,
Then you did it right :) Did you try to reload your fixtures? It will create a mahmut.aydin@netsys.com.tr user and 10 more random users.
Or are you trying to add only that mahmut.aydin@netsys.com.tr without resetting your DB? Well, if so, that's a bit trickier. First of all, in theory, you should not care too much about the data locally, so you can just reload your fixtures that will do the trick eventually, but yeah, it will reset the DB so for example all your additional data you add to the DB after loaded the fixtures will gone. Alternative solution in your specific case - just register a new user with that email on the registration page if you have it 🤷♂️ That's the easiest way when you need to add just one more specific user. And finally, if you still need to do it with fixtures - run the same
bin/console doctrine:fixtures:loadcommand but with--appendoption:It will try to append new data w/o clearing the DB before. But it may cause some duplicated constraints fails, depends on how good your randomness is :) I would recommend you to comment out everything you don't need before running that command, e.g. comment out that
// UserFactory::createMany(10);.I hope that helps :)
Cheers!
thank you Victor. Actually, randomness is not important. I only wanted to learn how to add new data to existing table. I see need to use --append parameter to command.
thank you
Hey Mahmut,
Great! About randomness - it depends on your unique constraints in the DB. When you append data but if somehow during it the same email that already exists in your DB will be generated - the whole data load command will fail, that's just something you should keep in mind.
Cheers!
"Houston: no signs of life"
Start the conversation!