Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Foundry: Fixtures You'll Love

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

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.

... 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().

... 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).

... 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.

... 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.

... 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.

Leave a comment!

19
Login or Register to join the conversation
Georg-L Avatar

Hey team,

I just followed the tutorial and got an Error. To blame me: I am using symfony 6.2.8.
But did I miss something? The new Method from MixController works fine and slug is created automatically, but deleting 'slug' => self::faker()->text() and executing "symfony console doctrine:fixtures:load" leads to

In ExceptionConverter.php line 47:

An exception occurred while executing a query: SQLSTATE[23502]: Not null violation: 7 ERROR: null value in column "slug" of relation "vinyl_mix" violates not-null constraint
DETAIL: Failing row contains (9, id veniam ducimus exercitationem est, Totam dolor et tenetur fuga voluptas nisi rerum. Est animi corpo..., 6, rock, 2023-04-17 17:21:34, 20, 2023-04-17 17:21:34, null).
Any comment is appreciated.
Thank you.

Reply

Hey @Georg-L

That's unexpected. Did you add the Slug attribute to your property? #[Slug(fields: ['title'])]
like shown in this code block https://symfonycasts.com/screencast/symfony-doctrine/sluggable#codeblock-1fd8df0329

Cheers!

Reply
Georg-L Avatar

Hey @MolloKhan,

thank you for your message. Me stupid! I managed to somehow delete this line and didn't notice.
sorry for bothering you.

Have a nice day

Reply

No prob! it happens :)

Reply
Rufnex Avatar
Rufnex Avatar Rufnex | posted 10 months ago | edited

Hey, great library. I need a bit of help with localization. If I want another language, how to do this? In the documentation I see

Faker\Factory::create('fr_FR');

But how to set this in Symfony? Thank you!

Reply

Hey Rufnex,

Good question! If you're talking about how to localize Faker with the Foundry bundle - here's an example: https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#faker . As you can see from the docs, you can do it via Symfony configuration:

# config/packages/zenstruck_foundry.yaml
when@dev: # see Bundle Configuration section about sharing this in the test environment
    zenstruck_foundry:
        faker:
            locale: fr_FR # set the locale

I hope this helps!

Cheers!

Reply
Rufnex Avatar

Hi Victor,

i had already tried this. unfortunately, only the standard language is used. Any further ideas?

Greetings
Rufnex

Reply

Hey Rufnex,

Hm, could you clear the cache and try again? It might be a cache issue. If not, please, make sure your current config really has that locally, you can debug it with:

bin/console debug:config zenstruck_foundry

Is the locale set there? Also, make sure you're debugging this config for the environment that your Symfony application is currently using. It might be so that you set the locale for dev but load your app in prod.

Cheers!

Reply
Rufnex Avatar

HeyHo Victor,

the configuration looks correct, but still only the default language like "et accusamus sunt ipsum non". strange behavioer .. or is it required to download somewhere the language packs?

Current configuration for extension with alias "zenstruck_foundry"
==================================================================

