> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# Eloquent 工厂

> 介绍如何使用模型工厂高效生成测试与数据填充所需的假数据。

## 什么是工厂

在测试和数据库填充中，往往需要向数据库插入记录。
比起手动指定每一列的值，使用 Laravel 的**模型工厂**可以为每个 Eloquent 模型定义一套默认属性。

每个新的 Laravel 应用都自带了 `database/factories/UserFactory.php`。

```php theme={null}
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](https://github.com/FakerPHP/Faker) 库，生成各种随机数据。

```mermaid theme={null}
flowchart LR
    F["Factory<br>database/factories/UserFactory"] -->|"make() / create()"| M["Model Instance<br>App\\Models\\User"]
    M -->|"create()"| DB[("Database")]
    F -->|"Faker"| R["随机数据<br>name, email, ..."]
    R --> F
```

<Info>
  Faker 的语言环境可以通过 `config/app.php` 中的 `faker_locale` 选项修改。
</Info>

## 生成工厂类

使用 `make:factory` Artisan 命令来创建工厂。

```shell theme={null}
php artisan make:factory PostFactory
```

新的工厂类会被放到 `database/factories` 目录下。

### 模型与工厂的自动发现

在模型上使用 `HasFactory` trait 时，Laravel 会自动查找对应的工厂类。
它会在 `Database\Factories` 命名空间中查找 “模型名 + Factory” 的类名。

如果命名规则不匹配，可以使用 `UseFactory` 属性显式指定。

```php theme={null}
use Illuminate\Database\Eloquent\Attributes\UseFactory;
use Database\Factories\Administration\FlightFactory;

#[UseFactory(FlightFactory::class)]
class Flight extends Model
{
    // ...
}
```

## 定义工厂

### `definition()` 方法

工厂的核心是 `definition()` 方法，用来定义默认属性。

```php theme={null}
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()`               | `john@example.com` |
| `fake()->sentence()`            | 随机句子               |
| `fake()->paragraph()`           | 随机段落               |
| `fake()->numberBetween(1, 100)` | `1`～`100` 的整数      |
| `fake()->dateTime()`            | 随机日期时间             |
| `fake()->boolean()`             | `true` / `false`   |

## 工厂状态（States）

通过 state 方法可以为工厂定义单独的属性变化。

```php theme={null}
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 可以组合使用。

```php theme={null}
$user = User::factory()->suspended()->create();
$admin = User::factory()->admin()->create();
```

### 软删除 state

对启用了软删除的模型，可以使用内置的 `trashed()` state。

```php theme={null}
$user = User::factory()->trashed()->create();
```

## 工厂回调（Callbacks）

通过 `afterMaking` 和 `afterCreating` 可以定义模型生成后的附加处理。

```php theme={null}
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 方法内也可以注册回调。

```php theme={null}
public function withProfile(): static
{
    return $this->state(fn (array $attributes) => [])
        ->afterCreating(function (User $user) {
            $user->profile()->create([
                'bio' => fake()->paragraph(),
            ]);
        });
}
```

## 生成模型

### make() — 不保存到数据库

```php theme={null}
use App\Models\User;

// 生成 1 个
$user = User::factory()->make();

// 生成 3 个（返回集合）
$users = User::factory()->count(3)->make();
```

### create() — 保存到数据库

```php theme={null}
// 生成 1 个并保存
$user = User::factory()->create();

// 生成 3 个并保存
$users = User::factory()->count(3)->create();
```

### 覆盖属性

给 `make()` 或 `create()` 传入数组，可以只覆盖特定属性。

```php theme={null}
$user = User::factory()->make([
    'name' => '山田太郎',
]);

$user = User::factory()->create([
    'name' => '铃木花子',
    'email' => 'hanako@example.com',
]);
```

也可以通过 state 实现内联覆盖。

```php theme={null}
$user = User::factory()->state([
    'name' => '田中一郎',
])->make();
```

<Info>
  用工厂生成模型时，批量赋值保护会被自动禁用。
</Info>

### Sequence（序列）

在批量生成模型时，如果希望按顺序切换属性，可以使用 `Sequence`。

```php theme={null}
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()` 方法可以写得更简洁。

```php theme={null}
$users = User::factory()
    ->count(2)
    ->sequence(
        ['name' => '第一个用户'],
        ['name' => '第二个用户'],
    )
    ->create();
