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 库,该库使您能够方便地为测试和播种生成各种随机数据。
您可以通过更新 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 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
方法注册,并允许您在制作或创建模型后执行其他任务。您应该通过在工厂类上定义 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
特性提供的模型的静态 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; // 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();
按照惯例,将 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();
多态关系
多态关系 也可以使用工厂创建。多态“morph many”关系的创建方式与典型的“has many”关系相同。例如,如果 `App\Models\Post` 模型与 `App\Models\Comment` 模型具有 `morphMany` 关系。
use App\Models\Post; $post = Post::factory()->hasComments(3)->create();
Morph To 关系
魔法方法不能用于创建 `morphTo` 关系。而是必须直接使用 `for` 方法,并且必须明确提供关系的名称。例如,假设 `Comment` 模型有一个 `commentable` 方法,该方法定义了一个 `morphTo` 关系。在这种情况下,我们可以使用 `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();