Lzh on GitHub

介绍

在测试应用或填充数据库时,你可能需要向数据库插入一些记录。与手动指定每一列的值不同,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 方法。该方法返回在使用工厂创建模型时应应用的一组默认属性值。

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

你可以通过更新 config/app.php 配置文件中的 faker_locale 选项来更改应用的 Faker 本地化语言。

定义模型工厂

生成工厂

要创建一个工厂,可以执行 Artisan 命令 make:factory

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

然后,在对应的工厂中定义模型属性:

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 状态方法,用于修改其默认属性值之一。

状态转换方法通常会调用 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();

工厂回调

工厂回调可通过 afterMakingafterCreating 方法注册,用于在模型被 makecreate 后执行额外操作。你应通过在工厂类中定义 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) {
            // 在 make 后执行的操作
        })->afterCreating(function (User $user) {
            // 在 create 后执行的操作
        });
    }

    // ...
}

你也可以在状态方法中注册工厂回调,以执行特定状态下的额外操作:

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) {
        // 在 make 后执行的操作
    })->afterCreating(function (User $user) {
        // 在 create 后执行的操作
    });
}

使用工厂创建模型

实例化模型

一旦定义好工厂后,你可以使用 Illuminate\Database\Eloquent\Factories\HasFactory trait 提供的静态 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',
]);

序列

有时,你可能希望为每个创建的模型交替设置某个属性的值。这可以通过定义状态变换序列(state transformation as a sequence)来实现。例如,你可能希望为每个创建的用户在 admin 列中交替使用 YN 值:

use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Sequence;
 
$users = User::factory()
    ->count(10)
    ->state(new Sequence(
        ['admin' => 'Y'],
        ['admin' => 'N'],
    ))
    ->create();

在这个例子中,将创建 10 个用户:其中 5 个用户的 admin 值为 Y,另外 5 个用户的 admin 值为 N

如果需要,你也可以在序列值中使用闭包。每次序列需要新值时,闭包都会被调用:

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

在序列闭包中,你可以访问注入到闭包中的 $sequence 实例的 $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();

工厂关系

一对多关系

接下来,让我们探讨如何使用 Laravel 的流式工厂方法(fluent factory methods)构建 Eloquent 模型关系。首先,假设我们的应用中有一个 App\Models\User 模型和一个 App\Models\Post 模型,并且 User 模型定义了一个 hasMany 关系关联到 Post。我们可以使用工厂的 has 方法创建一个拥有三篇文章的用户。has 方法接受一个工厂实例:

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

按照约定,当将 Post 模型传递给 has 方法时,Laravel 会假定 User 模型必须具有一个名为 posts 的方法来定义该关系。如果需要,你也可以显式指定想要操作的关系名称:

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

当然,你也可以对关联模型执行状态变换(state manipulations)。此外,如果你的状态变换需要访问父模型,可以使用基于闭包的状态变换:

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

使用魔术方法(Magic Methods)

为了方便,你可以使用 Laravel 的工厂魔术关系方法来构建关联。例如,下面的示例将根据约定,通过 User 模型的 posts 关系方法创建关联模型:

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

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

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

如果状态变换需要访问父模型,你也可以传递基于闭包的状态变换:

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

属于关系

在我们了解了如何使用工厂构建 “一对多” 关系之后,现在来探索这种关系的反向操作。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();

使用魔术方法(Magic Methods)

为了方便,你可以使用 Laravel 的工厂魔术关系方法来定义 “属于” 的关系。例如,下面的示例会根据约定,确定这三篇文章应该属于 Post 模型上的 user 关系:

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

多对多关系

像“一对多”关系一样,“多对多”关系也可以使用 has 方法来创建:

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

关联表(Pivot Table)属性

如果你需要为连接模型的中间表(pivot / intermediate table)设置属性,可以使用 hasAttached 方法。此方法的第二个参数接受一个包含 pivot 表属性名和对应值的数组:

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();

如果你已经有了想要附加到新建模型的模型实例,也可以将这些实例传递给 hasAttached 方法。在下面的例子中,同样的三个角色将被附加到所有三个用户:

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

使用魔术方法(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 关系,不能使用魔术方法(magic methods)来创建。此时必须直接使用 for 方法,并显式提供关系名称。例如,假设 Comment 模型有一个 commentable 方法定义了 morphTo 关系,在这种情况下,我们可以使用 for 方法直接创建三个属于同一篇文章的评论:

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

多态多对多关系(Polymorphic Many to Many Relationships)

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

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

当然,也可以使用魔术 has 方法来创建多态的 “many to many” 关系:

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

在工厂中定义关系

在模型工厂中定义关系时,通常会将一个新的工厂实例赋值给该关系的外键。这通常用于“反向”关系,例如 belongsTomorphTo 关系。例如,如果在创建一篇文章时希望同时创建一个用户,可以这样操作:

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 方法,确保在工厂创建的所有关系中重复使用同一个关联模型实例。

例如,假设你有 AirlineFlightTicket 模型,其中 Ticket 属于某个航空公司和航班,而 Flight 也属于某个航空公司。在创建票据时,你可能希望票据和航班使用同一个航空公司实例,这时可以将航空公司实例传给 recycle 方法:

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

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

recycle 方法也可以接收一个已有模型的集合。当传入集合时,工厂在需要该类型模型时,会从集合中随机选择一个模型:

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