Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Foundry: Always Pass a Factory Instance to a Relation

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.

I love Foundry. But using Foundry with Doctrine relationships is probably the hardest part of this library. So let's push a bit further. Pretend that, in this situation, we want to override the question value. Right now it grabs any random Question from the database. But I want to randomly grab only one of these 20 published questions.

Overriding the question Property

No problem! And this part is pretty manual. Put our callback... back... and return an array. There actually is a way in Foundry, to say:

please give me a random Question where some field matches some value.

But... in our case, we would need to say WHERE askedAt IS NOT NULL... which is too complex for that system to handle. But no worries! We'll just do this manually.

Above, on the createMany() call, add a $questions = before this. Back down here, add a use to the callback so that the $questions variable is accessible... then leverage array_rand() to grab a random item.

... lines 1 - 11
class AppFixtures extends Fixture
{
public function load(ObjectManager $manager)
{
$questions = QuestionFactory::createMany(20);
... lines 17 - 23
AnswerFactory::createMany(100, function() use ($questions) {
return [
'question' => $questions[array_rand($questions)]
];
});
... lines 29 - 30
}
}

Let's make sure this works! Reload the fixtures and...

symfony console doctrine:fixtures:load

No errors! We can use a special query to check this:

SELECT DISTINCT question_id FROM answer

symfony console doctrine:query:sql 'SELECT DISTINCT question_id FROM answer'

Yes! The answers are related to exactly 20 questions.

Accidentally Creating Extra Relation Objects

That was... manual but simple enough. And it was a great setup to show you a really common mistake when using Foundry with relationships.

In AnswerFactory, let's change the default question to create a new unpublished question. We can do this by saying QuestionFactory::new() - to create a QuestionFactory object - then ->unpublished().

There's no magic here: unpublished() is a method we created in the first tutorial: it changes the askedAt value to null. Then, to actually create the Question from the factory, add ->create().

... lines 1 - 28
final class AnswerFactory extends ModelFactory
{
... lines 31 - 37
protected function getDefaults(): array
{
return [
... lines 41 - 44
'question' => QuestionFactory::new()->unpublished()->create(),
];
}
... lines 48 - 60
}

This is totally legal: it will create a new unpublished Question, save it to the database and then that Question will be used as the question key when creating the Answer.

Well, that's what would normally happen. But since we are overriding the question key, this change should make absolutely no difference in our situation.

Famous last words. Reload the fixtures:

symfony console doctrine:fixtures:load

No errors... but check out how many questions there are in the database:

SELECT * from question

symfony console doctrine:query:sql 'SELECT * from question'

We should have 20+5: 25 questions. Instead... we have 125!

The problem is subtle... but maybe you spotted it! We're creating 100 answers... and the getDefaults() method is called for every one. That's.... good! But the moment that this question line is executed, it creates a new unpublished Question and saves it to the database. Then... a moment later, the question is overridden. This means that the 100 answers were all, in the end, correctly related to one of the 20 published questions. But it also means that, along the way, 100 extra questions were created, saved to the database... then never used.

What's the fix? Simple: remove ->create().

... lines 1 - 28
final class AnswerFactory extends ModelFactory
{
... lines 31 - 37
protected function getDefaults(): array
{
return [
... lines 41 - 44
'question' => QuestionFactory::new()->unpublished(),
];
}
... lines 48 - 60
}

This means that the question key is now set to a QuestionFactory object. The new() method returns a new QuestionFactory instance... and then the unpublished() method return self: so it returns that same QuestionFactory object.

Setting a relation property to a factory instance is totally allowed. In fact, you should always set a relation property to a factory instance if you can. Why?

Because this allows Foundry to delay creating the Question object until later. And in this case, it realizes that the question has been overridden, and so it avoids creating the extra object entirely... which is perfect.

Reload the fixtures one more time:

symfony console doctrine:fixtures:load

And check the question table:

symfony console doctrine:query:sql 'SELECT * from question'

We're back to 25 rows.

Next: let's use the new relationship to render answers on the frontend.

Leave a comment!