```

也可以通过闭包动态生成随机值。

```php theme={null}
use Illuminate\Database\Eloquent\Factories\Sequence;

$users = User::factory()
    ->count(10)
    ->state(new Sequence(
        fn (Sequence $sequence) => ['name' => '用户 ' . $sequence->index],
    ))
    ->create();
```

## 关联关系的工厂

```mermaid theme={null}
flowchart TD
    UF["UserFactory"] -->|"has(PostFactory)"| PF["PostFactory"]
    PF -->|"for(UserFactory)"| UF
    UF -->|"hasAttached(RoleFactory)"| RF["RoleFactory"]

    UF -->|"create()"| U["User 模型"]
    PF -->|"create()"| P["Post 模型"]
    RF -->|"create()"| R["Role 模型"]

    U -->|"hasMany"| P
    U -->|"belongsToMany"| R
```

### Has Many（一对多）

使用 `has()` 方法生成带“一对多”关联的模型。

```php theme={null}
use App\Models\Post;
use App\Models\User;

// 生成一个 User，附带 3 篇 Post
$user = User::factory()
    ->has(Post::factory()->count(3))
    ->create();
```

使用魔术方法（`has` + 关联名的复数）可以写得更简洁。

```php theme={null}
$user = User::factory()
    ->hasPosts(3)
    ->create();

// 覆盖属性
$user = User::factory()
    ->hasPosts(3, ['published' => false])
    ->create();
```

### Belongs To（反向关联）

使用 `for()` 方法指定所“属于”的父模型。

```php theme={null}
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();
```

魔术方法版本：

```php theme={null}
$posts = Post::factory()
    ->count(3)
    ->forUser(['name' => '田中花子'])
    ->create();
```

### Many to Many（多对多）

使用 `hasAttached()` 方法处理多对多关联，还可以指定中间表的属性。

```php theme={null}
use App\Models\Role;
use App\Models\User;

$user = User::factory()
    ->hasAttached(
        Role::factory()->count(3),
        ['active' => true]  // 中间表属性
    )
    ->create();
```

魔术方法版本：

```php theme={null}
$user = User::factory()
    ->hasRoles(1, ['name' => 'Editor'])
    ->create();
```

### 多态关联

普通“一对多”一样，可以使用 `has()` 或魔术方法。

```php theme={null}
use App\Models\Post;

$post = Post::factory()->hasComments(3)->create();
```

对于 `morphTo` 关联，不能使用魔术方法。需要在 `for()` 中明确指定关联名。

```php theme={null}
$comments = Comment::factory()->count(3)->for(
    Post::factory(), 'commentable'
)->create();
```

### 工厂内定义关联

在 `definition()` 中把另一个工厂赋值给外键，就能在生成模型时自动创建父模型。

```php theme={null}
class PostFactory extends Factory
{
    public function definition(): array
    {
        return [
            'user_id' => \App\Models\User::factory(),
            'title' => fake()->sentence(),
            'content' => fake()->paragraph(),
        ];
    }
}
```

### recycle() — 复用已有模型

如果多个关联想复用同一个模型实例，可以使用 `recycle()`。

```php theme={null}
// 同一个 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 类中调用工厂。

```php theme={null}
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：

```shell theme={null}
php artisan db:seed
```

## 在测试中使用

测试时通常将 `RefreshDatabase` trait 与工厂搭配使用。

```php theme={null}
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` 会在每个测试之间重置数据库，因此测试之间的数据不会相互干扰。

<Tip>
  使用 Pest 编写测试时工厂 API 完全一样。
  记得通过 `uses(RefreshDatabase::class)` 重置数据库。
</Tip>

## 下一步

<Card title="数据库填充" icon="seedling" href="/zh/seeding">
  查看如何组合 Seeder 与工厂来注入初始数据。
</Card>

<Card title="测试入门" icon="flask" href="/zh/testing">
  了解 Laravel 的测试功能以及 RefreshDatabase trait 的使用方式。
</Card>


## Related topics

- [数据库测试](/zh/database-testing.md)
- [数据库填充（Seeding）](/zh/seeding.md)
- [HTTP 测试](/zh/http-tests.md)
- [GeneratorCommand — 实现自定义 make: 命令](/zh/advanced/generator-command.md)
- [目录结构](/zh/directory-structure.md)
