简介

在测试应用程序或为数据库填充初始数据时,你可能需要向数据库中插入一些记录。Laravel 允许你使用模型工厂(model factories)为每个 Eloquent 模型定义一组默认属性,而无需手动指定每一列的值。

若要查看工厂的编写示例,请查看应用程序中的 database/factories/UserFactory.php 文件。所有新创建的 Laravel 应用程序都自带此工厂,其中包含以下工厂定义:

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 PHP 库,方便你为测试和数据填充生成各种类型的随机数据。

你可以更新 config/app.php 配置文件中的 faker_locale 选项,来更改应用程序的 Faker 语言环境。

定义模型工厂

生成工厂

若要创建工厂,请执行 make:factory Artisan 命令:

php artisan make:factory PostFactory

新的工厂类将被放置在 database/factories 目录中。

模型与工厂的发现约定

一旦定义了工厂,你就可以使用 Illuminate\Database\Eloquent\Factories\HasFactory 特性为模型提供的静态 factory 方法,为该模型实例化一个工厂实例。

HasFactory 特性的 factory 方法会按照约定来确定分配该特性的模型所对应的工厂。具体而言,该方法会在 Database\Factories 命名空间中查找与模型名称匹配且以 Factory 为后缀的工厂类。如果这些约定不适用于你的特定应用程序或工厂,你可以在模型中添加 UseFactory 特性,手动指定模型的工厂:

use Illuminate\Database\Eloquent\Attributes\UseFactory;
use Database\Factories\Administration\FlightFactory;

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

或者,你也可以重写模型上的 newFactory 方法,直接返回模型对应工厂的实例:

use Database\Factories\Administration\FlightFactory;

/**
 * 为模型创建新的工厂实例。
 */
protected static function newFactory()
{
    return FlightFactory::new();
}

然后,在对应的工厂上使用 UseModel 特性来指定模型:

use App\Administration\Flight;
use Illuminate\Database\Eloquent\Factories\Attributes\UseModel;
use Illuminate\Database\Eloquent\Factories\Factory;

#[UseModel(Flight::class)]
class FlightFactory extends Factory
{
    // ...
}

工厂状态

状态操作方法允许你定义可按任意组合应用于模型工厂的独立修改。例如,你的 Database\Factories\UserFactory 工厂可能包含一个 suspended 状态方法,用于修改其某个默认属性值。

状态转换方法通常会调用 Laravel 基础工厂类提供的 state 方法。state 方法接收一个闭包,该闭包会收到为工厂定义的原始属性数组,并应返回要修改的属性数组:

use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * 指示用户已被暂停。
 */
public function suspended(): Factory
{
    return $this->state(function (array $attributes) {
        return [
            'account_status' => 'suspended',
        ];
    });
}

“Trashed”状态

如果你的 Eloquent 模型支持软删除,你可以调用内置的 trashed 状态方法,指示创建的模型应处于“软删除”状态。你无需手动定义 trashed 状态,因为它对所有工厂自动可用:

use App\Models\User;

$user = User::factory()->trashed()->create();

工厂回调

工厂回调通过 afterMaking 和 afterCreating 方法注册,允许你在生成或创建模型后执行额外任务。你应该通过在工厂类中定义 configure 方法来注册这些回调。当工厂被实例化时,Laravel 会自动调用此方法:

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) {
            // ...
        });
    }

    // ...
}

你也可以在状态方法中注册工厂回调,以执行与特定状态相关的额外任务:

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) {
        // ...
    });
}

使用工厂创建模型

实例化模型

一旦定义了工厂,你就可以使用 Illuminate\Database\Eloquent\Factories\HasFactory 特性为模型提供的静态 factory 方法,为该模型实例化一个工厂实例。让我们来看几个创建模型的示例。首先,我们将使用 make 方法创建模型,但不将它们持久化到数据库:

use App\Models\User;

$user = User::factory()->make();

你可以使用 count 方法创建多个模型的集合:

$users = User::factory()->count(3)->make();

应用状态

你也可以将任意状态应用到模型上。如果你想对模型应用多个状态转换,只需直接调用这些状态转换方法:

$users = User::factory()->count(5)->suspended()->make();

覆盖属性

如果你想覆盖模型的一些默认值,可以将一个值数组传递给 make 方法。只有指定的属性会被替换,其余属性将保留工厂设定的默认值:

$user = User::factory()->make([
    'name' => 'Abigail Otwell',
]);

或者,也可以直接在工厂实例上调用 state 方法,执行内联状态转换:

$user = User::factory()->state([
    'name' => 'Abigail Otwell',
])->make();

批量赋值保护在使用工厂创建模型时会自动禁用。

持久化模型

create 方法会实例化模型实例,并使用 Eloquent 的 save 方法将它们持久化到数据库:

