Skip to content

Eloquent 工厂

介绍

在测试应用程序或为数据库填充时,可能需要在数据库中插入一些记录。Laravel 允许您使用模型工厂为每个 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;

        /**
         * Define the model's default state.
         *
         * @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 库,该库允许您方便地生成各种随机数据以进行测试和填充。

NOTE

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

定义模型工厂

生成工厂

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

shell
php artisan make:factory PostFactory

新的 factory 类将放置在 database/factories 目录中。

模型和 Factory 发现约定

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

HasFactory trait 的 factory 方法将使用约定来确定 trait 分配到的模型的合适工厂。具体而言,该方法将在 Database\Factories 命名空间中查找工厂,该工厂的类名与模型名称匹配,并以 Factory 为后缀。如果这些约定不适用于你的特定应用程序或工厂,你可以覆盖模型上的 newFactory 方法,直接返回模型相应工厂的实例:

php
    use Illuminate\Database\Eloquent\Factories\Factory;
    use Database\Factories\Administration\FlightFactory;

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

然后,在相应的工厂上定义一个“model”属性:

php
    use App\Administration\Flight;
    use Illuminate\Database\Eloquent\Factories\Factory;

    class FlightFactory extends Factory
    {
        /**
         * 工厂的相应型号的名称
         *
         * @var class-string<\Illuminate\Database\Eloquent\Model>
         */
        protected $model = Flight::class;
    }

工厂状态

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

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

php
    use Illuminate\Database\Eloquent\Factories\Factory;

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

“已删除”状态

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

php
    use App\Models\User;

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

工厂回调

Factory 回调是使用 afterMakingafterCreating 方法注册的,并允许您在创建或创建模型后执行其他任务。您应该通过在工厂类上定义 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) {
                // ...
            });
        }

        // ...
    }

您还可以在 state 方法中注册工厂回调,以执行特定于给定 state 的其他任务:

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

使用 Factory 创建模型

实例化模型

定义工厂后,你可以使用 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();

应用状态

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

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

覆盖属性

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

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

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

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

NOTE

使用 Factory 创建模型时,会自动禁用批量分配保护

持久化模型

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

php
    use App\Models\User;

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

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

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

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

序列

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

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

如有必要,您可以包含一个 closure 作为 sequence 值。每次 sequence 需要新值时,都会调用 Closure:

php
    use Illuminate\Database\Eloquent\Factories\Sequence;

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

在序列闭包中,您可以访问注入到闭包中的序列实例上的 $index$count 属性。$index 属性包含到目前为止通过序列发生的迭代次数,而 $count 属性包含将调用序列的总次数:

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

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

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

工厂关系

具有许多关系

接下来,让我们探索如何使用 Laravel 的 Fluent Factory 方法构建 Eloquent 模型关系。首先,假设我们的应用程序有一个 App\Models\User 模型和一个 App\Models\Post 模型。此外,我们假设 User 模型定义了与 PosthasMany 关系。我们可以使用 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();

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

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

使用 Magic 方法

为方便起见,您可以使用 Laravel 的 magic factory 关系方法来建立关系。例如,以下示例将使用 convention 来确定应通过 User 模型上的 posts 关系方法创建相关模型:

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

当使用魔术方法创建工厂关系时,你可以传递一个属性数组来覆盖相关模型:

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

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

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

属于关系

现在我们已经探索了如何使用 factories 构建 “has many” 关系,让我们探索一下这种关系的反面。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();

使用 Magic 方法

为方便起见,你可以使用 Laravel 的 magic factory 关系方法来定义 “belongs to” 关系。例如,以下示例将使用 convention 来确定这三个帖子应属于 Post 模型上的用户关系:

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

多对多关系

Like has many 关系,可以使用 has 方法创建“多对多”关系:

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

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

中间表属性

如果您需要定义应在链接模型的中间表/中间表上设置的属性,则可以使用 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();

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

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

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

使用 Magic 方法

为方便起见,你可以使用 Laravel 的 magic factory 关系方法来定义多对多关系。例如,以下示例将使用 convention 来确定应通过 User 模型上的 roles relationship 方法创建相关模型:

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

多态关系

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

php
    use App\Models\Post;

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

Morph To 关系

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

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

多态多对多关系

可以创建多态“多对多”(morphToMany / morphedByMany)关系,就像非多态“多对多”关系一样:

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

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

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

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

工厂内定义关系

要在模型工厂中定义关系,通常需要将新的工厂实例分配给关系的外键。这通常是对 “反向” 关系(如 belongsTomorphTo 关系)完成的。例如,如果您想在创建帖子时创建新用户,您可以执行以下操作:

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(),
        ];
    }

重复使用现有关系模型

如果您的模型与另一个模型共享公共关系,则可以使用 recycle 方法确保为工厂创建的所有关系回收相关模型的单个实例。

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

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

如果您的模型属于公共用户或团队,您可能会发现recycle方法特别有用。

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

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