Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Foundry Tricks

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.

In QuestionFactory, we're already doing a pretty good job of making some of this data random so that all of the questions aren't identical. To help with this, Foundry comes with built-in support for Faker: a library that's great at creating all kinds of interesting, fake data.

Using Faker

If you look at the top of the Foundry docs, you'll see a section called Faker and a link to the Faker documentation. This tells you everything that Faker can do... which is... a lot. Let's use it to make our fixtures even better.

Tip

The Faker library now has a new home! At https://github.com/FakerPHP/Faker. Same great library, shiny new home.

For example, for the random -1 to -100 days, we can make it more readable by replacing the new \DateTime() with self::faker() - that's how you can get an instance of the Faker object - then ->dateTimeBetween() to go from -100 days to -1 day.

... lines 1 - 19
final class QuestionFactory extends ModelFactory
{
protected function getDefaults(): array
{
return [
... lines 25 - 38
'askedAt' => rand(1, 10) > 2 ? self::faker()->dateTimeBetween('-100 days', '-1 days') : null,
... line 40
];
}
... lines 43 - 55
}

And because this is more flexible, we can even change it from -100 days to -1 minute!

... lines 1 - 19
final class QuestionFactory extends ModelFactory
{
protected function getDefaults(): array
{
return [
... lines 25 - 38
'askedAt' => rand(1, 10) > 2 ? self::faker()->dateTimeBetween('-100 days', '-1 minute') : null,
... line 40
];
}
... lines 43 - 55
}

Even the random true/false condition at the beginning can be generated by Faker. What we really want is to create published questions about 70% of the time. We can do that with self::faker()->boolean(70):

... lines 1 - 19
final class QuestionFactory extends ModelFactory
{
protected function getDefaults(): array
{
return [
... lines 25 - 38
'askedAt' => self::faker()->boolean(70) ? self::faker()->dateTimeBetween('-100 days', '-1 minute') : null,
... line 40
];
}
... lines 43 - 55
}

This is cool, but the real problem is that the name and question are always the same. That is definitely not realistic. Let's fix that: set name to self::faker()->realText() to get several words of "real looking" text:

... lines 1 - 19
final class QuestionFactory extends ModelFactory
{
protected function getDefaults(): array
{
return [
'name' => self::faker()->realText(50),
... lines 26 - 40
];
}
... lines 43 - 55
}

For slug, there's a feature for that! self::faker()->slug:

... lines 1 - 19
final class QuestionFactory extends ModelFactory
{
protected function getDefaults(): array
{
return [
... line 25
'slug' => self::faker()->slug,
... lines 27 - 40
];
}
... lines 43 - 55
}

Tip

Direct property access is deprecated since v1.14 of fakerphp/faker - use self::faker()->slug() instead of self::faker()->slug

Finally, for the question text, it can be made much more interesting by using self::faker->paragraphs().

... lines 1 - 19
final class QuestionFactory extends ModelFactory
{
protected function getDefaults(): array
{
return [
... lines 25 - 26
'question' => self::faker()->paragraphs(
... lines 28 - 29
),
... lines 31 - 32
];
}
... lines 35 - 47
}

Faker lets you use paragraphs like a property or you can call a function and pass arguments, which are the number of paragraphs and whether you want them returned as text - which we do - or as an array. For the number of paragraphs, we can use Faker again! self::faker()->numberBetween(1, 4) and then true to return this as a string.

... lines 1 - 19
final class QuestionFactory extends ModelFactory
{
protected function getDefaults(): array
{
return [
... lines 25 - 26
'question' => self::faker()->paragraphs(
self::faker()->numberBetween(1, 4),
true
),
... lines 31 - 32
];
}
... lines 35 - 47
}

Let's take this for a test drive! Find your terminal and reload the fixtures with:

symfony console doctrine:fixtures:load

Go check the homepage and... yea!

Oh, but the "real text" for the name is way too long. What I meant to do is pass ->realText(50). Let's reload the fixtures again:

symfony console doctrine:fixtures:load

And... there we go! We now have many Question objects and they represent a rich set of unique data. This is why I love Foundry.

Doing Things Before Saving

If you click into one of the questions, you can see that the slug is unique... but was generated in a way that is completely unrelated to the question's name. That's "maybe" ok... but it's not realistic. Could we fix that?

Of course! Foundry comes with a nice "hook" system where we can do actions before or after each item is saved. Inside QuestionFactory, the initialize() method is where you can add these hooks.

Remove the slug key from getDefaults() and, instead, down here, uncomment this beforeInstantiate() and change it to afterInstantiate().

