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:
In the teams
table, I have two teams!
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!