
# Eloquent：数据工厂

- [介绍](#introduction)
- [定义模型工厂](#defining-model-factories)
    - [生成工厂](#generating-factories)
    - [工厂状态](#factory-states)
    - [工厂回调](#factory-callbacks)
- [使用工厂创建模型](#creating-models-using-factories)
    - [实例化模型](#instantiating-models)
    - [持久化模型](#persisting-models)
    - [序列](#sequences)
- [工厂关联](#factory-relationships)
    - [一对多关联](#has-many-relationships)
    - [Belongs To 关联](#belongs-to-relationships)
    - [多对多关联](#many-to-many-relationships)
    - [多态关联](#polymorphic-relationships)
    - [在工厂中定义关联](#defining-relationships-within-factories)
    - [为关联复用已有模型](#recycling-an-existing-model-for-relationships)

<a name="introduction"></a>
## 介绍

测试应用或填充数据库时，你可能需要向数据库插入一些记录。Laravel 允许你使用模型工厂，为每个 [Eloquent 模型](/docs/laravel/13.x/eloquent)定义一组默认属性，而无需手动指定每一列的值。

要了解工厂的写法，可以查看应用中的 `database/factories/UserFactory.php` 文件。所有新建的 Laravel 应用都包含这个工厂，其定义如下：

```php
namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
 */
class UserFactory extends Factory
{
    /**
     * 工厂当前使用的密码。
     */
    protected static ?string $password;

    /**
     * 定义模型的默认状态。
     *
     * @return array<string, mixed>
     */
    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),
        ];
    }

    /**
     * 指定模型的邮箱地址应处于未验证状态。
     */
    public function unverified(): static
    {
        return $this->state(fn (array $attributes) => [
            'email_verified_at' => null,
        ]);
    }
}
```

如你所见，工厂最基本的形式就是继承 Laravel 基础工厂类、并定义 `definition` 方法的类。`definition` 方法会返回一组默认属性值，在使用该工厂创建模型时应用这些值。

通过 `fake` 辅助函数，工厂可以使用 [Faker](https://github.com/FakerPHP/Faker) PHP 库，让你能够方便地生成各种用于测试和数据库填充的随机数据。

> [!NOTE]
> 你可以通过更新 `config/app.php` 配置文件中的 `faker_locale` 选项，修改应用的 Faker 语言环境。

<a name="defining-model-factories"></a>
## 定义模型工厂

<a name="generating-factories"></a>
### 生成工厂

要创建工厂，可以执行 `make:factory` [Artisan 命令](/docs/laravel/13.x/artisan)：

```shell
php artisan make:factory PostFactory
```

新的工厂类会放在 `database/factories` 目录中。

<a name="factory-and-model-discovery-conventions"></a>
#### 模型与工厂的发现约定

定义好工厂后，你就可以使用 `Illuminate\Database\Eloquent\Factories\HasFactory` trait 为模型提供的静态 `factory` 方法，实例化该模型对应的工厂。

`HasFactory` trait 的 `factory` 方法会遵循约定，为使用该 trait 的模型查找对应的工厂。具体来说，该方法会在 `Database\Factories` 命名空间中查找类名与模型名匹配、且以 `Factory` 为后缀的工厂。如果这些约定不适用于你的应用或工厂，你可以在模型上添加 `UseFactory` 特性，手动指定模型的工厂：

```php
use Illuminate\Database\Eloquent\Attributes\UseFactory;
use Database\Factories\Administration\FlightFactory;

#[UseFactory(FlightFactory::class)]
class Flight extends Model
{
    // ...
}
```

你也可以重写模型上的 `newFactory` 方法，直接返回模型对应的工厂实例：

```php
use Database\Factories\Administration\FlightFactory;

/**
 * 为模型创建新的工厂实例。
 */
protected static function newFactory()
{
    return FlightFactory::new();
}
```

然后，在对应的工厂上使用 `UseModel` 特性指定模型：

```php
use App\Administration\Flight;
use Illuminate\Database\Eloquent\Factories\Attributes\UseModel;
use Illuminate\Database\Eloquent\Factories\Factory;

#[UseModel(Flight::class)]
class FlightFactory extends Factory
{
    // ...
}
```

<a name="factory-states"></a>
### 工厂状态

状态操作方法允许你定义独立的修改，并以任意组合应用到模型工厂上。例如，`Database\Factories\UserFactory` 工厂可以包含一个 `suspended` 状态方法，用于修改某个默认属性值。

状态转换方法通常会调用 Laravel 基础工厂类提供的 `state` 方法。`state` 方法接受一个闭包，该闭包会接收工厂定义的原始属性数组，并应返回需要修改的属性数组：

```php
use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * 表示用户已被停用。
 */
public function suspended(): Factory
{
    return $this->state(function (array $attributes) {
        return [
            'account_status' => 'suspended',
        ];
    });
}
```

<a name="trashed-state"></a>
#### 「Trashed」状态

如果 Eloquent 模型支持[软删除](/docs/laravel/13.x/eloquent#soft-deleting)，你可以调用内置的 `trashed` 状态方法，表示创建出的模型应当已经处于「软删除」状态。你无需手动定义 `trashed` 状态，因为所有工厂都自动提供了该状态：

```php
use App\Models\User;

$user = User::factory()->trashed()->create();
```

<a name="factory-callbacks"></a>
### 工厂回调

工厂回调通过 `afterMaking` 和 `afterCreating` 方法注册，让你能够在实例化或创建模型后执行额外的任务。你应该在工厂类上定义 `configure` 方法来注册这些回调。实例化工厂时，Laravel 会自动调用该方法：

```php
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) {
            // ...
        })->afterCreating(function (User $user) {
            // ...
        });
    }

    // ...
}
```

你也可以在状态方法中注册工厂回调，以执行特定状态所需的额外任务：

```php
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * 表示用户已被停用。
 */
public function suspended(): Factory
{
    return $this->state(function (array $attributes) {
        return [
            'account_status' => 'suspended',
        ];
    })->afterMaking(function (User $user) {
        // ...
    })->afterCreating(function (User $user) {
        // ...
    });
}
```

<a name="creating-models-using-factories"></a>
## 使用工厂创建模型

<a name="instantiating-models"></a>
### 实例化模型

定义好工厂后，你就可以使用 `Illuminate\Database\Eloquent\Factories\HasFactory` trait 为模型提供的静态 `factory` 方法，实例化该模型对应的工厂。下面来看几个创建模型的示例。首先，我们会使用 `make` 方法创建模型，但不将其持久化到数据库：

```php
use App\Models\User;

$user = User::factory()->make();
```

你可以使用 `count` 方法创建包含多个模型的集合：

```php
$users = User::factory()->count(3)->make();
```

<a name="applying-states"></a>
#### 应用状态

你也可以将任意[状态](#factory-states)应用到模型上。如果希望对模型应用多个状态转换，直接调用相应的状态转换方法即可：

```php
$users = User::factory()->count(5)->suspended()->make();
```

<a name="overriding-attributes"></a>
#### 覆盖属性

如果希望覆盖模型的某些默认值，你可以向 `make` 方法传入一个值数组。只有指定的属性会被替换，其余属性仍会使用工厂中定义的默认值：

```php
$user = User::factory()->make([
    'name' => 'Abigail Otwell',
]);
```

你也可以直接在工厂实例上调用 `state` 方法，执行内联状态转换：

```php
$user = User::factory()->state([
    'name' => 'Abigail Otwell',
])->make();
```

> [!NOTE]
> 使用工厂创建模型时，会自动禁用[批量赋值保护](/docs/laravel/13.x/eloquent#mass-assignment)。

<a name="persisting-models"></a>
### 持久化模型

`create` 方法会实例化模型，并使用 Eloquent 的 `save` 方法将其持久化到数据库：

```php
use App\Models\User;

// 创建一个 App\Models\User 实例……
$user = User::factory()->create();

// 创建三个 App\Models\User 实例……
$users = User::factory()->count(3)->create();
```

你可以向 `create` 方法传入属性数组，覆盖工厂的默认模型属性：

```php
$user = User::factory()->create([
    'name' => 'Abigail',
]);
```

<a name="sequences"></a>
### 序列

有时，你可能希望每次创建模型时，交替使用某个属性的不同值。你可以将状态转换定义为序列来实现这一点。例如，你可能希望为每个新建用户交替设置 `admin` 列的值为 `Y` 和 `N`：

```php
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Sequence;

$users = User::factory()
    ->count(10)
    ->state(new Sequence(
        ['admin' => 'Y'],
        ['admin' => 'N'],
    ))
    ->create();
```

在本例中，会创建五个 `admin` 值为 `Y` 的用户，以及五个 `admin` 值为 `N` 的用户。

如果需要，你可以使用闭包作为序列值。每当序列需要新值时，都会调用该闭包：

```php
use Illuminate\Database\Eloquent\Factories\Sequence;

$users = User::factory()
    ->count(10)
    ->state(new Sequence(
        fn (Sequence $sequence) => ['role' => UserRoles::all()->random()],
    ))
    ->create();
```

在序列闭包中，你可以访问注入到闭包的序列实例上的 `$index` 属性。`$index` 属性记录了序列到目前为止已执行的迭代次数：

```php
$users = User::factory()
    ->count(10)
    ->state(new Sequence(
        fn (Sequence $sequence) => ['name' => 'Name '.$sequence->index],
    ))
    ->create();
```

为方便起见，你也可以使用 `sequence` 方法应用序列，该方法在内部调用 `state` 方法。`sequence` 方法接受闭包或一组按顺序使用的属性数组：

```php
$users = User::factory()
    ->count(2)
    ->sequence(
        ['name' => 'First User'],
        ['name' => 'Second User'],
    )
    ->create();
```

<a name="factory-relationships"></a>
## 工厂关联

<a name="has-many-relationships"></a>
### 一对多关联

接下来，我们看看如何使用 Laravel 的流式工厂方法构建 Eloquent 模型关联。首先，假设应用中有 `App\Models\User` 和 `App\Models\Post` 模型，且 `User` 模型定义了与 `Post` 模型的 `hasMany` 关联。我们可以使用 Laravel 工厂提供的 `has` 方法，创建一个拥有三篇文章的用户。`has` 方法接受一个工厂实例：

```php
use App\Models\Post;
use App\Models\User;

$user = User::factory()
    ->has(Post::factory()->count(3))
    ->create();
```

按照约定，将 `Post` 模型传给 `has` 方法时，Laravel 会假设 `User` 模型中有一个用于定义关联的 `posts` 方法。如果需要，你可以显式指定要操作的关联名称：

```php
$user = User::factory()
    ->has(Post::factory()->count(3), 'posts')
    ->create();
```

当然，你也可以对关联模型执行状态操作。如果状态变更需要访问父模型，你还可以传入一个基于闭包的状态转换：

```php
$user = User::factory()
    ->has(
        Post::factory()
            ->count(3)
            ->state(function (array $attributes, User $user) {
                return ['user_type' => $user->type];
            })
    )
    ->create();
```

<a name="has-many-relationships-using-magic-methods"></a>
#### 使用魔术方法

为方便起见，你可以使用 Laravel 的工厂关联魔术方法构建关联。例如，下面的示例会根据约定，确定应通过 `User` 模型上的 `posts` 关联方法来创建关联模型：

```php
$user = User::factory()
    ->hasPosts(3)
    ->create();
```

使用魔术方法创建工厂关联时，你可以传入属性数组，覆盖关联模型上的属性：

```php
$user = User::factory()
    ->hasPosts(3, [
        'published' => false,
    ])
    ->create();
```

你也可以传入多个属性数组，为每个关联模型指定各自的状态。Laravel 会按顺序应用这些数组：

```php
$user = User::factory()
    ->hasPosts(
        ['title' => 'First Post'],
        ['title' => 'Second Post'],
        ['title' => 'Third Post'],
    )
    ->create();
```

如果状态变更需要访问父模型，你可以提供一个基于闭包的状态转换：

```php
$user = User::factory()
    ->hasPosts(3, function (array $attributes, User $user) {
        return ['user_type' => $user->type];
    })
    ->create();
```

<a name="belongs-to-relationships"></a>
### Belongs To 关联

了解了如何使用工厂构建「一对多」关联后，我们再来看这种关联的反向用法。`for` 方法可用于定义工厂创建的模型所归属的父模型。例如，我们可以创建三个属于同一个用户的 `App\Models\Post` 模型实例：

```php
use App\Models\Post;
use App\Models\User;

$posts = Post::factory()
    ->count(3)
    ->for(User::factory()->state([
        'name' => 'Jessica Archer',
    ]))
    ->create();
```

如果已经有父模型实例，并且希望将它与正在创建的模型关联起来，你可以将该模型实例传给 `for` 方法：

```php
$user = User::factory()->create();

$posts = Post::factory()
    ->count(3)
    ->for($user)
    ->create();
```

<a name="belongs-to-relationships-using-magic-methods"></a>
#### 使用魔术方法

为方便起见，你可以使用 Laravel 的工厂关联魔术方法定义「belongs to」关联。例如，下面的示例会根据约定，确定这三篇文章应通过 `Post` 模型上的 `user` 关联归属于用户：

```php
$posts = Post::factory()
    ->count(3)
    ->forUser([
        'name' => 'Jessica Archer',
    ])
    ->create();
```

<a name="many-to-many-relationships"></a>
### 多对多关联

与[一对多关联](#has-many-relationships)一样，你可以使用 `has` 方法创建「多对多」关联：

```php
use App\Models\Role;
use App\Models\User;

$user = User::factory()
    ->has(Role::factory()->count(3))
    ->create();
```

<a name="pivot-table-attributes"></a>
#### 中间表属性

如果需要为连接模型的中间表设置属性，你可以使用 `hasAttached` 方法。该方法的第二个参数接受一个包含中间表属性名和值的数组：

```php
use App\Models\Role;
use App\Models\User;

$user = User::factory()
    ->hasAttached(
        Role::factory()->count(3),
        ['active' => true]
    )
    ->create();
```

如果状态变更需要访问关联模型，你可以提供一个基于闭包的状态转换：

```php
$user = User::factory()
    ->hasAttached(
        Role::factory()
            ->count(3)
            ->state(function (array $attributes, User $user) {
                return ['name' => $user->name.' Role'];
            }),
        ['active' => true]
    )
    ->create();
```

你也可以传入由多个中间表属性数组组成的数组，为每个关联模型提供独立的中间表数据：

```php
$user = User::factory()
    ->hasAttached(
        Role::factory(),
        [
            ['active' => true],
            ['active' => false],
        ]
    )
    ->create();
```

如果已经有模型实例，并且希望将它们附加到正在创建的模型上，你可以将这些实例传给 `hasAttached` 方法。在本例中，相同的三个角色会附加到全部三个用户上：

```php
$roles = Role::factory()->count(3)->create();

$users = User::factory()
    ->count(3)
    ->hasAttached($roles, ['active' => true])
    ->create();
```

<a name="many-to-many-relationships-using-magic-methods"></a>
#### 使用魔术方法

为方便起见，你可以使用 Laravel 的工厂关联魔术方法定义多对多关联。例如，下面的示例会根据约定，确定应通过 `User` 模型上的 `roles` 关联方法来创建关联模型：

```php
$user = User::factory()
    ->hasRoles(1, [
        'name' => 'Editor'
    ])
    ->create();
```

<a name="polymorphic-relationships"></a>
### 多态关联

[多态关联](/docs/laravel/13.x/eloquent-relationships#polymorphic-relationships)也可以使用工厂创建。多态「morph many」关联的创建方式与普通的「has many」关联相同。例如，假设 `App\Models\Post` 模型定义了与 `App\Models\Comment` 模型的 `morphMany` 关联：

```php
use App\Models\Post;

$post = Post::factory()->hasComments(3)->create();
```

<a name="morph-to-relationships"></a>
#### Morph To 关联

你不能使用魔术方法创建 `morphTo` 关联，而必须直接使用 `for` 方法，并显式指定关联名称。例如，假设 `Comment` 模型有一个 `commentable` 方法，定义了 `morphTo` 关联。这时，我们可以直接使用 `for` 方法，创建三条属于同一篇文章的评论：

```php
$comments = Comment::factory()->count(3)->for(
    Post::factory(), 'commentable'
)->create();
```

<a name="polymorphic-many-to-many-relationships"></a>
#### 多态多对多关联

多态「多对多」（`morphToMany` / `morphedByMany`）关联的创建方式与非多态「多对多」关联相同：

```php
use App\Models\Tag;
use App\Models\Video;

$video = Video::factory()
    ->hasAttached(
        Tag::factory()->count(3),
        ['public' => true]
    )
    ->create();
```

当然，你也可以使用魔术 `has` 方法创建多态「多对多」关联：

```php
$video = Video::factory()
    ->hasTags(3, ['public' => true])
    ->create();
```

<a name="defining-relationships-within-factories"></a>
### 在工厂中定义关联

要在模型工厂中定义关联，你通常会将新的工厂实例赋给关联的外键。这种写法通常用于 `belongsTo` 和 `morphTo` 等「反向」关联。例如，如果希望在创建文章时创建一个新用户，你可以这样做：

```php
use App\Models\User;

/**
 * 定义模型的默认状态。
 *
 * @return array<string, mixed>
 */
public function definition(): array
{
    return [
        'user_id' => User::factory(),
        'title' => fake()->title(),
        'content' => fake()->paragraph(),
    ];
}
```

如果关联列的值依赖于定义该关联的工厂，你可以为属性指定闭包。该闭包会接收工厂已计算出的属性数组：

```php
/**
 * 定义模型的默认状态。
 *
 * @return array<string, mixed>
 */
public function definition(): array
{
    return [
        'user_id' => User::factory(),
        'user_type' => function (array $attributes) {
            return User::find($attributes['user_id'])->type;
        },
        'title' => fake()->title(),
        'content' => fake()->paragraph(),
    ];
}
```

<a name="recycling-an-existing-model-for-relationships"></a>
### 为关联复用已有模型

如果多个模型与另一个模型有共同的关联，你可以使用 `recycle` 方法，确保工厂创建的所有这些关联都复用该关联模型的同一个实例。

例如，假设有 `Airline`、`Flight` 和 `Ticket` 模型，其中机票属于一家航空公司和一个航班，而航班也属于一家航空公司。创建机票时，你通常希望机票和航班属于同一家航空公司，因此可以将航空公司实例传给 `recycle` 方法：

```php
Ticket::factory()
    ->recycle(Airline::factory()->create())
    ->create();
```

当多个模型属于同一个用户或团队时，`recycle` 方法会特别有用。

`recycle` 方法也接受已有模型的集合。将集合传给 `recycle` 方法后，当工厂需要该类型的模型时，会从集合中随机选择一个模型：

```php
Ticket::factory()
    ->recycle($airlines)
    ->create();
```