... lines 1 - 20
final class QuestionFactory extends ModelFactory
{
protected function getDefaults(): array
{
return [
'name' => self::faker()->realText(50),
'question' => self::faker()->paragraphs(
self::faker()->numberBetween(1, 4),
true
),
'askedAt' => self::faker()->boolean(70) ? self::faker()->dateTimeBetween('-100 days', '-1 minute') : null,
'votes' => rand(-20, 50),
];
}
... lines 35 - 52
}

So afterInstantiate(), we want to run this function. Inside, to generate a random slug based off of the name, we can say: if not $question->getSlug() - in case we set it manually for some reason:

... lines 1 - 20
final class QuestionFactory extends ModelFactory
{
... lines 23 - 35
protected function initialize(): self
{
// see https://github.com/zenstruck/foundry#initialization
return $this
->afterInstantiate(function(Question $question) {
if (!$question->getSlug()) {
... lines 42 - 43
}
})
;
}
... lines 48 - 52
}

then use Symfony's Slugger - $slugger = new AsciiSlugger():

... lines 1 - 6
use Symfony\Component\String\Slugger\AsciiSlugger;
... lines 8 - 20
final class QuestionFactory extends ModelFactory
{
... lines 23 - 35
protected function initialize(): self
{
// see https://github.com/zenstruck/foundry#initialization
return $this
->afterInstantiate(function(Question $question) {
if (!$question->getSlug()) {
$slugger = new AsciiSlugger();
... line 43
}
})
;
}
... lines 48 - 52
}

and set it with $question->setSlug($slugger->slug($question->getName())).

... lines 1 - 6
use Symfony\Component\String\Slugger\AsciiSlugger;
... lines 8 - 20
final class QuestionFactory extends ModelFactory
{
... lines 23 - 35
protected function initialize(): self
{
// see https://github.com/zenstruck/foundry#initialization
return $this
->afterInstantiate(function(Question $question) {
if (!$question->getSlug()) {
$slugger = new AsciiSlugger();
$question->setSlug($slugger->slug($question->getName()));
}
})
;
}
... lines 48 - 52
}

Nice! Let's try it. Move over, reload the fixtures again:

symfony console doctrine:fixtures:load

And... go back to the homepage. Let's see: if I click the first one... yes! It works. It has some uppercase letters... which we could normalize to lowercase. But I'm not going to worry about that because, in a few minutes, we'll add an even better way of generating slugs across our entire system.

Foundry "State"

Let's try one last thing with Foundry. To have nice testing data, we need a mixture of published and unpublished questions. We're currently accomplishing that by randomly setting some askedAt properties to null. Instead let's create two different sets of fixtures: exactly 20 that are published and exactly 5 that are unpublished.

To do this, first remove the randomness from askedAt in getDefaults(): let's always set this.

... lines 1 - 20
final class QuestionFactory extends ModelFactory
{
... lines 23 - 27
protected function getDefaults(): array
{
return [
... lines 31 - 35
'askedAt' => self::faker()->dateTimeBetween('-100 days', '-1 minute'),
... line 37
];
}
... lines 40 - 57
}

If we stopped here, we would, of course, have 20 questions that are all published. But now, add a new public function to the factory: public function unpublished() that returns self.

... lines 1 - 20
final class QuestionFactory extends ModelFactory
{
public function unpublished(): self
{
... line 25
}
... lines 27 - 57
}

I totally just made up that name. Inside, return $this->addState() and pass it an array with askedAt set to null.

... lines 1 - 20
final class QuestionFactory extends ModelFactory
{
public function unpublished(): self
{
return $this->addState(['askedAt' => null]);
}
... lines 27 - 57
}

Here's the deal: when you call addState(), it changes the default data inside this instance of the factory. Oh, and the return statement here just helps to return self... which allows method chaining.

To use this, go back to AppFixtures. Start with QuestionFactory::new() - to get a second instance of QuestionFactory:

... lines 1 - 9
class AppFixtures extends Fixture
{
public function load(ObjectManager $manager)
{
... lines 14 - 15
QuestionFactory::new()
... lines 17 - 18
;
... lines 20 - 21
}
}

then ->unpublished() to change the default askedAt data. You can see why I called the method unpublished(): it makes this super clear.

... lines 1 - 9
class AppFixtures extends Fixture
{
public function load(ObjectManager $manager)
{
... lines 14 - 15
QuestionFactory::new()
->unpublished()
... line 18
;
... lines 20 - 21
}
}

Finish with ->createMany(5).

... lines 1 - 9
class AppFixtures extends Fixture
{
public function load(ObjectManager $manager)
{
... lines 14 - 15
QuestionFactory::new()
->unpublished()
->createMany(5)
;
... lines 20 - 21
}
}

I love this! It reads like a story: create a new factory, make everything unpublished and create 5.

Let's... even make sure it works! At the terminal, reload the fixtures:

symfony console doctrine:fixtures:load

Then... refresh the homepage.