zenstruck_foundry:
    auto_refresh_proxies: true
    faker:
        locale: fr_FR
        seed: null
        service: null
    instantiator:
        without_constructor: false
        allow_extra_attributes: false
        always_force_properties: false
        service: null
        ```
Reply

Hey Rufnex,

Ah, ok, it seems correct to me. Well, you don't need to install any extra localization pack, it's implemented via so called Provider in the faker bundle: https://github.com/FakerPHP/Faker/tree/main/src/Faker/Provider - you just need to specify a correct provider for the "locale" option I suppose. OK, could you try that Faker\Factory::create('fr_FR') instead as described in the docs and try to generate data with it? Are those data really localized? If so, it sounds like a misconfiguration or a bug in the Foundry bundle, I'd recommend you to open an issue there.

Btw, keep in mind that not all the data might be translated. Please, try on the specific methods like name(), region(), vat(), or phoneNumber() as it's listed as translated in the docs: https://fakerphp.github.io/locales/fr_FR/ . It might be so that the method you're calling always returns Latin text.

I hope this helps!

Cheers!

Reply
Rufnex Avatar

Sorry, can you explain me, where to add Faker\Factory::create('fr_FR') ? i'm a bit confused ;)

Reply
Ruslan Avatar
Ruslan Avatar Ruslan | Rufnex | posted 10 months ago | edited

I think Victor talk about something like this:

    protected function getDefaults(): array
    {
        $faker = Factory::create('fr_FR');
        return [
            'title' => $faker->lastName(),
//            'title' => self::faker()->words(5, true),
            'description' => self::faker()->paragraph(),
            'trackCount' => self::faker()->numberBetween(5, 20),
            'genre' => self::faker()->randomElement(['pop', 'rock']),
            'votes' => self::faker()->numberBetween(-50, 50),
        ];
    }
1 Reply
Rufnex Avatar

Thanks you! Unfortunately, only latain is produced here as well.

Reply

Hey Rufnex,

Hm, that's weird if even with the Faker\Factory::create('fr_FR') you still get latin data. What method are you. calling on that custom localized faker? Could you show a part of your code where you're using it? Because as far as I understand your code it should just work this way, probably you're missing something obvious.

Cheers!

Reply

Hey Rufnex,

Yep, exactly like Ruslan mentioned below in https://symfonycasts.com/screencast/symfony-doctrine/foundry#comment-27727

I.e. you create a faker instance manually via $faker = Factory::create('fr_FR'); passing the desired locale and then use it below.

Cheers!

Reply
Rufnex Avatar

I followed the example .. so in my factory i have this code:

    protected function getDefaults(): array
    {
        $faker = Factory::create('fr_FR');
        return [
            // TODO add your default values here (https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#model-factories)
            'title' => $faker->words(5, true),
            'description' => $faker->text(),
            'hits' => self::faker()->randomNumber(),
        ];
    }
Reply

Hey Rufnex,

Oh, that's exactly what is in the docs actually, I'm not sure why it does not work. Well, the only explanation might be is that those methods you're using like words()/text() are not localized to French. I'd recommend you to change that code to something like this:

    protected function getDefaults(): array
    {
        $faker = Factory::create('fr_FR');
        // This should dump a French region and a French phone number that is started with +33
        dd($faker->region(), $faker->phoneNumber());

        return [
            // TODO add your default values here (https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#model-factories)
            'title' => $faker->words(5, true),
            'description' => $faker->text(),
            'hits' => self::faker()->randomNumber(),
        ];
    }

And re-load your fixtures. In the console, you should get a dumped French region and a French number that starts with +33 - it you see it - then localization actually work, but it would mean as I said before that those method you're using like text() are not localized to French.

But if you don't see a french region and french phone number dumped - than it might be a bug in the Faker library you have installed. I'd recommend you to upgrade to the latest version and double-check if it works there. if not - probably report a bug in the Faker repository. Though, it would be a very obvious bug I suppose so other devs had to notice it already. I'm leaning towards the fact that the methods you're using are just not localizaed.

Cheers!

Reply
Rufnex Avatar
Rufnex Avatar Rufnex | Victor | posted 10 months ago | edited | HIGHLIGHTED

Hi Victor,

I think I have found the problem. The formatters text() and word() can't seem to be localized. To create a local text you can use realText()

$faker->realText()
$faker->realText('30')

It seems that the translation only works for the following: https://fakerphp.github.io/formatters/

If someone has the same problem, the insight might be helpful. And yes RTFM ;o)

Anyway, with the translated results, I think I'd rather stay with Latin LOL.

Thanks for your help, your service is great!

1 Reply

Hey Rufnex,

That's exactly what I was trying to explain you a few times :) Yeah, not everything is localized, unfortunately... or fortunately, because it might be done on purpose, that's just a strategy of that lib.

Good catch on that realText() method, and thanks for sharing it with others!

Anyway, with the translated results, I think I'd rather stay with Latin LOL

Lol! But yeah, that's the reason why the "lorem ipsum" is so popular, it has no "semantic load", just text :)

Anyway, I'm really happy we figured this mystery out! ;)

Cheers!

1 Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.1",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "babdev/pagerfanta-bundle": "^3.7", // v3.7.0
        "doctrine/doctrine-bundle": "^2.7", // 2.7.0
        "doctrine/doctrine-migrations-bundle": "^3.2", // 3.2.2
        "doctrine/orm": "^2.12", // 2.12.3
        "knplabs/knp-time-bundle": "^1.18", // v1.19.0
        "pagerfanta/doctrine-orm-adapter": "^3.6", // v3.6.1
        "pagerfanta/twig": "^3.6", // v3.6.1
        "sensio/framework-extra-bundle": "^6.2", // v6.2.6
        "stof/doctrine-extensions-bundle": "^1.7", // v1.7.0
        "symfony/asset": "6.1.*", // v6.1.0
        "symfony/console": "6.1.*", // v6.1.2
        "symfony/dotenv": "6.1.*", // v6.1.0
        "symfony/flex": "^2", // v2.2.2
        "symfony/framework-bundle": "6.1.*", // v6.1.2
        "symfony/http-client": "6.1.*", // v6.1.2
        "symfony/monolog-bundle": "^3.0", // v3.8.0
        "symfony/proxy-manager-bridge": "6.1.*", // v6.1.0
        "symfony/runtime": "6.1.*", // v6.1.1
        "symfony/twig-bundle": "6.1.*", // v6.1.1
        "symfony/ux-turbo": "^2.0", // v2.3.0
        "symfony/webpack-encore-bundle": "^1.13", // v1.15.1
        "symfony/yaml": "6.1.*", // v6.1.2
        "twig/extra-bundle": "^2.12|^3.0", // v3.4.0
        "twig/twig": "^2.12|^3.0" // v3.4.1
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.2
        "symfony/debug-bundle": "6.1.*", // v6.1.0
        "symfony/maker-bundle": "^1.41", // v1.44.0
        "symfony/stopwatch": "6.1.*", // v6.1.0
        "symfony/web-profiler-bundle": "6.1.*", // v6.1.2
        "zenstruck/foundry": "^1.21" // v1.21.0
    }
}
userVoice