Laravel factory relationships... respect create() or make()

Viewed 3295

According to Laravel's documentation on defining relationships within model factories:

You may also attach relationships to models using Closure attributes in your factory definitions. For example, if you would like to create a new User instance when creating a Post, you may do the following:

$factory->define(App\Post::class, function ($faker) {
    return [
        'title' => $faker->title,
        'content' => $faker->paragraph,
        'user_id' => function () {
            return factory(App\User::class)->create()->id;
        }
    ];
});

The issue I'm running into is the reference to create() within the relationship definition. It seems to me that this doesn't belong here.

It works great if I am wanting to persist my relationships to the database:

factory(App\Post::class)->create();

By running the code directly above this, a new App\Post and a new App\User will be created and both persisted to the database.

But if I just want to new up the model(s) and not persist anything (at all) to the database by running:

factory(App\Post::class)->make();

It does what I want up to a certain point. A new App\Post instance is created but not persisted, however App\Comment is created and is persisted to the database.


It seems to me, that what I really want is something like this:

$factory->define(App\Post::class, function ($faker) {
    return [
        'title' => $faker->title,
        'content' => $faker->paragraph,
        'user_id' => function () {
            // here I only want to declare the relationship,
            // not decide whether I want to create() or make()
            // the relationship, something like:
            return factory(App\User::class)->createOrMake()->id;

            // or perhaps something like:
            return factory(App\User::class)->id;
        }
    ];
});

The end result is that I want the related data to respect what I'm trying to do from the top of the call, down. Make everything. Or create everything.


Am I doing something wrong here? Or is this something that doesn't currently exist?

Thanks!

2 Answers

You want the lazy() method for your related models!

At least in Laravel 5.5.

I found your question here because I was having the same issue. I found the answer thanks to Laravel's beautifully written code -- lazy() was defined just above the make() XDebug took me to -- and I've tried it out and it seems to work.

This is what I'm doing:

$factory->define(App\ArtItem::class, function (Faker $faker) {
    return [
        // 'id' => $faker->randomDigit,
        'slug' => $faker->unique->word,
        'artist_id' => factory(App\Artist::class)->lazy(),
        'image_id' => factory(App\Image::class)->lazy(),
        'created_at' => $faker->dateTime,
        'updated_at' => $faker->dateTime,
        'deleted_at' => $faker->dateTime,
    ];
});

Lazy() is an interesting creature that returns a closure, so you can't do factory(App\Image::class)->lazy()->id but I'm still seeing it successfully setting the correct image_id and artist_id.

I certainly hope you found the solution long before this, but maybe this'll help someone else!

This is finally solvable now using Factory Callbacks added in Laravel 5.6.12.

$factory->afterMaking(Post::class, static function (Post $post) {
    if (!$post->user_id) {
        $post->user()->associate(factory(User::class)->make(['id' => 0]));
    }
});
$factory->afterCreating(Post::class, static function (Post $post) {
    if (!$post->user_id) {
        $post->user()->associate(factory(User::class)->create())->save();
    }
});

Now, whenever you make a Post, if you don't pass it a user_id, it will make a User for you and set the relationship without saving it. But if you create a Post, it will persist a new User to the database and save its ID to the Post model.

Note that both the afterMaking and afterCreating closures are called when you create a factory model. I am temporarily setting the User id to 0 in case the foreign key posts.user_id was defined not null. It will be updated to the correct value in afterCreating. This is a terrible hack, but it works until we get a proper beforeCreating hook which would allow us to create the related model first.

Related