All good! If we dug into the database, we'd find 20 published questions and five unpublished. Foundry can do more - especially with Doctrine relations and testing - and we'll talk about Doctrine relations in the next tutorial.

But first, the slug property is being set automatically in our fixtures. That's cool... but I'd really love for the slug to automatically be set to a URL-safe version of the name no matter where we create a Question object. Basically, we shouldn't never need to worry about setting the slug manually.

So next let's install a bundle that will give our entity Sluggable and Timestampable superpowers.

Leave a comment!

59
Login or Register to join the conversation
Sherri Avatar

The createMany() instance method is deprecated. Use this instead:

QuestionFactory::new()
->unpublished()
->many(5)
->create()
;

2 Reply
Gizmola Avatar

It's a static method now: QuestionFactory::createMany(30); will work.

1 Reply
Default user avatar
Default user avatar Adrien Benoit | posted 5 months ago

Hey ! Is it possible to set multiple states ?
Just like :

QuestionFactory::new()
->unpublished()
->unready()


->create();

I tried but i can't do it. Or maybe there is an other way to do that ?
Thanks for your answer.

1 Reply

Hey Adrian,

Yes, it should be possible! Hm, probably you forgot a return statement in one of your methods, e.g. in unpublished() or unready() - it's the most comment error I think. Otherwise, it would be good to see an error if you have any.

Cheers!

Reply
Default user avatar
Default user avatar Adrien Benoit | victor | posted 5 months ago

That's was that ! Sorry for useless question ! Thanks you for your answer

Reply

Hey Adrien,

No problem, that's easy to miss ;) I'm happy to hear it works for you know!

Cheers!

Reply
Pieter-jan C. Avatar
Pieter-jan C. Avatar Pieter-jan C. | posted 1 year ago

the use of self::faker()->slug is deprecated and should be slug()

1 Reply

Hey Pieter-jan C.

Good catch. Thanks for your report we will add a note about it.

Cheers!

Reply
Walid-M Avatar
Walid-M Avatar Walid-M | posted 19 days ago

Hello,

Can we make fixtures for [Embeddables] class ?

For example "User Entity" use "Adress embeddable class" who store the field City, Town, Street

How we can use Foundry to generate many User ?

Thank you.

Reply
Jesse-Rushlow Avatar
Jesse-Rushlow Avatar Jesse-Rushlow | SFCASTS | Walid-M | posted 10 days ago | edited

Howdy!

Yes, but not directly. If we have:

#[ORM\Entity(repositoryClass: UserRepository::class)]
#[ORM\Table(name: '`user`')]
class User
{
    #[ORM\Column(length: 255)]
    private ?string $name = null;

    #[ORM\Embedded(class: Address::class)]
    private Address $address;
    
    public function __construct()
    {
        $this->address = new Address();
    }
    
...
}

And

#[ORM\Embeddable]
class Address
{
    #[ORM\Column]
    private string $city;

    #[ORM\Column]
    private string $state;
...
}

We can run bin/console make:factory selecting User and MakerBundle will generate a factory that looks like:

final class UserFactory extends ModelFactory
{
    protected function getDefaults(): array
    {
        return [
            'name' => self::faker()->text(),
            'address.city' => self::faker()->text(),
            'address.state' => self::faker()->text(),
        ];
    }

    protected static function getClass(): string
    {
        return User::class;
    }
}

They key thing is your User class needs to have a getter and setter for User::address and Address of course needs getters/setters for it's respective properties.

Then you just use foundry as you would with any other entity. E.g. $users = UserFactory::createMany(5);

1 Reply
Pawel Avatar

Hi, I just try to generate some text but text are in Latin

I added configuration in file config/package/zenstruck_foundry.yaml

when@dev: &dev
    # See full configuration: https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#full-default-bundle-configuration
    zenstruck_foundry:
        # Whether to auto-refresh proxies by default (https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#auto-refresh)
        auto_refresh_proxies: true
        faker:
            # Change the default faker locale.
            locale: en_US # Example: fr_FR

when@test: *dev

but it doesn't work, what I missed?

Reply

Hey Pawel,

Do you get an error, or just the locale is not changing? If it's the latter, try clearing the cache manually rm -rf var/cache

Cheers!

Reply
Pawel Avatar
Pawel Avatar Pawel | MolloKhan | posted 1 month ago | edited

Hey MolloKhan,
Thank for your reply.
I cleared cache, resutl is better, two or 3 my entieties has now data in en_US, but one has still Latin names.

    protected function getDefaults(): array
    {
        return [
            // TODO add your default values here (https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#model-factories)
            'Name' => self::faker()->text(20),
        ];
    }

My method getDefaults looks for Category and GoalTypes looks like above, but no idea, why they give different results.
I just try to create new project, but after
download https://bitbucket.org/pawelwaw/test/downloads/pawelwaw-seventagen-d894536c507f.zip
`composer install `

