Laravel Factories: Tips for Handling Dependent Data

Laravel Factories: Tips for Handling Dependent Data

Something I recently came across while working on a Laravel app was an unoptimized factory. I’ve seen this many times and have even been guilty of it. Let’s say we have three models, a User, a Team, and a Project. A team can have a team owner (teams.user_id), a project can have an owner (projects.user_id) and a team (projects.team_id). In the ProjectFactory, I want to create a user and a team, and that team should have the user as the owner. The first attempt might look something like this:

class ProjectFactory extends Factory
{
    protected $model = Project::class;

    public function definition(): array
    {
        return [
            'title' => $this->faker->sentence(),
            'description' => $this->faker->paragraph(),
            'team_id' => Team::factory(),
            'user_id' => User::factory(),
        ];
    }
}

The problem with this is the Team factory is creating its own User and now the User factory created a user, and they are not related.

The next iteration could look like this:

class ProjectFactory extends Factory
{
    protected $model = Project::class;

    public function definition(): array
    {
        $team = Team::factory()->create();

        return [
            'title' => $this->faker->sentence(),
            'description' => $this->faker->paragraph(),
            'team_id' => $team->id,
            'user_id' => $team->user_id,
        ];
    }
}

This works and gives us what we want, or does it?

It will give us a Project with a User and Team that are related. However, what happens if we were using the factory and we already knew the team? Something like the following:

$project = Project::factory()
    ->for($previouslyCreatedTeam, 'team')
    ->for($previouslyCreatedTeam->owner, 'user')
    ->create();

If we look in the database, we should see a single user, team, and project record. However, looking at the users table, I have two users:

Users table

In the teams table, I have two teams!

Teams table

What happened?

The issue is, any time the factory runs, it calls the definition method, and then it looks at the fields that were passed to the factory to see which fields of the definition it needs to use. When the definition method is called, though, nothing is preventing it from calling the Team factory and creating a Team and User.

class ProjectFactory extends Factory
{
    protected $model = Project::class;

    public function definition(): array
    {
        // This runs regardless of the fields provided
        // to the factory, creating extra teams and users
        // when not necessary.
        $team = Team::factory()->create();

        return [
            'title' => $this->faker->sentence(),
            'description' => $this->faker->paragraph(),
            'team_id' => $team->id,
            'user_id' => $team->user_id,
        ];
    }
}

How do we fix this?

It’s actually right in the Laravel docs.

If the relationship's columns depend on the factory that defines it you may assign a closure to an attribute.

So to fix it, we can use a closure to get the User from the Team factory used in the definition:

class ProjectFactory extends Factory
{
    protected $model = Project::class;

    public function definition(): array
    {
        return [
            'title' => $this->faker->sentence(),
            'description' => $this->faker->paragraph(),
            'team_id' => Team::factory(),
            // Fetch the user_id from the team that was 
            // generated in the factory above.
            'user_id' => fn (array $attributes) => Team::find($attributes['team_id'])->user_id,
        ];
    }
}

This is a bit of an edge case and doesn’t frequently happen, so it’s easy to miss in the docs. I hope this helps, and now you can go and improve your factories. Thanks for reading!

Did you find this article valuable?

Support Sean Kegel by becoming a sponsor. Any amount is appreciated!