跳转至内容

Eloquent: 数据工厂

简介

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

要查看如何编写工厂的示例,请查看应用程序中的 database/factories/UserFactory.php 文件。此工厂包含在所有新的 Laravel 应用程序中,并包含以下工厂定义:

1namespace Database\Factories;
2 
3use Illuminate\Database\Eloquent\Factories\Factory;
4use Illuminate\Support\Facades\Hash;
5use Illuminate\Support\Str;
6 
7/**
8 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
9 */
10class UserFactory extends Factory
11{
12 /**
13 * The current password being used by the factory.
14 */
15 protected static ?string $password;
16 
17 /**
18 * Define the model's default state.
19 *
20 * @return array<string, mixed>
21 */
22 public function definition(): array
23 {
24 return [
25 'name' => fake()->name(),
26 'email' => fake()->unique()->safeEmail(),
27 'email_verified_at' => now(),
28 'password' => static::$password ??= Hash::make('password'),
29 'remember_token' => Str::random(10),
30 ];
31 }
32 
33 /**
34 * Indicate that the model's email address should be unverified.
35 */
36 public function unverified(): static
37 {
38 return $this->state(fn (array $attributes) => [
39 'email_verified_at' => null,
40 ]);
41 }
42}

如您所见,工厂最基本的形式是继承 Laravel 基础工厂类并定义一个 definition 方法的类。definition 方法返回使用该工厂创建模型时应应用的默认属性值集。

通过 fake 辅助函数,工厂可以访问 Faker PHP 库,该库允许您方便地生成各种随机数据,用于测试和数据库填充。

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

定义模型工厂

生成工厂

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

1php artisan make:factory PostFactory

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

模型和工厂发现约定

定义工厂后,您可以使用 Illuminate\Database\Eloquent\Factories\HasFactory trait 为模型提供的静态 factory 方法来实例化该模型的工厂实例。

HasFactory trait 的 factory 方法将使用约定来确定分配了该 trait 的模型对应的工厂。具体来说,该方法将在 Database\Factories 命名空间中查找类名与模型名匹配并以 Factory 为后缀的工厂。如果这些约定不适用于您的特定应用程序或工厂,您可以在模型上添加 UseFactory 属性来手动指定模型的工厂:

1use Illuminate\Database\Eloquent\Attributes\UseFactory;
2use Database\Factories\Administration\FlightFactory;
3 
4#[UseFactory(FlightFactory::class)]
5class Flight extends Model
6{
7 // ...
8}

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

1use Database\Factories\Administration\FlightFactory;
2 
3/**
4 * Create a new factory instance for the model.
5 */
6protected static function newFactory()
7{
8 return FlightFactory::new();
9}

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

1use App\Administration\Flight;
2use Illuminate\Database\Eloquent\Factories\Attributes\UseModel;
3use Illuminate\Database\Eloquent\Factories\Factory;
4 
5#[UseModel(Flight::class)]
6class FlightFactory extends Factory
7{
8 // ...
9}

工厂状态

状态操作方法允许您定义离散的修改,这些修改可以以任意组合应用于您的模型工厂。例如,您的 Database\Factories\UserFactory 工厂可能包含一个 suspended 状态方法,用于修改其默认属性值之一。

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

1use Illuminate\Database\Eloquent\Factories\Factory;
2 
3/**
4 * Indicate that the user is suspended.
5 */
6public function suspended(): Factory
7{
8 return $this->state(function (array $attributes) {
9 return [
10 'account_status' => 'suspended',
11 ];
12 });
13}

“软删除”状态

如果您的 Eloquent 模型可以被 软删除,您可以调用内置的 trashed 状态方法来指示创建的模型应该已经被“软删除”。您无需手动定义 trashed 状态,因为它会自动对所有工厂可用:

1use App\Models\User;
2 
3$user = User::factory()->trashed()->create();

工厂回调