and

doctrine:migrations:migrate

command
doctrine:fixutures:load

still give me latin texts. Maybe you can try on your machine.

Reply
Pawel Avatar
Pawel Avatar Pawel | Pawel | posted 1 month ago | edited

I finally found out why this happen.

'Name' => self::faker()->text(20),

method text use finally method word this is not supported internationaliration and call Lorem.php class insted of Text Class from configured Provider Class, So I suppose I should add myself class Lorem which will extends Base Lorem class.
Or as I decided get word form realText function

    protected function getDefaults(): array
    {
        preg_match('/(?P<name>(\w+){8,10})/', self::faker()->realText(100), $matches);
        return [
            'Name' => $matches[0],
        ];
    }
Reply

Ohh, that's interesting and makes sense, the text generator was not going through the translation process. I'm not sure if that's on purpose or just a bug on the bundle

Reply
gazzatav Avatar
gazzatav Avatar gazzatav | posted 4 months ago

It occurred to me that the whole get-vote set-vote thing will not work with more than one user voting at the same time as it's not atomic - by the time we read the vote someone else has already changed it and we set it to the same value as they did, thereby losing our vote. My solution would be to create functions in the database to up-vote and down-vote in a transaction. I've worked through all the doctrine related tutorials and not seen any evidence of it creating functions. Am I right in thinking that this is something that doctrine could not figure out or is there a mechanism to help it do this?

(I asked in this chapter because it's the first place we get serious about the voting.)

Reply
Gizmola Avatar

I started this series with symfony 5.0 and php 7.3. I've worked through some issues with Foundry (had to update to php74 to get a version that actually has the make:factory command) and at the point where AsciiSlugger() was used, I had to composer require symfony/string. I don't know if symfony/string is now included by default, but I thought this might bite someone else.

One other issues:
If you end up in a state where you are getting these types of errors:

Fatal error: Could not check compatibility between Symfony\Bridge\ProxyManager\LazyProxy\PhpDumper\LazyLoadingValueHolderGenerator::generate(ReflectionClass $originalClass, Zend\Code\Generator\ClassGenerator $classGenerator): void and ProxyManager\ProxyGenerator\LazyLoadingValueHolderGenerator::generate(ReflectionClass $originalClass, Laminas\Code\Generator\ClassGenerator $classGenerator), because class Zend\Code\Generator\ClassGenerator is not available in

This can be remedied by requiring composer require laminas/laminas-zendframework-bridge

Reply

Hey Gizmola!

Thanks for posting the solution to the issues you got - and I'm sorry you hit them!

> I started this series with symfony 5.0 and php 7.3

yea, I can't remember exactly why, but PHP 7.4 is the minimum version we have marked for this tutorial. Some things may work on php 7.3 (as you saw), but it gets "iffy" since we haven't tested with it.

> I don't know if symfony/string is now included by default, but I thought this might bite someone else.

That's actually a good point. In the tutorial code, it IS installed because both symfony/console 5.2 and symfony/property info 5.2 (both of which we have installed) requires it. But with a version of Symfony 5.1 or before, you wouldn't have it. That would have been a good thing for me to mention :).

Cheers!

Reply
davidmintz Avatar
davidmintz Avatar davidmintz | posted 10 months ago

Hey, this might be a little OT but I am applying these skills to a project with great results, mostly, but after changing some things around with my entities and underlying database, when I run doctrine:fixtures:load the Exception I get is a foreign key constraint violation because I have a table with a self-referencing foreign key. I understand the problem: before purging and loading I need to run UPDATE people SET payer_id = NULL. Or perhaps SET FOREIGN_KEY_CHECKS=0.Is there a way to wire this up to happen automatically? Event hook or something?

I have looked at https://symfony.com/bundles.... Would that be the approach, more or less?

The workaround I am using for the time being is just before doctrine:fixtures:load I run a little shell script that runs a SQL snippet against the database inside the container.

Reply

Hey davidmintz

I think you can tweak your relation to CASCADE remove objects, that will drop all related objects with parent one, and purging will work from the box.

Ping me back if it works ;)

Cheers!

Reply
davidmintz Avatar

Yay! Thank you!

