为什么 Laravel 不能猜测你的 Factory 关系

发布: (2026年1月14日 GMT+8 02:43)
3 min read
原文: Dev.to

Source: Dev.to

问题

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

    public function distributor(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }
}
$owner      = UserFactory::new()->create(['name' => 'Owner']);
$distributor = UserFactory::new()->create(['name' => 'Distributor']);

$client = ClientFactory::new()
    ->for($owner)
    ->for($distributor)
    ->create();

Laravel 会把 user_id 设置为 distributor 的 ID,而把 distributor_id 留为 null。这是因为 for($model) 只会检查模型的 ,而不会考虑变量名。由于 $owner$distributor 都是 User 实例,Laravel 会选取它找到的 第一个 User 关系(user)。

明确指定关系

通过在第二个参数传入关系名称来告诉 Laravel 使用哪个关系:

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

现在两个外键都会被正确填充。

另一种方式:直接设置外键

有时直接为键赋值会更清晰:

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

这样可以避免一连串的 for() 调用,并且意图更加明确。

设计考虑

遵循 Laravel 的约定通常能获得最佳体验:

  • 单一模型的关系(例如 ClientUser)非常直接。
  • 为同一模型添加第二个关系可能会让人困惑。在某些情况下,引入专门的模型(例如 Distributor)能够更好地表达业务领域。
  • 命名很重要。如果业务语言使用 “owner” 与 “distributor”,则相应地命名关系可以降低认知负担。
Client   → Customer
user     → owner

使用清晰、领域特定的名称可以减少意外。

实际案例

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

在这个例子中,默认的 for($user) 能正常工作,因为 Address 上只有一个指向 User 的关系。

要点总结

  • for() 会选取第一个与模型类匹配的关系。
  • 当存在多个关系时,使用第二个参数明确指定关系。
  • 也可以在工厂负载中直接设置外键。
  • 将模型和关系的命名与业务语言保持一致,以避免混淆。
  • 当业务需求偏离 Laravel 的约定时,显式指定胜过隐式魔法。

感谢 Joel Clermont 提供的原始视频,它启发了本文的创作。他的 Laravel 小技巧值得一看。

Back to Blog

相关文章

阅读更多 »