12
Login or Register to join the conversation
Tristan N. Avatar
Tristan N. Avatar Tristan N. | posted 1 year ago

Hello,

Since I went back to this course, I had a problem with the database and Docker.

After running: "docker-compose up -d" and running the first command line of the video "symfony console doctrine:fixtures:load",

I had this error:

An exception occurred in driver: SQLSTATE[HY000] [1049] Unknown database 'root'

Then I deleted the container and followed the procedure of the previous course to install a new container (...down -> ...up -d -> mysql -u root ...). But, in addition: I had something strange: when I asked: "docker-compose ps", I hadn't the same table!

When I do this course 2 weeks ago, and asked "docker-compose ps" I had the columns: Name | Command | State (set to "up") | Ports (set to "0.0.0.0:50153->3306/tcp, 33060/tcp")

Whereas now, I have : NAME | COMMAND | SERVICE (set to "database") | STATUS ( set to "running") | PORTS (set to "33060/tcp, 0.0.0.0:49363->3306/tcp)

Thus, I think the problem comes from Docker or a missing command for docker but I don't know what to do (I don't touch "env." or ".env.local" files & I already clear the cache)

Thanks

Reply

Hey Tristan N.!

Hmm. This might not be a big deal. With Mysql & Docker, iirc, I think Docker might not create the actual "database" for you. And so you can create it via symfony console doctrine:database:create. The fact that you see "Unknown database" means that it is successfully connected to MySQL.

But, in addition: I had something strange: when I asked: "docker-compose ps", I hadn't the same table!

Hmm. I think there are 2 things happening here:

1) The different "headers" on the table suggest that you may have just upgraded to a newer version of Doctrine with new headers. When I run docker-compose ps, I see the same new columns that you see (e.g. NAME | COMMAND | SERVICE, etc).

2) The exposed port was 50153 before and it's now 49363. That's expected: each time you restart Docker, it will bind port 3306 of the container to a different, random port on your host machine. But that's ok: the symfony binary automatically reads the random port and uses it when it adds the DATABASE_URL environment variable.

Let me know if this helps!

Cheers!

1 Reply
Tristan N. Avatar

Yes, it works! Thank you, Ryan ^^

Reply
Simon C. Avatar
Simon C. Avatar Simon C. | posted 1 year ago

How do I create a relation where its the same class? For example I have a parent/child object where if it has an ID in a parent_id field then it needs to fetch the parent object as well - I want to be able to create random parents but not for all

Reply
Ruslan Avatar

Hi. Tell me please, Ho to do if we need some kind of "fixed" dictionary/table like Country, City or Statuses - data which never changes?

Reply
Victor Avatar Victor | SFCASTS | Ruslan | posted 1 year ago | edited

Hey Ruslan,

It depends on, if you store those data in a special table or use scalar values for them. You could create a special table where you will store the statuses for example, and then you will do exactly what we do in this screencast, i.e. make a DB relation between a specific status and e.g. an Order entity. But usually such things as statuses we save as scalar values, i.e. you create a new "varchar" column for Order entity called "status" and set a scalar value to it, e.g. "paid". And all possible statuses we write as constants on an entity. Then, you will be able to create a method that will return all possible statuses, e.g. getSupportedStatuses():


Order
{
    public const STATUS_NEW = 'new';
    public const STATUS_PAID = 'paid';

    // ...

    public static function getSupportedStatuses(): array
    {
        return [
            self::STATUS_NEW,
            self::STATUS_PAID,
        ]
    }
}

Then, in the factory, you can leverage faker to get a random element of the array e.g. " $faker->randomElement(Order:: getSupportedStatuses());"

Or you can just hardcode the specific status by default. Or create a special method on the factory that will help you to set a specific status on the entity that you will pass as an argument.

I hope this helps!

Cheers!

Reply
Ruslan Avatar

Thank you for clarification. For "scalar" is clear.
But for still unclear , if I need Country (table of all countries) - it's fixed dictionary.
How will you do in that situation? Will you fill data in migration or write Fixture ?

Reply
Victor Avatar Victor | SFCASTS | Ruslan | posted 1 year ago | edited