We have a Person entity and its subclass Patient, organized by way of Single Table Inheritance (seems like a reasonable choice for my case). A Patient sometimes has a Payer, but usually not (we're gonna bill the Patient, not someone else, and the rest is their problem. Yeah this is the USA where the healthcare system itself is terminally ill). So the Patient's "payer" property is in a unidirectional OneToMany relation with Person. I had an issue Integrity constraint violation: 1451 Cannot delete or update a parent row... with purging as described above. Adding the following annotation solved it:



/**
* @ORM\ManyToOne(targetEntity=Person::class)
* @ORM\JoinColumn(name="payer_id",onDelete="SET NULL")
*/
private $payer;
Reply

Yo! Thanks for the reply, and I'm happy that my answer get you closer to solving issue =)

Cheers and stay safe!

1 Reply
MattHB Avatar

using:


->afterInstantiate(function(Question $question) {
if (!$question->getSlug()) { // <---------------------------------

causes this to throw

Typed property App\Entity\Question::$slug must not be accessed before initialization

presumably it doesnt like the getter being called.

removing the if works fine.

Reply

Hey MattHB!

Yep, that's totally true if you add types to your properties and don't initialize them. So, your fix is definitely 👍. Alternatively, you can default $slug to null:


private ?string $slug = null;

Cheers!

1 Reply
robspijkerman Avatar
robspijkerman Avatar robspijkerman | posted 1 year ago

I have run into an issue with dateTimeBetween. The current version of Symfony is requiring DatetimeImmutable and there is no comparative in dateTimeBetween that I can see.

This is the line in question in QuestionFactory.php

'askedAt' => self::faker()->boolean(70) ? self::faker()->dateTimeBetween('-100 days', '-1 minute') : null,

In PropertyAccessor.php line 262:

Expected argument of type "DateTimeImmutable or null", "instance of DateTime" given at property path "askedAt".

In Question.php line 91:

Argument 1 passed to App\Entity\Question::setAskedAt() must be an instance of DateTimeImmutable or null, instance of DateTime given, called in C:\dev\cauldron_overflow\vendor\symfony\property-access\PropertyAccessor.php on line 578

What would you suggest?

Cheers

Rob

Reply

Hey @Rob!

Hmm, I see what you're saying! Ok, let's look at the two sides of this:

1) When you use dateTimeBetween from Faker - https://fakerphp.github.io/... - that *does* give you a DateTime object. As far as I can see, there is no way currently to opt into a DateTimeImmutable. But, that's ok! (see my next point)

2) The error is being caused because the setAskedAt() method in your entity has a DateTimeImmutable type-hint on it. If you check, the method probably looks like this:


public function setAskedAt(?\DateTimeImmutable $askedAt)
{
// ....
}

Fortunately, this is YOUR code to change. I think changing it to a \DateTimeInterface would work fine, as DateTime objects implement this :). I just checked the "finished" code from our tutorial, and it *does* use DateTimeInterface - here is exactly how the method looks:


public function setAskedAt(?\DateTimeInterface $askedAt): self
{
$this->askedAt = $askedAt;

return $this;
}

My guess is that you selected the datetime_immutable option when using the make:entity command, which uses this type-hint. If you did this on purpose... that's valid but... well, unfortunately, it doesn't look like the dateTimeBetween() plays nicely with this.

Cheers!

Reply

When is the next episode coming out? There's some really important stuff in that next one that would be amazing to have. ie Authentication and generating fixtures with relationships are things I would really love to have tutorials on. The documentation on zenstruck foundry is really confusing and I'm having a hell of a time getting it to work. :-/

Reply

Hey somecallmetim!

Yea, sorry about the big delay - we've been busy with a bunch of frontend stuff (Stimulus, Turbo), which I know a lot of people LOVE... but also a lot of people want this stuff too :).

> Authentication and generating fixtures with relationships are things I would really love to have tutorials on

These are the next two that will happen in this series (probably relations first... so we have more to play with for Authentication). I was waiting for Symfony 5.3 for authentication, as it has some nice changes - and Symfony 5.3 will be released in about 2 weeks. Overall, my guess is that the next parts of this series will be at least 1 month away (the Turbo tutorial is in front of it).

However, if you have any specific Foundry questions or problems, feel free to ask them here! That might help me know what things we should talk about with it... or even ways to improve the Foundry docs (I talk frequently with its author).

Cheers!

1 Reply
sadikoff Avatar sadikoff | SFCASTS | posted 1 year ago | edited

Hey there

I have, I have =) I guess somewhere probably in getDefaults() method you returned an array with "0" key but it should be an associative array with keys synced with your entity properties

But it is quite difficult to say why you have this error without any trace information, which fixture throws it in what place and what is the code in that place.

Cheers!

Reply
m3tal Avatar

I have 3 entities :

Product - > ProductsOptions -> ProductExtra
if I am using like this


ProductFactory::new()->createMany(50,[
'productOptions'=>ProductOptionsFactory::new()->many(5)
]);


all work perfectly.

if I want to insert here the 3rd like this


ProductFactory::new()->createMany(50,[
'productOptions'=>ProductOptionsFactory::new()->createMany(5, [
'productExtra'=>ProductExtrasFactory::new()->many(10)
])
]);


I receive a big error

  An exception occurred while executing 'INSERT INTO product_options (name, price, type, required, fk_product_id_id) VALUES (?, ?, ?, ?, ?)' with params ["aut", 1,  
"1", 1, null]:

SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'fk_product_id_id' cannot be null

it looks like the product_id is not passed to the next productOptions
Do you know the problem?

thanks.

Reply

Hey m3tal!

Hmm. Do you happen to have any cascade={"persist"} on any of your Doctrine annotations for the relationships? Also, could you pass the stack trace of the error (if you don't see it in the terminal, you can add -vvv when you run the command to get it). Finally, if you could post the relevant entity code (the related properties, ManyToOne/OneToMany annotations, and the getDefaults() of your factory classes), that would help :).

Overall, I don't see any problem from the code you've posted - so we need to dig deeper :).

Cheers!

Reply
m3tal Avatar

Hi.
Kevin, your friend, give me the workaround.
Here's


ProductFactory::new()->createMany(50,[
'productOptions'=>ProductOptionsFactory::new([
'productExtra'=>ProductExtrasFactory::new()->many(10)
])->many(5)
]);

thanks Kevin Bond.

Reply

Woo! He's the best! Thanks for sharing the solution here! I should have seen that my self to be honest - I'm glad Kevin did!

Reply
Peter K. Avatar
Peter K. Avatar Peter K. | posted 1 year ago

What if I have a class like this:


class Car {
public function __construct(User $user){
$this->user = $user;
// bla bla bla
}
}

How can I actually pass $user via factory?
What I am doing is looping over each user and then


CarFactory::new()->create([
'user' => $user
])

but this is leaving my user blank...

Reply
Peter K. Avatar

seems like it is overwriting it, once I have changed __construct(User $user123) it seems to be working

Reply

Hey Peter,

Hm, fairly speaking I don't know, I would suppose it should work with "User $user" argument name as well, or probably it's a bug that might be already be fixed in the latest version of that bundle, or probably it's a known limitation of this bundle. Anyway, thank you for sharing your solution with others!

Cheers!

Reply
Peter K. Avatar
Peter K. Avatar Peter K. | posted 1 year ago

Hi Ryan,

in s4 I believe we were using AppFixtures to generate fixtures.

I had it working kind of ok but what I really hated was I wanted to insert into "car" table exactly these names ["Volvo","BMW","Tesla"] I had to loop over this array and thats how I inserted exact data into tables.

So now I have this:

$client = ClientFactory::new()->create();
$carArray = ['Alfa' => 'AL', 'Beta' => 'BE', 'Gamma' => 'GA', 'Delta' => 'DE'];
foreach($carArray as $key => $value){
CarFactory::new()->create(['name' => $key, 'code' => $value, 'client' => $client]);
}

I have like 30 tables (lookups) like above that I have industry fixed values that I want to use as fixtures. So I had to create that array with values loop over it and insert over and over again. This was working but is there any way to define those arrays in CarFactory class so I wont be messy if I will keep it in AppFixtures?

I would like to define those arrays somewhere in factory and then just call CarFactory::new()->createFromArray(["Volvo","BMW","Tesla"]) and this time it would loop 3 times and create 3 objects.

Last question. Imagine you have very old database schema and you want to migrate your data to new db structure.

I would want to somehow define a connection to DB loop over data and insert them to proper new tables so each Factory class would make a connection to old database structure, get data and insert them appropriately.

Also I am trying to figure it out how to pass random object per each iteration according their documentation:

PostFactory::new()->many(5)->create(); // create 5 Posts

CommentFactory::new()->create(['post' => PostFactory::random()]);

This will select random object out of 5 but what If I want to create 5 posts and 10 comments and each comment will have a random post.

PostFactory::new()->many(5)->create(); // create 5 Posts

CommentFactory::new()->createMany(10, ['post' => PostFactory::random()]);

This will always select same "random" post so I will end up with 10 comments with random post but thats not what I want I want to have 10 comments and each comment will have random post

Reply

Hey

Peter K.!

Excellent question!

> This was working but is there any way to define those arrays in CarFactory class so I wont be messy if I will keep it in AppFixtures?

I think what you're looking for our "stories": https://github.com/zenstruc... - with these, you could define - using the boring, but readable code that you have at the top of your post, the one with the loop - all of the lookup data you need. And then you can simply execute the story to get everything set up. This is what stories are meant for :) - let me know what you think.

> Also I am trying to figure it out how to pass random object per each iteration according their documentation

This is where Foundry gets really interesting, but also a little tricky, as there are subtle ways to do this "relation" stuff and they all have different effects. So, I think what you want is this:


CommentFactory::new()->createMany(10, function() {
return ['post' => PostFactory::random()];
});

Let me know if that works :). With the original code, PostFactory::random() is executed once, and that ONE object is passed to the createMany() function, which it then uses for all 10 comments. With the callback, the callback is called 10 times, which allows the random() function to be called each time. This is documented, but only in the Many-To-Many section - the ManyToOne probably needs another example :).

