Buy Access to Course
20.

Foundry: Fixtures You'll Love

|

Share this awesome video!

|

Building fixtures is pretty simple, but kind of boring. And it would be super boring to manually create 25 mixes inside the load() method. That's why we're going to install an awesome library called "Foundry". To do that, run:

composer require zenstruck/foundry --dev

We're using --dev because we only need this tool when we're developing or running tests. When this finishes, run

git status

to see that the recipe enabled a bundle and also created one config file... which we won't need to look at.

Factories: make:factory

In short, Foundry helps us create entity objects. It's... almost easier just to see it in action. First, for each entity in your project (right now, we only have one), you'll need a corresponding factory class. Create that by running

php bin/console make:factory

which is a Maker command that comes from Foundry. Then, you can select which entity you want to create a factory for... or generate a factory for all your entities. We'll generate one for VinylMix. And... that created a single file: VinylMixFactory.php. Let's go check it out: src/Factory/VinylMixFactory.php.

65 lines | src/Factory/VinylMixFactory.php
// ... lines 1 - 10
/**
* @extends ModelFactory<VinylMix>
*
* @method static VinylMix|Proxy createOne(array $attributes = [])
* @method static VinylMix[]|Proxy[] createMany(int $number, array|callable $attributes = [])
// ... lines 16 - 27
*/
final class VinylMixFactory extends ModelFactory
{
// ... lines 31 - 37
protected function getDefaults(): array
{
return [
// TODO add your default values here (https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#model-factories)
'title' => self::faker()->text(),
'trackCount' => self::faker()->randomNumber(),
'genre' => self::faker()->text(),
'votes' => self::faker()->randomNumber(),
'slug' => self::faker()->text(),
'createdAt' => null, // TODO add DATETIME ORM type manually
'updatedAt' => null, // TODO add DATETIME ORM type manually
];
}
// ... lines 51 - 63
}

Cool! Above the class, you can see a bunch of methods being described... which will help our editor know what super-powers this has. This factory is really good at creating and saving VinylMix objects... or creating many of them, or finding a random one, or a random set, or a random range. Phew!

getDefaults()

The only important code that we see inside this class is getDefaults(), which returns default data that should be used for each property when a VinylMix is created. We'll talk more about that in a minute.

But first... let's run blindly forward and use this class! In AppFixtures, delete everything and replace it with VinylMixFactory::createOne().

19 lines | src/DataFixtures/AppFixtures.php
// ... lines 1 - 5
use App\Factory\VinylMixFactory;
// ... lines 7 - 9
class AppFixtures extends Fixture
{
public function load(ObjectManager $manager): void
{
VinylMixFactory::createOne();
$manager->flush();
}
}

That's it! Spin over and reload the fixtures with:

symfony console doctrine:fixtures:load

And... it fails! Boo

Expected argument type "DateTime", "null" given at property path "createdAt"

It's telling us that something tried to call setCreatedAt() on VinylMix... but instead of passing a DateTime object, it passed null. Hmm. Inside of VinylMix, if you scroll up and open TimestampableEntity, yup! We have a setCreatedAt() method that expects a DateTime object. Something called this... but passed null.

This actually helps show off how Foundry works. When we call VinylMixFactory::createOne(), it creates a new VinylMix and then sets all of this data onto it. But remember, all of these properties are private. So it doesn't set the title property directly. Instead, it calls setTitle() and setTrackCount() Down here, for createdAt and updatedAt, it called setCreatedAt() and passed it null.

In reality, we don't need to set these two properties because they will be set automatically by the timestampable behavior.

If we try this now...

symfony console doctrine:fixtures:load

It works! And if we go check out our site... awesome. This mix has 928,000 tracks, a random title, and 301 votes. All of this is coming from the getDefaults() method.

Fake Data with Faker

To generate interesting data, Foundry leverages another library called "Faker", whose only job is to... create fake data. So if you want some fake text, you can say self::faker()->, followed by whatever you want to generate. There are many different methods you can call on faker() to get all kinds of fun fake data. Super handy!

Creating Many Objects

Our factory did a pretty good job... but let's customize things to make it a bit more realistic. Actually, first, having one VinylMix still isn't very useful. So instead, inside AppFixtures, change this to createMany(25).

19 lines | src/DataFixtures/AppFixtures.php
// ... lines 1 - 11
public function load(ObjectManager $manager): void
{
VinylMixFactory::createMany(25);
// ... lines 15 - 16
}
// ... lines 18 - 19

This is where Foundry shines. If we reload our fixtures now:

symfony console doctrine:fixtures:load

With a single line of code, we have 25 random fixtures to work with! Though, the random data could be a bit better... so let's improve that.

Customizing getDefaults()

Inside VinylMixFactory, change the title. Instead of text() - which can sometimes be a wall of text, change to words()... and let's use 5 words, and pass true so it returns this as a string. Otherwise, the words() method returns an array. For trackCount, we do want a random number, but... probably a number between 5 and 20. For genre, let's go for a randomElement() to randomly choose either pop or rock. Those are the two genres that we've been working with so far. And, whoops... make sure you call this like a function. There we go. Finally, for votes, choose a random number between -50 and 50.

62 lines | src/Factory/VinylMixFactory.php
// ... lines 1 - 28
final class VinylMixFactory extends ModelFactory
{
// ... lines 31 - 37
protected function getDefaults(): array
{
return [
'title' => self::faker()->words(5, true),
'trackCount' => self::faker()->numberBetween(5, 20),
'genre' => self::faker()->randomElement(['pop', 'rock']),
'votes' => self::faker()->numberBetween(-50, 50),
'slug' => self::faker()->text(),
];
}
// ... lines 48 - 60
}

Much better! Oh, and you can see that make:factory added a bunch of our properties here by default, but it didn't add all of them. One that's missing is description. Add it: 'description' => self::faker()-> and then use paragraph(). Finally, for slug, we don't need that at all because it will be set automatically.

62 lines | src/Factory/VinylMixFactory.php
// ... lines 1 - 37
protected function getDefaults(): array
{
return [
// ... line 41
'description' => self::faker()->paragraph(),
// ... lines 43 - 45
];
}
// ... lines 48 - 62

Phew! Let's try this! Reload the fixtures:

symfony console doctrine:fixtures:load

Then head over and refresh. That looks so much better. We do have one broken image... but that's just because the API I'm using has some "gaps" in it... nothing to worry about.

Foundry can do a ton of other cool things, so definitely check out its docs. It's especially useful when writing tests, and it works great with database relations. So we'll see it again in a more complex way in the next tutorial.

Next, let's add pagination! Because eventually, we won't be able to list every mix in our database all at once.