工厂回调是使用 afterMakingafterCreating 方法注册的,允许您在制造或创建模型后执行额外的任务。您应该通过在工厂类上定义 configure 方法来注册这些回调。当工厂被实例化时,Laravel 会自动调用此方法:

1namespace Database\Factories;
2 
3use App\Models\User;
4use Illuminate\Database\Eloquent\Factories\Factory;
5 
6class UserFactory extends Factory
7{
8 /**
9 * Configure the model factory.
10 */
11 public function configure(): static
12 {
13 return $this->afterMaking(function (User $user) {
14 // ...
15 })->afterCreating(function (User $user) {
16 // ...
17 });
18 }
19 
20 // ...
21}

您还可以在状态方法内注册工厂回调,以执行特定于给定状态的额外任务:

1use App\Models\User;
2use Illuminate\Database\Eloquent\Factories\Factory;
3 
4/**
5 * Indicate that the user is suspended.
6 */
7public function suspended(): Factory
8{
9 return $this->state(function (array $attributes) {
10 return [
11 'account_status' => 'suspended',
12 ];
13 })->afterMaking(function (User $user) {
14 // ...
15 })->afterCreating(function (User $user) {
16 // ...
17 });
18}

使用工厂创建模型

实例化模型

定义工厂后,您可以使用 Illuminate\Database\Eloquent\Factories\HasFactory trait 为模型提供的静态 factory 方法来实例化该模型的工厂实例。让我们看一些创建模型的例子。首先,我们将使用 make 方法创建模型,而不将它们持久化到数据库:

1use App\Models\User;
2 
3$user = User::factory()->make();

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

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

应用状态

您还可以将任何 状态 应用于模型。如果您想对模型应用多个状态转换,只需直接调用状态转换方法即可:

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

覆盖属性

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

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

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

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

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

持久化模型

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

1use App\Models\User;
2 
3// Create a single App\Models\User instance...
4$user = User::factory()->create();
5 
6// Create three App\Models\User instances...
7$users = User::factory()->count(3)->create();

您可以通过将属性数组传递给 create 方法来覆盖工厂的默认模型属性:

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

序列

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

1use App\Models\User;
2use Illuminate\Database\Eloquent\Factories\Sequence;
3 
4$users = User::factory()
5 ->count(10)
6 ->state(new Sequence(
7 ['admin' => 'Y'],
8 ['admin' => 'N'],
9 ))
10 ->create();

在此示例中,将创建 5 个 admin 值为 Y 的用户,以及 5 个 admin 值为 N 的用户。

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

1use Illuminate\Database\Eloquent\Factories\Sequence;
2 
3$users = User::factory()
4 ->count(10)
5 ->state(new Sequence(
6 fn (Sequence $sequence) => ['role' => UserRoles::all()->random()],
7 ))
8 ->create();

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

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

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

1$users = User::factory()
2 ->count(2)
3 ->sequence(
4 ['name' => 'First User'],
5 ['name' => 'Second User'],
6 )
7 ->create();

工厂关联

一对多关联

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

1use App\Models\Post;
2use App\Models\User;
3 
4$user = User::factory()
5 ->has(Post::factory()->count(3))
6 ->create();

按照惯例,当将 Post 模型传递给 has 方法时,Laravel 会假设 User 模型必须有一个定义该关联的 posts 方法。如有必要,您可以明确指定要操作的关联名称:

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

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

1$user = User::factory()
2 ->has(
3 Post::factory()
4 ->count(3)
5 ->state(function (array $attributes, User $user) {
6 return ['user_type' => $user->type];
7 })
8 )
9 ->create();

使用魔术方法

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

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

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

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

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

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

从属关联 (Belongs To)

现在我们已经探索了如何使用工厂构建“一对多”关联,让我们探索该关联的逆向。for 方法可用于定义工厂创建的模型所属的父模型。例如,我们可以创建三个属于同一个用户的 App\Models\Post 模型实例:

1use App\Models\Post;
2use App\Models\User;
3 
4$posts = Post::factory()
5 ->count(3)
6 ->for(User::factory()->state([
7 'name' => 'Jessica Archer',
8 ]))
9 ->create();