Cheers!

Reply
quynh-ngan_vu Avatar
quynh-ngan_vu Avatar quynh-ngan_vu | posted 2 years ago

Hi!
Just a little note :)
Francois Zaninotto recently archived the library Faker in October 2020.
https://github.com/fzaninot...

Will you continue using Faker? If not, will you update this course and the symfony documentation?

Cheers!

Reply

Hey quynh-ngan_vu!

Yea, I did see that Faker was recently archived. Fortunately, it's already been rescued! https://github.com/FakerPHP...

Unless something changes dramatically, yes, I'll definitely continue using it :). We're going to update some courses & add some notes to use the new library. For the official Symfony docs, I don't think they mention Faker there, so I don't think there is anything to update.

Cheers!

Reply

Hi. I am working with the afterInstantiate in the initialize function.
Everything is working fine but my PHPStorm is showing a warning. How am I supposed to get that away? I prefer to make sure there are no more warnings in my PHPStorm before i commit my stuff into my git.

The warning is as follows:

Return value is expected to be 'UserFactory', '\Zenstruck\Foundry\Factory' returned

Here is my code:

protected function initialize(): self
{
return $this
->afterInstantiate(
function (User $user) {
$user->setPassword(
$this->passwordEncoder->encodePassword(
$user,
'engage'
)
);
}
);
}

Thanks for your help in advance!

Reply
Kevin B. Avatar
Kevin B. Avatar Kevin B. | sciro | posted 2 years ago | edited

Hey sciro - I'm the creator of foundry.

To be clear, the code does work as expected and this is only a PHPStorm warning? I'm also using PHPStorm and I don't see this - what version (of PHPStorm) are you on?

The issue is, `self` in this context, refers to `UserFactory` but the `afterInstantiate()` method has a `self` return type which in that context, refers to `\Zenstruck\Foundry\Factory`. So I understand the warning but as far as I'm aware, this is valid PHP (for 7.2+ anyway) which is why I'm asking about your PHPStorm version.

Does removing the `self` return hint remove the warning (this will still be valid code)?

Reply

Hey Kevin B.,
I was using 2020.3 EAP when the error was showing. I now changed back to 2020.2.3 and the warning is gone! I just tested to change the PHP language level in the settings, it was on 7.2, even if I put it on 7.3 or 7.4 it still shows the same warning in 2020.3 EAP. With 2020.2.3 everythings works fine, that's so weird. Didn't expect that to happen even with a EAP.

And when I remove the : self the warning stops showing up in 2020.3 EAP too. But I'm just gonna use 2020.2.3 to be safe for now.

Thank you for taking your time to help me! I really like foundry, it's great! :)

1 Reply
Default user avatar
Default user avatar Utilisateur | posted 2 years ago

Hi!
I'm trying to work my way around Foundry but I'm facing two issues.

First one: I'd like to create dummy users and I've got a password property. So I tried to emulate the Symfony Doc tip to encode password into my UserFactory class, creating a __construct method and using passwordEncoder, though I can't make it work. Should I create a random fake password (self::faker()->password) first and then replace it, or am I doing all that wrong? Any advice would be greatly appreciated.

Second issue: I've got that Article class I'd like to fill with dummy articles too. There's a relation between Article and User: the author property. I don't know how to write my code so that in ArticleFactory the author property is set using one of the dummy user's usernames created earlier. I always get stuck with following error, whatever value I write: Expected argument of type "App\Entity\User or null", "string" given at property path "author".

I'd be very grateful to read your thoughts on those issues. I hope I explained them right.

Cheers!

Reply
Kevin B. Avatar
Kevin B. Avatar Kevin B. | Utilisateur | posted 2 years ago | edited

Hey Utilisateur

The latest version of Foundry allows defining "factories as services" so you now can inject the password encoder service into your UserFactory. The example in the docs shows just that.

Ryan's suggestion of pre-encoding the password is still my preferred method but you now have the option. Pre-encoding is now documented as well (#3 in the linked list).

1 Reply
Default user avatar
Default user avatar Utilisateur | Kevin B. | posted 2 years ago | edited

Hi Kevin B.
Thanks for the heads up; it's always good to know an option is available without requiring workaround.
Cheers!

Reply

Hey Utilisateur !

Excellent questions!

> First one: I'd like to create dummy users and I've got a password property

I had a long conversation about this with Foundry's creator Kevin. The problem (and it's kind of by design) is that the factories are not services, and so you can't use autowiring (Which is why you're having trouble putting the passwordEncoder into the constructor - that just won't work). Kevin helped me to see that the password is a very special situation where you need a service to do per-processing before the value is saved. He recommends using the php bin/console security:encode-password command to manually generate a password and then use that in UserFactory. So you would literally set password to some crazy, encoded string like $argon2id$v=19$m=65536,t=4,p=1$Jv2osf1dBQHZF06/IojYmw$SjeYLKBJCExAvn03b7vwE3pUBbrgitMrt5LQF0fi49M and above that I'd add a comment like "password = foo".

