什么是工厂
在测试和数据库填充中,往往需要向数据库插入记录。
比起手动指定每一列的值,使用 Laravel 的模型工厂可以为每个 Eloquent 模型定义一套默认属性。
每个新的 Laravel 应用都自带了 database/factories/UserFactory.php。
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
class UserFactory extends Factory
{
// 缓存同一个密码,避免每次都重新哈希
protected static ?string $password;
public function definition(): array
{
return [
'name' => fake()->name(),
'email' => fake()->unique()->safeEmail(),
'email_verified_at' => now(),
'password' => static::$password ??= Hash::make('password'),
'remember_token' => Str::random(10),
];
}
}
通过 fake() 辅助函数可以使用 Faker 库,生成各种随机数据。
Faker 的语言环境可以通过 config/app.php 中的 faker_locale 选项修改。
生成工厂类
使用 make:factory Artisan 命令来创建工厂。
php artisan make:factory PostFactory
新的工厂类会被放到 database/factories 目录下。
模型与工厂的自动发现
在模型上使用 HasFactory trait 时,Laravel 会自动查找对应的工厂类。
它会在 Database\Factories 命名空间中查找 “模型名 + Factory” 的类名。
如果命名规则不匹配,可以使用 UseFactory 属性显式指定。
use Illuminate\Database\Eloquent\Attributes\UseFactory;
use Database\Factories\Administration\FlightFactory;
#[UseFactory(FlightFactory::class)]
class Flight extends Model
{
// ...
}
定义工厂
definition() 方法
工厂的核心是 definition() 方法,用来定义默认属性。
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
class PostFactory extends Factory
{
public function definition(): array
{
return [
'user_id' => \App\Models\User::factory(),
'title' => fake()->sentence(),
'content' => fake()->paragraphs(3, true),
'published_at' => fake()->optional()->dateTimeBetween('-1 year', 'now'),
];
}
}
Faker 常用方法一览:
| 方法 | 示例结果 |
|---|
fake()->name() | John Doe |
fake()->email() | [email protected] |
fake()->sentence() | 随机句子 |
fake()->paragraph() | 随机段落 |
fake()->numberBetween(1, 100) | 1~100 的整数 |
fake()->dateTime() | 随机日期时间 |
fake()->boolean() | true / false |
工厂状态(States)
通过 state 方法可以为工厂定义单独的属性变化。
use Illuminate\Database\Eloquent\Factories\Factory;
class UserFactory extends Factory
{
public function definition(): array
{
return [
'name' => fake()->name(),
'email' => fake()->unique()->safeEmail(),
'account_status' => 'active',
];
}
// 停用用户的 state
public function suspended(): static
{
return $this->state(fn (array $attributes) => [
'account_status' => 'suspended',
]);
}
// 管理员用户的 state
public function admin(): static
{
return $this->state(fn (array $attributes) => [
'is_admin' => true,
]);
}
}
state 可以组合使用。
$user = User::factory()->suspended()->create();
$admin = User::factory()->admin()->create();
软删除 state
对启用了软删除的模型,可以使用内置的 trashed() state。
$user = User::factory()->trashed()->create();
工厂回调(Callbacks)
通过 afterMaking 和 afterCreating 可以定义模型生成后的附加处理。
namespace Database\Factories;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
class UserFactory extends Factory
{
public function configure(): static
{
return $this->afterMaking(function (User $user) {
// make() 之后执行(尚未存入数据库)
})->afterCreating(function (User $user) {
// create() 之后执行(已经存入数据库)
});
}
// ...
}
state 方法内也可以注册回调。
public function withProfile(): static
{
return $this->state(fn (array $attributes) => [])
->afterCreating(function (User $user) {
$user->profile()->create([
'bio' => fake()->paragraph(),
]);
});
}
生成模型
make() — 不保存到数据库
use App\Models\User;
// 生成 1 个
$user = User::factory()->make();
// 生成 3 个(返回集合)
$users = User::factory()->count(3)->make();
create() — 保存到数据库
// 生成 1 个并保存
$user = User::factory()->create();
// 生成 3 个并保存
$users = User::factory()->count(3)->create();
覆盖属性
给 make() 或 create() 传入数组,可以只覆盖特定属性。
$user = User::factory()->make([
'name' => '山田太郎',
]);
$user = User::factory()->create([
'name' => '铃木花子',
'email' => '[email protected]',
]);
也可以通过 state 实现内联覆盖。
$user = User::factory()->state([
'name' => '田中一郎',
])->make();
Sequence(序列)
在批量生成模型时,如果希望按顺序切换属性,可以使用 Sequence。
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Sequence;
$users = User::factory()
->count(10)
->state(new Sequence(
['admin' => 'Y'],
['admin' => 'N'],
))
->create();
// 5 条数据 admin='Y',5 条 admin='N'
使用 sequence() 方法可以写得更简洁。
$users = User::factory()
->count(2)
->sequence(
['name' => '第一个用户'],
['name' => '第二个用户'],
)
->create();
也可以通过闭包动态生成随机值。
use Illuminate\Database\Eloquent\Factories\Sequence;
$users = User::factory()
->count(10)
->state(new Sequence(
fn (Sequence $sequence) => ['name' => '用户 ' . $sequence->index],
))
->create();
关联关系的工厂
Has Many(一对多)
使用 has() 方法生成带“一对多”关联的模型。
use App\Models\Post;
use App\Models\User;
// 生成一个 User,附带 3 篇 Post
$user = User::factory()
->has(Post::factory()->count(3))
->create();
使用魔术方法(has + 关联名的复数)可以写得更简洁。
$user = User::factory()
->hasPosts(3)
->create();
// 覆盖属性
$user = User::factory()
->hasPosts(3, ['published' => false])
->create();
Belongs To(反向关联)
使用 for() 方法指定所“属于”的父模型。
use App\Models\Post;
use App\Models\User;
// 生成属于特定用户的 3 篇 Post
$posts = Post::factory()
->count(3)
->for(User::factory()->state(['name' => '田中花子']))
->create();
// 也可以使用已存在的模型作为父级
$user = User::factory()->create();
$posts = Post::factory()->count(3)->for($user)->create();
魔术方法版本:
$posts = Post::factory()
->count(3)
->forUser(['name' => '田中花子'])
->create();
Many to Many(多对多)
使用 hasAttached() 方法处理多对多关联,还可以指定中间表的属性。
use App\Models\Role;
use App\Models\User;
$user = User::factory()
->hasAttached(
Role::factory()->count(3),
['active' => true] // 中间表属性
)
->create();
魔术方法版本:
$user = User::factory()
->hasRoles(1, ['name' => 'Editor'])
->create();
多态关联
普通“一对多”一样,可以使用 has() 或魔术方法。
use App\Models\Post;
$post = Post::factory()->hasComments(3)->create();
对于 morphTo 关联,不能使用魔术方法。需要在 for() 中明确指定关联名。
$comments = Comment::factory()->count(3)->for(
Post::factory(), 'commentable'
)->create();
工厂内定义关联
在 definition() 中把另一个工厂赋值给外键,就能在生成模型时自动创建父模型。
class PostFactory extends Factory
{
public function definition(): array
{
return [
'user_id' => \App\Models\User::factory(),
'title' => fake()->sentence(),
'content' => fake()->paragraph(),
];
}
}
recycle() — 复用已有模型
如果多个关联想复用同一个模型实例,可以使用 recycle()。
// 同一个 Airline 在 Ticket 与 Flight 中反复使用
Ticket::factory()
->recycle(Airline::factory()->create())
->create();
// 从集合中随机挑选
$airlines = Airline::factory()->count(3)->create();
Ticket::factory()->count(10)->recycle($airlines)->create();
在数据填充中使用
可以在 DatabaseSeeder 或其他 Seeder 类中调用工厂。
namespace Database\Seeders;
use App\Models\User;
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
public function run(): void
{
// 生成 10 个用户,每个用户各带 3 篇 Post
User::factory()
->count(10)
->hasPosts(3)
->create();
}
}
执行 Seeder:
在测试中使用
测试时通常将 RefreshDatabase trait 与工厂搭配使用。
namespace Tests\Feature;
use App\Models\User;
use App\Models\Post;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class PostTest extends TestCase
{
use RefreshDatabase;
public function test_user_can_view_their_posts(): void
{
$user = User::factory()->create();
$posts = Post::factory()->count(3)->for($user)->create();
$response = $this->actingAs($user)
->get('/dashboard');
$response->assertOk();
$response->assertSee($posts->first()->title);
}
public function test_suspended_user_cannot_post(): void
{
$user = User::factory()->suspended()->create();
$response = $this->actingAs($user)
->post('/posts', ['title' => '测试']);
$response->assertForbidden();
}
}
RefreshDatabase 会在每个测试之间重置数据库,因此测试之间的数据不会相互干扰。
使用 Pest 编写测试时工厂 API 完全一样。
记得通过 uses(RefreshDatabase::class) 重置数据库。
下一步
数据库填充
查看如何组合 Seeder 与工厂来注入初始数据。
测试入门
了解 Laravel 的测试功能以及 RefreshDatabase trait 的使用方式。