use App\Models\User;

// 创建一个 App\Models\User 实例……
$user = User::factory()->create();

// 创建三个 App\Models\User 实例……
$users = User::factory()->count(3)->create();

您可以向 create 方法传入属性数组,覆盖工厂的默认模型属性:

$user = User::factory()->create([
    'name' => 'Abigail',
]);

序列(Sequences)

有时,您可能希望为每个创建的模型交替设置指定模型属性的值。您可以通过将状态转换定义为序列来实现这一点。例如,您可能希望为每个创建的用户将 admin 列的值在 Y 和 N 之间交替:

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 的用户。

如有必要,您可以将闭包作为序列值。序列每次需要新值时,都会调用该闭包:

use Illuminate\Database\Eloquent\Factories\Sequence;

$users = User::factory()
    ->count(10)
    ->state(new Sequence(
        fn (Sequence $sequence) => ['role' => UserRoles::all()->random()],
    ))
    ->create();

在序列闭包内,您可以访问注入到闭包中的序列实例上的 $index 属性。$index 属性包含到目前为止序列已迭代的次数:

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

为方便起见,也可以使用 sequence 方法应用序列,该方法只是在内部调用 state 方法。sequence 方法接受一个闭包或一组序列化属性数组:

$users = User::factory()
    ->count(2)
    ->sequence(
        ['name' => 'First User'],
        ['name' => 'Second User'],
    )
    ->create();

工厂关系(Factory Relationships)

一对多关系(Has Many Relationships)

接下来,让我们探索如何使用 Laravel 的流畅工厂方法构建 Eloquent 模型关系。首先,假设我们的应用程序有一个 App\Models\User 模型和一个 App\Models\Post 模型。同时,假设 User 模型定义了与 Post 的 hasMany 关系。我们可以使用 Laravel 工厂提供的 has 方法创建一个拥有三篇文章的用户。has 方法接受一个工厂实例:

use App\Models\Post;
use App\Models\User;

$user = User::factory()
    ->has(Post::factory()->count(3))
    ->create();

按照约定,当向 has 方法传入 Post 模型时,Laravel 会假定 User 模型必须有一个定义该关系的 posts 方法。如有必要,您可以明确指定要操作的关系名称:

$user = User::factory()
    ->has(Post::factory()->count(3), 'posts')
    ->create();

当然,您可以对关联模型执行状态操作。此外,如果您的状态更改需要访问父模型,还可以传入基于闭包的状态转换:

$user = User::factory()
    ->has(
        Post::factory()
            ->count(3)
            ->state(function (array $attributes, User $user) {
                return ['user_type' => $user->type];
            })
    )
    ->create();

使用魔术方法(Using Magic Methods)

为方便起见,您可以使用 Laravel 的魔术工厂关系方法来构建关系。例如,以下示例将通过约定来确定关联模型应通过 User 模型上的 posts 关系方法创建:

$user = User::factory()
    ->hasPosts(3)
    ->create();

使用魔术方法创建工厂关系时,您可以传入一个属性数组,以覆盖关联模型上的属性:

$user = User::factory()
    ->hasPosts(3, [
        'published' => false,
    ])
    ->create();

您还可以传入多个属性数组,为关联模型创建各自独立的模型状态。Laravel 会按顺序应用每个数组:

$user = User::factory()
    ->hasPosts(
        ['title' => 'First Post'],
        ['title' => 'Second Post'],
        ['title' => 'Third Post'],
    )
    ->create();

如果您的状态更改需要访问父模型,可以提供一个基于闭包的状态转换:

$user = User::factory()
    ->hasPosts(3, function (array $attributes, User $user) {
        return ['user_type' => $user->type];
    })
    ->create();

属于关系(Belongs To Relationships)

既然我们已经了解了如何使用工厂构建“一对多”关系,接下来探讨该关系的反向情况。for 方法可用于定义工厂创建的模型所属的父模型。例如,我们可以创建三个属于同一个用户的 App\Models\Post 模型实例:

use App\Models\Post;
use App\Models\User;

$posts = Post::factory()
    ->count(3)
    ->for(User::factory()->state([
        'name' => 'Jessica Archer',
    ]))
    ->create();

如果您已经有一个需要与所创建模型关联的父模型实例,可以将该模型实例传入 for 方法:

$user = User::factory()->create();

$posts = Post::factory()
    ->count(3)
    ->for($user)
    ->create();

使用魔术方法(Using Magic Methods)

为方便起见,您可以使用 Laravel 的魔术工厂关系方法来定义“属于”关系。例如,以下示例将通过约定来确定这三篇文章应属于 Post 模型上的 user 关系:

$posts = Post::factory()
    ->count(3)
    ->forUser([
        'name' => 'Jessica Archer',
    ])
    ->create();