Part of the reason that (as manual, and not fancy as this sounds) this is really nice, is that password encoding is actually something that can really start to slow down your tests. Foundry even has a section talking about this: https://github.com/zenstruc...

So, that's what I recommend - it was a weird idea at first, but I like it :).

> Second issue: I've got that Article class I'd like to fill with dummy articles too. There's a relation between Article and User: the author property. I don't know how to write my code so that in ArticleFactory the author property is set using one of the dummy user's usernames created earlier

Hmm. What you would typically do is:

A) In ArticleFactory::getDefaults(), ignore everything else, and return the minimum number of fields needed to successfully create an Article. So for the author property, you should use: 'author' => UserFactory::new() (yes, you actually pass a UserFactory instance - it's a bit better than an actual User object, because it's "lazy" and won't be created if you override the author property when using the factory... it's a bit complex to explain shortly, and there is a PR currently to update some things in this area - https://github.com/zenstruc....

B) When you are using ArticleFactory, if you' would like to override the author, you can totally do that. But you need to set the author attribute to a User object or UserFactory. I'm guessing you'll need something like this:


// whatever code you have currently to create some users
UserFactory::new()->createMany();

ArticleFactory::new()->create([
// override the author
// the repository() method gives you a repository proxy
// https://github.com/zenstruck/foundry#repository-proxy
'author' => UserFactory::repository()->random()
]);

Let me know if that helps!

Cheers!

Reply
Default user avatar

Hey Ryan, I wasn't expecting you to reply this fast. I've just tried your solution and it worked like a charm! Thanks a lot!

Just for the record, in the meantime I stumbled upon another tutorial of yours where you used the passwordEncoder into a constructor in the AppFixtures class. I guess it's still better from a performance standpoint - as you said - to encode it beforehand and pass it as a string in the UserFactory class so it is not encoded again for each new dummy user.

Anyway, you really helped me out there. Cheers!

1 Reply
Ismael A. Avatar
Ismael A. Avatar Ismael A. | posted 2 years ago

Hi!

In the example, the hook afterInstantiate was used for defining the slug if it wasn't defined already.
Are there any benefits for for using that hook instead of using beforeInstantiate and work around the attributes array? Not applicable to this example, but I thought maybe the before hook would be helpful when using immutable entities or just constructor arguments instead of setters.

Cheers!

Reply

Hey Ismael A.!

Hmm, nice thinking! Actually, I would say that *your* way probably has more benefits in general, because you're using more of Foundry's features - I just hadn't thought of it! Using afterInstantiate() gives you more power (e.g. you could call any method on your entity - like configure() or something), but before is kinda cool :).

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": "^7.4.1",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "doctrine/doctrine-bundle": "^2.1", // 2.1.1
        "doctrine/doctrine-migrations-bundle": "^3.0", // 3.0.2
        "doctrine/orm": "^2.7", // 2.8.2
        "knplabs/knp-markdown-bundle": "^1.8", // 1.9.0
        "knplabs/knp-time-bundle": "^1.11", // v1.16.0
        "sensio/framework-extra-bundle": "^6.0", // v6.2.1
        "sentry/sentry-symfony": "^4.0", // 4.0.3
        "stof/doctrine-extensions-bundle": "^1.4", // v1.5.0
        "symfony/asset": "5.1.*", // v5.1.2
        "symfony/console": "5.1.*", // v5.1.2
        "symfony/dotenv": "5.1.*", // v5.1.2
        "symfony/flex": "^1.3.1", // v1.17.5
        "symfony/framework-bundle": "5.1.*", // v5.1.2
        "symfony/monolog-bundle": "^3.0", // v3.5.0
        "symfony/stopwatch": "5.1.*", // v5.1.2
        "symfony/twig-bundle": "5.1.*", // v5.1.2
        "symfony/webpack-encore-bundle": "^1.7", // v1.8.0
        "symfony/yaml": "5.1.*", // v5.1.2
        "twig/extra-bundle": "^2.12|^3.0", // v3.0.4
        "twig/twig": "^2.12|^3.0" // v3.0.4
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.3", // 3.4.0
        "symfony/debug-bundle": "5.1.*", // v5.1.2
        "symfony/maker-bundle": "^1.15", // v1.23.0
        "symfony/var-dumper": "5.1.*", // v5.1.2
        "symfony/web-profiler-bundle": "5.1.*", // v5.1.2
        "zenstruck/foundry": "^1.1" // v1.5.0
    }
}