DEV Community

Cover image for Why Laravel Can't Guess Your Factory Relationships
Ivan Mykhavko
Ivan Mykhavko

Posted on

Why Laravel Can't Guess Your Factory Relationships

Laravel factories make testing a breeze, especially when you've got models that connect to each other. But sometimes, they'll trip you up in ways that aren't obvious at first. Joel Clermont made a great video about this, and I wanted to share my own take.

The Problem

Picture this: you've got a Client model, and it has two relationships to the same User model.

final class Client extends Model
{
    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }

    public function distributor(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }
}
Enter fullscreen mode Exit fullscreen mode

Now you're writing a test:

$owner = UserFactory::new()->create(['name' => 'Owner']);
$distributor = UserFactory::new()->create(['name' => 'Distributor']);

$client = ClientFactory::new()
    ->for($owner)
    ->for($distributor)
    ->create();
Enter fullscreen mode Exit fullscreen mode

What happens? Laravel sets user_id to the distributor's id, not the owner's. The distributor_id stays null. Not what you wanted.

Why This Happens

Laravel isn't broken, it's just sticking to its rules.

When you call for($model), Laravel looks at the model type, not your variable name. Both $owner and $distributor are User models. Laravel can't read your mind, so it just grabs the first relationship to User it finds, which is user. The variable names don't matter.

The Solution

Be explicit, tell laravel which relationship to use:

$client = ClientFactory::new()
    ->for($owner, 'user')
    ->for($distributor, 'distributor')
    ->create();
Enter fullscreen mode Exit fullscreen mode

Now it works perfectly. The second argument is just the relationship name as a string.

Alternative: skip for

Sometimes being direct is clearer:

$client = ClientFactory::new()->create([
    'user_id' => $owner->id,
    'distributor_id' => $distributor->id,
]);
Enter fullscreen mode Exit fullscreen mode

Nothing wrong here. Honestly, it's often clearer than stacking a bunch of for() calls.

When Conventions Break Down

Here's the thing: Laravel works best when you follow conventions. A Client with a user relationship? Perfect. But when you add a second relationship to the same model without a clear semantic difference, you're bending the rules a bit. From a pure Laravel-domain perspective, this might even suggest introducing a separate Distributor model. That doesn't mean it's wrong, sometimes you genuinely need multiple relationships to the same model.
So being explicit about relationship names keeps everything clear.

I checked my current project, and there are not many cases where for() is used with an explicit relationship name.
Just found this one:

$address = AddressFactory::new()
    ->for($user)
    ->has(DirectionFactory::new()
        ->has(DirectionScheduleFactory::new()->count(10), 'schedules'))
    ->create();
Enter fullscreen mode Exit fullscreen mode

Naming matters

Some naming improvements can also reduce confusion:

  • ClientCustomer
  • userowner

When your relationship is called user but your domain talks about "owners," you're creating unnecesary mental overhead. Clearer names = fewer surprises.

Conclusion

Factories are powerful, but they rely on conventions. When your model design moves away from those conventions, explicitness beats magic:

  • Pass the relationship name to for(), or
  • Set the foreign keys yourself.

Laravel is doing exactly what it should. The responsibility is on us to be clear about our intent.

Author's Note

Thanks for sticking around!
Find me on dev.to, linkedin, or you can check out my work on github.

Notes from real-world Laravel.


Thanks to Joel Clermont for the original video that inspired this post. His Laravel tips are always worth checking out.

Top comments (4)

Collapse
 
xwero profile image
david duymelinck

Another option is using the for magic method.

$client = Client::factory()
   ->forOwner(['name' => 'Owner'])
   ->forDistributor(['name' => 'Distributor'])
   ->create();
Enter fullscreen mode Exit fullscreen mode

ClientFactory::new() feels like ancient code to me, too much typing.

Collapse
 
tegos profile image
Ivan Mykhavko

Very nice approach! 👍
Personally, I'm not a fan of this kind of "magic".

I prefer either setting the foreign key columns directly, or if the model always requires these relations using a dedicated fixture.

I intentionally chose the ClientFactory::new() style with explicit factory usage, without magic methods and extra traits on the model. This keeps things more explicit.

There are also established conventions that usigng factories this way.

Collapse
 
xwero profile image
david duymelinck • Edited

Personally, I'm not a fan of this kind of "magic".

Normally I have the same reservations concerning magic methods.
In this case I think the magic method is beneficial.

I prefer either setting the foreign key columns directly

The problem I see with that approach is maintenance. When the decision is made to change the column, then the test will break. I want to do as less test maintenance as possible. When the model configuration can find it for you, let it do its thing.

ClientFactory::new() style with explicit factory usage

How is Client::factory() less explicit? I don't think one is less explicit than the other.

extra traits on the model

I think that is the best reason to use the factory class. Because the factory is a part of the test code. Which means the model should have no ties with the factory. No trait or attribute on the model is the way to go.

For conventions I first go to the Laravel documentation, but in this case it is better to use the factory class. So thank you for bringing that to my attention.

Thread Thread
 
tegos profile image
Ivan Mykhavko

Fair points!
Good discussion, thanks for sharing your perspective.