如果您已经有一个应该与您正在创建的模型关联的父模型实例,您可以将该模型实例传递给 for 方法:

1$user = User::factory()->create();
2 
3$posts = Post::factory()
4 ->count(3)
5 ->for($user)
6 ->create();

使用魔术方法

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

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

多对多关联

一对多关联 一样,“多对多”关联可以使用 has 方法创建:

1use App\Models\Role;
2use App\Models\User;
3 
4$user = User::factory()
5 ->has(Role::factory()->count(3))
6 ->create();

中间表属性

如果您需要定义应该在链接模型的中间表(Pivot Table)上设置的属性,您可以使用 hasAttached 方法。此方法接受一个中间表属性名称和值的数组作为其第二个参数:

1use App\Models\Role;
2use App\Models\User;
3 
4$user = User::factory()
5 ->hasAttached(
6 Role::factory()->count(3),
7 ['active' => true]
8 )
9 ->create();

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

1$user = User::factory()
2 ->hasAttached(
3 Role::factory()
4 ->count(3)
5 ->state(function (array $attributes, User $user) {
6 return ['name' => $user->name.' Role'];
7 }),
8 ['active' => true]
9 )
10 ->create();

如果您已经有想要附加到您正在创建的模型的模型实例,您可以将这些模型实例传递给 hasAttached 方法。在此示例中,相同的三个角色将附加到所有三个用户:

1$roles = Role::factory()->count(3)->create();
2 
3$users = User::factory()
4 ->count(3)
5 ->hasAttached($roles, ['active' => true])
6 ->create();

使用魔术方法

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

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

多态关联

多态关联 也可以使用工厂创建。多态“一对多 (morphMany)”关联的创建方式与典型“一对多 (hasMany)”关联相同。例如,如果 App\Models\Post 模型与 App\Models\Comment 模型有 morphMany 关联:

1use App\Models\Post;
2 
3$post = Post::factory()->hasComments(3)->create();

多态从属关联 (Morph To)

魔术方法不能用于创建 morphTo 关联。相反,必须直接使用 for 方法,并明确提供关联名称。例如,假设 Comment 模型有一个定义 morphTo 关联的 commentable 方法。在这种情况下,我们可以通过直接使用 for 方法创建三个属于同一篇文章的评论:

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

多态多对多关联

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

1use App\Models\Tag;
2use App\Models\Video;
3 
4$video = Video::factory()
5 ->hasAttached(
6 Tag::factory()->count(3),
7 ['public' => true]
8 )
9 ->create();

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

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

在工厂内定义关联

要在您的模型工厂内定义关联,您通常会将一个新的工厂实例分配给该关联的外键。这通常针对诸如 belongsTomorphTo 关联之类的“逆向”关联完成。例如,如果您想在创建文章时创建一个新用户,您可以执行以下操作:

1use App\Models\User;
2 
3/**
4 * Define the model's default state.
5 *
6 * @return array<string, mixed>
7 */
8public function definition(): array
9{
10 return [
11 'user_id' => User::factory(),
12 'title' => fake()->title(),
13 'content' => fake()->paragraph(),
14 ];
15}

如果关联的列依赖于定义它的工厂,您可以为属性分配一个闭包。该闭包将接收工厂评估后的属性数组:

1/**
2 * Define the model's default state.
3 *
4 * @return array<string, mixed>
5 */
6public function definition(): array
7{
8 return [
9 'user_id' => User::factory(),
10 'user_type' => function (array $attributes) {
11 return User::find($attributes['user_id'])->type;
12 },
13 'title' => fake()->title(),
14 'content' => fake()->paragraph(),
15 ];
16}

在关联中复用现有模型

如果您有与其他模型共享公共关联的模型,您可以使用 recycle 方法来确保关联模型的单个实例被工厂创建的所有关联所复用。

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

1Ticket::factory()
2 ->recycle(Airline::factory()->create())
3 ->create();

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

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

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