Hey Ruslan,

Ah, ok! Well, migrations and fixtures are totally different things :) Migrations are basically used to "migrate" DB changes on production without losing real data. If you want to pre-fill your local database with some dummy data so it don't be just an empty DB - fixtures are exactly what you need.

But regarding your question, in the AppFixtures where you create all your fixtures fetch a real Country entity object/objects you need from the DB via entity manager and set them, something like we do in this course:


class AppFixtures extends Fixture
{
    public function load(ObjectManager $manager)
    {
        // Fetch the Country entity object here
        $country = $manager->getRepository(Country::class)->find(1);

        QuestionTagFactory::createMany(100, function() {
            return [
                'tag' => TagFactory::random(),
                'question' => QuestionFactory::random(),
                'country' => $country, // And set it here
            ];
        });

       // ...
    }
}

Something like this. I hope this helps!

Cheers!

Reply
Abdeljabar T. Avatar
Abdeljabar T. Avatar Abdeljabar T. | posted 1 year ago

Let's say I have an Image entity related to Post entity. Is there a way to fill the db table image with fake file paths and generate images on my public/uploads or public/images?

Reply

Hey Abdeljabar T.

What if you upload a set of images into your public directory and then, you just have a list of their paths in your fixtures that you will assign randomly to your Image objects?

Cheers!

Reply

Missed global goal of this lesson - Foundry just help to set database with fake data?

Reply

Hey Mepcuk!

Ha, fair enough :). The goal of the previous chapter is probably "Having fake database data is great, and Foundry make it easy, and here is how you use Foundry with Doctrine relations". This chapter really just focuses on covering one edge-case when people use Foundry with relations (that's this part here: https://symfonycasts.com/sc... ).

Cheers!

Reply
Cat in space

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

This tutorial also works great for Symfony 6!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.1",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "babdev/pagerfanta-bundle": "^3.3", // v3.3.0
        "composer/package-versions-deprecated": "^1.11", // 1.11.99.3
        "doctrine/doctrine-bundle": "^2.1", // 2.4.2
        "doctrine/doctrine-migrations-bundle": "^3.0", // 3.1.1
        "doctrine/orm": "^2.7", // 2.9.5
        "knplabs/knp-markdown-bundle": "^1.8", // 1.9.0
        "knplabs/knp-time-bundle": "^1.11", // v1.16.1
        "pagerfanta/doctrine-orm-adapter": "^3.3", // v3.3.0
        "pagerfanta/twig": "^3.3", // v3.3.0
        "sensio/framework-extra-bundle": "^6.0", // v6.2.1
        "stof/doctrine-extensions-bundle": "^1.4", // v1.6.0
        "symfony/asset": "5.3.*", // v5.3.4
        "symfony/console": "5.3.*", // v5.3.7
        "symfony/dotenv": "5.3.*", // v5.3.7
        "symfony/flex": "^1.3.1", // v1.17.5
        "symfony/framework-bundle": "5.3.*", // v5.3.7
        "symfony/monolog-bundle": "^3.0", // v3.7.0
        "symfony/runtime": "5.3.*", // v5.3.4
        "symfony/stopwatch": "5.3.*", // v5.3.4
        "symfony/twig-bundle": "5.3.*", // v5.3.4
        "symfony/validator": "5.3.*", // v5.3.14
        "symfony/webpack-encore-bundle": "^1.7", // v1.12.0
        "symfony/yaml": "5.3.*", // v5.3.6
        "twig/extra-bundle": "^2.12|^3.0", // v3.3.1
        "twig/string-extra": "^3.3", // v3.3.1
        "twig/twig": "^2.12|^3.0" // v3.3.2
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.3", // 3.4.0
        "symfony/debug-bundle": "5.3.*", // v5.3.4
        "symfony/maker-bundle": "^1.15", // v1.33.0
        "symfony/var-dumper": "5.3.*", // v5.3.7
        "symfony/web-profiler-bundle": "5.3.*", // v5.3.5
        "zenstruck/foundry": "^1.1" // v1.13.1
    }
}
userVoice