多对多关系(Many to Many Relationships)

与一对多关系类似,“多对多”关系也可以通过 has 方法创建:

use App\Models\Role;
use App\Models\User;

$user = User::factory()
    ->has(Role::factory()->count(3))
    ->create();

中间表属性(Pivot Table Attributes)

如果您需要定义链接模型的中间表/枢纽表上应设置的属性,可以使用 hasAttached 方法。该方法接受一个包含中间表属性名称和值的数组作为第二个参数:

use App\Models\Role;
use App\Models\User;

$user = User::factory()
    ->hasAttached(
        Role::factory()->count(3),
        ['active' => true]
    )
    ->create();

如果您的状态更改需要访问关联模型,可以提供一个基于闭包的状态转换:

$user = User::factory()
    ->hasAttached(
        Role::factory()
            ->count(3)
            ->state(function (array $attributes, User $user) {
                return ['name' => $user->name.' Role'];
            }),
        ['active' => true]
    )
    ->create();

您也可以传入一个中间表属性数组,为每个关联模型提供唯一的中间表数据:

$user = User::factory()
    ->hasAttached(
        Role::factory(),
        [
            ['active' => true],
            ['active' => false],
        ]
    )
    ->create();

如果您已经有一些想要关联到所创建模型的模型实例,可以将这些模型实例传入 hasAttached 方法。在本例中,同样的三个角色将被关联到所有三个用户:

$roles = Role::factory()->count(3)->create();

$users = User::factory()
    ->count(3)
    ->hasAttached($roles, ['active' => true])
    ->create();

使用魔术方法(Using Magic Methods)

为方便起见,您可以使用 Laravel 的魔术工厂关系方法来定义多对多关系。例如,以下示例将通过约定来确定关联模型应通过 User 模型上的 roles 关系方法创建:

$user = User::factory()
    ->hasRoles(1, [
        'name' => 'Editor'
    ])
    ->create();

多态关系(Polymorphic Relationships)

多态关系

也可以使用工厂来创建。多态的“morph many”关系的创建方式与典型的“has many”关系相同。例如,如果 App\Models\Post 模型与 App\Models\Comment 模型存在 morphMany 关系:
use App\Models\Post;

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

Morph To 关系

不能使用魔术方法来创建 morphTo 关系。相反,必须直接使用 for 方法,并且必须显式提供关系的名称。例如,假设 Comment 模型有一个 commentable 方法,该方法定义了一个 morphTo 关系。在这种情况下,我们可以直接使用 for 方法来创建三条属于同一帖子的评论:

$comments = Comment::factory()->count(3)->for(
    Post::factory(), 'commentable'
)->create();

多态多对多关系

多态的“多对多”(morphToMany / morphedByMany)关系的创建方式与非多态的“多对多”关系相同:

use App\Models\Tag;
use App\Models\Video;

$video = Video::factory()
    ->hasAttached(
        Tag::factory()->count(3),
        ['public' => true]
    )
    ->create();

当然,魔术 has 方法也可用于创建多态的“多对多”关系:

$video = Video::factory()
    ->hasTags(3, ['public' => true])
    ->create();

在工厂中定义关系

要在模型工厂中定义关系,通常需要将一个新的工厂实例分配给该关系的外键。这通常用于像 belongsTo 和 morphTo 关系这样的“反向”关系。例如,如果在创建帖子时想要同时创建一个新用户,可以这样做:

use App\Models\User;

/**
 * 定义模型的默认状态。
 *
 * @return array<string, mixed>
 */
public function definition(): array
{
    return [
        'user_id' => User::factory(),
        'title' => fake()->title(),
        'content' => fake()->paragraph(),
    ];
}

如果关系的列取决于定义该关系的工厂,则可以为属性分配一个闭包。该闭包将接收工厂已求值的属性数组:

/**
 * 定义模型的默认状态。
 *
 * @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(),
    ];
}

为关系复用现有模型

如果有共享同一关系的模型,可以使用 recycle 方法,以确保工厂创建的所有关系都复用同一个相关模型实例。

例如,假设有 Airline、Flight 和 Ticket 模型,其中票据属于航空公司和航班,航班也属于航空公司。在创建票据时,可能希望票据和航班都使用同一家航空公司,因此可以将一个航空公司实例传递给 recycle 方法:

Ticket::factory()
    ->recycle(Airline::factory()->create())
    ->create();

如果模型属于同一个用户或团队,可能会发现 recycle 方法特别有用。

recycle 方法也接受现有模型的集合。当向 recycle 方法提供集合时,当工厂需要该类型的模型时,会从集合中随机选择一个模型:

Ticket::factory()
    ->recycle($airlines)
    ->create();