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
{
/**
* The current password being used by the 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),
];
}
/**
* Indicate that the model's email address should be unverified.
*/
public function unverified(): static
{
return $this->state(fn (array $attributes) => [
'email_verified_at' => null,
]);
}
}可以看出,最基本的工厂是继承 Laravel 基础工厂类并定义 definition 方法的类。definition 方法返回使用该工厂创建模型时应应用的默认属性值集合。
通过 fake 辅助函数,工厂可以使用 Faker PHP 库,方便地生成各类随机数据用于测试与填充。
INFO
你可以通过更新 config/app.php 配置文件中的 faker_locale 选项来更改应用的 Faker 区域设置。
定义模型工厂
生成工厂
要创建工厂,请执行 make:factory Artisan 命令:
php artisan make:factory PostFactory新的工厂类会放在 database/factories 目录中。
模型与工厂发现约定
定义工厂后,你可以使用 Illuminate\Database\Eloquent\Factories\HasFactory trait 提供给模型的静态 factory 方法,为该模型实例化工厂。
HasFactory trait 的 factory 方法会按约定为已应用该 trait 的模型确定合适的工厂。具体而言,该方法会在 Database\Factories 命名空间中查找类名与模型名匹配并以 Factory 结尾的工厂。若这些约定不适用于你的应用或工厂,可在模型上重写 newFactory 方法,直接返回对应工厂的实例:
use Database\Factories\Administration\FlightFactory;
/**
* Create a new factory instance for the model.
*/
protected static function newFactory()
{
return FlightFactory::new();
}然后,在对应的工厂上定义 model 属性:
use App\Administration\Flight;
use Illuminate\Database\Eloquent\Factories\Factory;
class FlightFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* @var class-string<\Illuminate\Database\Eloquent\Model>
*/
protected $model = Flight::class;
}
工厂状态
状态操作方法允许你定义可任意组合应用到模型工厂的离散修改。例如,Database\Factories\UserFactory 工厂可能包含一个 suspended 状态方法,用于修改某个默认属性值。
状态转换方法通常会调用 Laravel 基础工厂类提供的 state 方法。state 接受一个闭包,闭包会收到工厂定义的原始属性数组,并应返回要修改的属性数组:
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* Indicate that the user is suspended.
*/
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();
工厂回调
工厂回调通过 afterMaking 与 afterCreating 方法注册,可在 make 或 create 模型后执行额外任务。应在工厂类上定义 configure 方法来注册这些回调。Laravel 在实例化工厂时会自动调用该方法:
namespace Database\Factories;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
class UserFactory extends Factory
{
/**
* Configure the model 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;
/**
* Indicate that the user is suspended.
*/
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 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();
INFO
使用工厂创建模型时,批量赋值保护会自动禁用。
持久化模型
create 方法会实例化模型,并通过 Eloquent 的 save 方法将其持久化到数据库:
use App\Models\User;
// Create a single App\Models\User instance...
$user = User::factory()->create();
// Create three App\Models\User instances...
$users = User::factory()->count(3)->create();
你可以通过向 create 方法传入属性数组来覆盖工厂的默认模型属性:
$user = User::factory()->create([
'name' => 'Abigail',
]);
序列
有时你可能希望为每个创建的模型交替某个属性的值。可将状态转换定义为序列来实现。例如,你可能希望让每个创建用户的 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 或 $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();
按约定,当向 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();
使用魔术方法
为方便起见,你可以使用 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();
多态关系
多态关联也可以使用工厂创建。多态「morph many」关联的创建方式与典型「一对多」关联相同。例如,若 App\Models\Post 模型与 App\Models\Comment 模型有 morphMany 关联:
use App\Models\Post;
$post = Post::factory()->hasComments(3)->create();
Morph To 关联
不能使用魔术方法创建 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();
在工厂内定义关联
要在模型工厂内定义关联,通常会将新的工厂实例赋给关联的外键。这一般用于 belongsTo、morphTo 等「反向」关联。例如,若希望在创建帖子时同时创建新用户,可以这样做:
use App\Models\User;
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'user_id' => User::factory(),
'title' => fake()->title(),
'content' => fake()->paragraph(),
];
}若关联的列依赖于定义它的工厂,可将闭包赋给属性。闭包会收到工厂已求值的属性数组:
/**
* Define the model's default state.
*
* @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();