laravel
8. Eloquent ORM
Eloquent:工厂

介绍

在测试您的应用程序或为数据库播种时,您可能需要向数据库中插入一些记录。Laravel 允许您使用模型工厂为每个 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 (opens in a new tab) PHP 库,这使您可以方便地为测试和播种生成各种随机数据。

[!NOTE]
您可以通过更新 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 的工厂。如果这些约定不适用于您的特定应用程序或工厂,您可以在模型上覆盖 newFactory 方法,以直接返回模型相应工厂的实例:

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

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

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

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

“已删除”状态

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

use App\Models\User;

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

工厂回调

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

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

持久化模型

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

序列

有时,您可能希望为每个创建的模型交替更改给定模型属性的值。您可以通过将状态转换定义为序列来实现此目的。例如,您可能希望为每个创建的用户在 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();

在这个示例中,将创建五个 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$count 属性。$index 属性包含到目前为止通过序列的迭代次数,而 $count 属性包含序列将被调用的总次数:

$users = User::factory()
                ->count(10)
                ->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 的流畅工厂方法构建 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();

按照惯例,当将 Post 模型传递给 has 方法时,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();

使用神奇方法

为了方便起见,您可以使用 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();

使用神奇方法

为了方便起见,您可以使用 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();

中间表属性

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

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

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

使用神奇方法

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

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

多态关系

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

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

多态属于关系

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

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

多态多对多关系

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

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

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

$videos = 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模型,其中票属于航空公司和航班,而航班也属于航空公司。在创建票时,您可能希望票和航班都属于同一个航空公司,因此您可以将一个航空公司实例传递给recycle方法:

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

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

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

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