Eloquent:Factory

簡介

在測試專案或為資料庫填充資料時,我們可能會需要先插入一些資料到資料庫內。比起在建立這個測試資料時手動指定各個欄位的值,在 Laravel 中,我們可以使用 Model Factory 來為各個 Eloquent Model 定義一系列的預設屬性。

若要看看如何撰寫 Factory 的範例,請參考專案中的 database/factories/UserFactory.php。該 Factory 包含在所有的 Laravel 新專案內,裡面有下列 Factory 定義:

1namespace Database\Factories;
2 
3use Illuminate\Database\Eloquent\Factories\Factory;
4use Illuminate\Support\Str;
5 
6class UserFactory extends Factory
7{
8 /**
9 * Define the model's default state.
10 *
11 * @return array
12 */
13 public function definition()
14 {
15 return [
16 'name' => fake()->name(),
17 'email' => fake()->unique()->safeEmail(),
18 'email_verified_at' => now(),
19 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
20 'remember_token' => Str::random(10),
21 ];
22 }
23}
1namespace Database\Factories;
2 
3use Illuminate\Database\Eloquent\Factories\Factory;
4use Illuminate\Support\Str;
5 
6class UserFactory extends Factory
7{
8 /**
9 * Define the model's default state.
10 *
11 * @return array
12 */
13 public function definition()
14 {
15 return [
16 'name' => fake()->name(),
17 'email' => fake()->unique()->safeEmail(),
18 'email_verified_at' => now(),
19 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
20 'remember_token' => Str::random(10),
21 ];
22 }
23}

如上所示,最基礎的 Factory 格式就像這樣,只需繼承 Laravel 的基礎 Factory 類別並定義一個 definition 方法。definition 方法應回傳一組預設的屬性值,會在使用 Factory 建立 Model 時被套用到該 Model 上。

通過 fake 輔助函式,Factory 就可以存取 Faker PHP 函式庫。該函式庫可用來方便地產生各種類型的隨機資料以進行測試或資料填充。

lightbulb

可以通過在 config/app.php 設定檔中加上 faker_locale 選項來設定專案的 Faker 語系設定。

定義 Model Factory

產生 Factory

若要建立 Factory,請執行 make:factory Artisan 指令

1php artisan make:factory PostFactory
1php artisan make:factory PostFactory

新的 Factory 類別會被放在 database/factories 目錄內。

Model 於 Factory 的自動偵測慣例

定義好 Factory 後,就可以使用 Illuminate\Database\Eloquent\Factories\HasFactory Trait 提供給 Model 的靜態 factory 方法來為該 Model 初始化一個 Factory 實體。

HasFactory Trait 的 factory 方法會使用慣例來判斷適合用於該 Model 的 Factory。更準確來講,該方法會在 Database\Factories 命名空間下尋找符合該 Model 名稱並以 Factory 結尾的類別。若這些慣例不適合用在你正在寫的專案或 Factory,則可以在 Model 上複寫 newFactory 方法來直接回傳與該 Model 對應的 Factory 實體:

1use Database\Factories\Administration\FlightFactory;
2 
3/**
4 * Create a new factory instance for the model.
5 *
6 * @return \Illuminate\Database\Eloquent\Factories\Factory
7 */
8protected static function newFactory()
9{
10 return FlightFactory::new();
11}
1use Database\Factories\Administration\FlightFactory;
2 
3/**
4 * Create a new factory instance for the model.
5 *
6 * @return \Illuminate\Database\Eloquent\Factories\Factory
7 */
8protected static function newFactory()
9{
10 return FlightFactory::new();
11}

接著,在對應的 Factory 上定義一個 model 屬性:

1use App\Administration\Flight;
2use Illuminate\Database\Eloquent\Factories\Factory;
3 
4class FlightFactory extends Factory
5{
6 /**
7 * The name of the factory's corresponding model.
8 *
9 * @var string
10 */
11 protected $model = Flight::class;
12}
1use App\Administration\Flight;
2use Illuminate\Database\Eloquent\Factories\Factory;
3 
4class FlightFactory extends Factory
5{
6 /**
7 * The name of the factory's corresponding model.
8 *
9 * @var string
10 */
11 protected $model = Flight::class;
12}

State - Factory 狀態

State 操作方法可定義一些個別的修改,並可任意組合套用到 Model Factory 上。舉例來說,Database\Factories\UserFactory Factory 可包含一個 suspended (已停用) State 方法,用來修改該 Model Factory 的預設屬性值。

State 變換方法通常是呼叫 Laravel 基礎 Factory 類別所提供的 state 方法。這個 state 方法接受一個閉包,該閉包會收到一組陣列,陣列內包含了由這個 Factory 所定義的原始屬性。該閉包應回傳一組陣列,期中包含要修改的屬性:

1/**
2 * Indicate that the user is suspended.
3 *
4 * @return \Illuminate\Database\Eloquent\Factories\Factory
5 */
6public function suspended()
7{
8 return $this->state(function (array $attributes) {
9 return [
10 'account_status' => 'suspended',
11 ];
12 });
13}
1/**
2 * Indicate that the user is suspended.
3 *
4 * @return \Illuminate\Database\Eloquent\Factories\Factory
5 */
6public function suspended()
7{
8 return $this->state(function (array $attributes) {
9 return [
10 'account_status' => 'suspended',
11 ];
12 });
13}

Trashed(已刪除)」State

若 Eloquent Model 有開啟軟刪除功能,則我們可以叫用內建的 trashed State 方法來代表要建立的 Model 應被標記為「已軟刪除」。所有的 Factory 都自動擁有該方法,因此不需手動定義 trashed State:

1use App\Models\User;
2 
3$user = User::factory()->trashed()->create();
1use App\Models\User;
2 
3$user = User::factory()->trashed()->create();

Factory 回呼

Factory 回呼使用 afterMakingafterCreating 方法來註冊,能讓你在產生或建立 Model 時執行額外的任務。要註冊這些回呼,應在 Factory 類別上定義一個 configure 方法。Laravel 會在 Factory 初始化後自動呼叫這個方法:

1namespace Database\Factories;
2 
3use App\Models\User;
4use Illuminate\Database\Eloquent\Factories\Factory;
5use Illuminate\Support\Str;
6 
7class UserFactory extends Factory
8{
9 /**
10 * Configure the model factory.
11 *
12 * @return $this
13 */
14 public function configure()
15 {
16 return $this->afterMaking(function (User $user) {
17 //
18 })->afterCreating(function (User $user) {
19 //
20 });
21 }
22 
23 // ...
24}
1namespace Database\Factories;
2 
3use App\Models\User;
4use Illuminate\Database\Eloquent\Factories\Factory;
5use Illuminate\Support\Str;
6 
7class UserFactory extends Factory
8{
9 /**
10 * Configure the model factory.
11 *
12 * @return $this
13 */
14 public function configure()
15 {
16 return $this->afterMaking(function (User $user) {
17 //
18 })->afterCreating(function (User $user) {
19 //
20 });
21 }
22 
23 // ...
24}

使用 Factory 來建立 Model

產生 Model

定義好 Factory 後,就可以使用 Illuminate\Database\Eloquent\Factories\HasFactory trait 提供給 Model 的 factory 靜態方法來產生用於該 Model 的 Factory 實體。來看看一些建立 Model 的範例。首先,我們先使用 make 方法來在不儲存進資料庫的情況下建立 Model:

1use App\Models\User;
2 
3$user = User::paginate();
1use App\Models\User;
2 
3$user = User::paginate();

可以使用 count 方法來建立包含多個 Model 的 Collection:

1$users = User::factory()->count(3)->make();
1$users = User::factory()->count(3)->make();

套用 State

也可以將 State 套用至 Model 上。若想套用多個 State 變換到 Model 上,只需要直接呼叫 State 變換方法即可:

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

複寫屬性

若想複寫 Model 上的一些預設值,可以傳入陣列到 make 方法上。只要指定要取代的屬性即可,剩下的屬性會保持 Factory 所指定的預設值:

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

或者,也可以直接在 Factory 實體上呼叫 state 方法來內嵌 State 變換:

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

大量賦值保護 會在使用 Factory 建立 Model 時自動禁用。

保存 Model

create 方法會產生 Model 實體並使用 Eloquent 的 save 方法來將其永久保存於資料庫內:

1use App\Models\User;
2 
3public function test_models_can_be_persisted()
4{
5 // 建立單一 App\Models\User 實體...
6 $user = User::factory()->create();
7 
8 // 建立三個 App\Models\User 實體...
9 $users = User::factory()->count(3)->create();
10 
11 // 在測試中使用 Model...
12}
1use App\Models\User;
2 
3public function test_models_can_be_persisted()
4{
5 // 建立單一 App\Models\User 實體...
6 $user = User::factory()->create();
7 
8 // 建立三個 App\Models\User 實體...
9 $users = User::factory()->count(3)->create();
10 
11 // 在測試中使用 Model...
12}

可以通過將一組屬性陣列傳入 create 方法來複寫該 Factory 的預設 Model 屬性:

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

Sequence - 序列

有時候,我們可能會需要為每個建立的 Model 更改某個特定的屬性。可以通過將 State 變換定義為序列來達成。舉例來說,我們可能會想為每個建立的使用者設定 admin 欄位的值為 YN

1use App\Models\User;
2use Illuminate\Database\Eloquent\Factories\Sequence;
3 
4$users = User::factory()
5 ->count(10)
6 ->state(new Sequence(
7 ['admin' => 'Y'],
8 ['admin' => 'N'],
9 ))
10 ->create();
1use App\Models\User;
2use Illuminate\Database\Eloquent\Factories\Sequence;
3 
4$users = User::factory()
5 ->count(10)
6 ->state(new Sequence(
7 ['admin' => 'Y'],
8 ['admin' => 'N'],
9 ))
10 ->create();

在上面的範例中,有五個使用者會以 adminY 建立,另外五個使用者將以 adminN 建立。

若有需要,也可以提供閉包作為序列的值。該閉包會在每次序列需要新值是被叫用:

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

在 Sequence 閉包中,可以在注入到閉包中的 Sequence 實體上存取 $index$count 屬性。$index 屬性包含了該 Sequence 到目前為止所進行的迭代數,而 $count 屬性則代表了該 Sequence 總過將被叫用幾次:

1$users = User::factory()
2 ->count(10)
3 ->sequence(fn ($sequence) => ['name' => 'Name '.$sequence->index])
4 ->create();
1$users = User::factory()
2 ->count(10)
3 ->sequence(fn ($sequence) => ['name' => 'Name '.$sequence->index])
4 ->create();

為了讓開發起來更方便,也提供了一個 sequence 方法可用來套用 Sequence。該方法會在內部幫你呼叫 state 方法。sequence 方法的引數為一個陣列,或是一組會被依序套用的屬性陣列:

1$users = User::factory()
2 ->count(2)
3 ->sequence(
4 ['name' => 'First User'],
5 ['name' => 'Second User'],
6 )
7 ->create();
1$users = User::factory()
2 ->count(2)
3 ->sequence(
4 ['name' => 'First User'],
5 ['name' => 'Second User'],
6 )
7 ->create();

Factory 關聯

HasMany 關聯

接著,來看看如何使用 Laravel 中流利的 Factory 方法建立 Eloquent Model 關聯。首先,假設專案中有個 App\Models\User Model 以及 App\Models\Post Model。然後,假設 User Model 中定義了對 PosthasMany 關聯。我們可以使用 Laravel Factory 提供的 has 方法來建立一個有三篇貼文的使用者。這個 has 方法接受一個 Factory 實體:

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

依照慣例,當傳入 Post Model 給 has 方法時,Laravel 會假設 User Model 中有定義這個關聯的 posts 方法。若有需要,可以明顯指定要操作的關聯名稱:

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

當然,也可以在關聯 Model 上進行 State 操作。此外,若 State 更改需要存取上層 Model,也可以傳入基於閉包的 State 變換:

1$user = User::factory()
2 ->has(
3 Post::factory()
4 ->count(3)
5 ->state(function (array $attributes, User $user) {
6 return ['user_type' => $user->type];
7 })
8 )
9 ->create();
1$user = User::factory()
2 ->has(
3 Post::factory()
4 ->count(3)
5 ->state(function (array $attributes, User $user) {
6 return ['user_type' => $user->type];
7 })
8 )
9 ->create();

使用魔術方法

為了方便起見,可以使用 Laravel 的魔術 Factory 關聯方法來建立關聯。舉例來說,下列範例會使用慣例來判斷應通過 User Model 上的 posts 關聯方法來建立關聯 Model:

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

在使用魔術方法建立 Factory 關聯時,可以傳入包含屬性的陣列來在關聯 Model 上複寫:

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

若 State 更改需要存取上層 Model,可以提供一個基於閉包的 State 變換:

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

BelongsTo 關聯

我們已經瞭解如何使用 Factory 來建立「Has Many」關聯了,接著來看看這種關聯的想法。使用 for 方法可以用來定義使用 Factory 建立的 Model 所隸屬 (Belong To) 的上層 Model。舉例來說,我們可以建立三個隸屬於單一使用者的 App\Models\Post Model 實體:

1use App\Models\Post;
2use App\Models\User;
3 
4$posts = Post::factory()
5 ->count(3)
6 ->for(User::factory()->state([
7 'name' => 'Jessica Archer',
8 ]))
9 ->create();
1use App\Models\Post;
2use App\Models\User;
3 
4$posts = Post::factory()
5 ->count(3)
6 ->for(User::factory()->state([
7 'name' => 'Jessica Archer',
8 ]))
9 ->create();

若已經有應與這些正在建立的 Model 關聯的上層 Model 實體,可以將該 Model 實體傳入 for 方法:

1$user = User::factory()->create();
2 
3$posts = Post::factory()
4 ->count(3)
5 ->for($user)
6 ->create();
1$user = User::factory()->create();
2 
3$posts = Post::factory()
4 ->count(3)
5 ->for($user)
6 ->create();

使用魔術方法

為了方便起見,可以使用 Laravel 的魔術 Factory 關聯方法來定義「Belongs To」關聯。舉例來說,下列範例會使用慣例來判斷應使用 Post Model 上的 user 關聯方法來設定這三個貼文應隸屬於哪裡:

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

多對多關聯

HasMany 關聯,「多對多」關聯也可以通過 has 方法建立:

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

Pivot 表屬性

若有需要為這些 Model 定義關聯 Pivot/中介資料表上的屬性,則可使用 hasAttached 方法。這個方法接受一個陣列,其中包含 Pivot 資料表上的屬性名稱,第二個引數則為其值:

1use App\Models\Role;
2use App\Models\User;
3 
4$user = User::factory()
5 ->hasAttached(
6 Role::factory()->count(3),
7 ['active' => true]
8 )
9 ->create();
1use App\Models\Role;
2use App\Models\User;
3 
4$user = User::factory()
5 ->hasAttached(
6 Role::factory()->count(3),
7 ['active' => true]
8 )
9 ->create();

若 State 更改需要存取關聯 Model,可以提供一個基於閉包的 State 變換:

1$user = User::factory()
2 ->hasAttached(
3 Role::factory()
4 ->count(3)
5 ->state(function (array $attributes, User $user) {
6 return ['name' => $user->name.' Role'];
7 }),
8 ['active' => true]
9 )
10 ->create();
1$user = User::factory()
2 ->hasAttached(
3 Role::factory()
4 ->count(3)
5 ->state(function (array $attributes, User $user) {
6 return ['name' => $user->name.' Role'];
7 }),
8 ['active' => true]
9 )
10 ->create();

若已有 Model 實體想讓正在建立的 Model 附加,可以將該 Model 實體傳入 hasAttached 方法。在此範例中,會將三個相同的角色附加給三個使用者:

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

使用魔術方法

為了方便起見,可以使用 Laravel 的魔術 Factory 關聯方法來定義 Many to Many 關聯。舉例來說,下列範例會使用慣例來判斷應通過 User Model 上的 roles 關聯方法來建立關聯 Model:

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

多型 (Polymorphic) 關聯

多型 (Polymorphic) 關聯 也可以使用 Factory 來建立。可使用與一般「HasMany」關聯相同的方法來建多型「Morph Many」關聯。舉例來說,若 App\Models\Post Model 使用 morphMany 關聯到 App\Models\Comment Model:

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

MorphTo 關聯

在建立 morphTo 關聯時無法使用魔法方法。必須直接使用 for 方法,並明顯提供該關聯的名稱。舉例來說,假設 Comment Model 有個 commantable 方法,該方法定義了 morphTo 關聯。在這種情況下,我們可以直接使用 for 方法來建立三個隸屬於單一貼文的留言:

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

多型的多對多關聯

要建立多型的「多對多」(morphyToMany / morphedByMany) 關聯,就與其他非多型的「多對多」關聯一樣:

1use App\Models\Tag;
2use App\Models\Video;
3 
4$videos = Video::factory()
5 ->hasAttached(
6 Tag::factory()->count(3),
7 ['public' => true]
8 )
9 ->create();
1use App\Models\Tag;
2use App\Models\Video;
3 
4$videos = Video::factory()
5 ->hasAttached(
6 Tag::factory()->count(3),
7 ['public' => true]
8 )
9 ->create();

當然,也可以使用 has 魔法方法來建立多型的「多對多」關聯:

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

在 Factory 中定義關聯

若要在 Model Factory 中定義關聯,則通常需要為該關聯的外部索引鍵 (Foreign Key) 指定新的 Factory 實體。一般是使用「相反」的關聯來處理,如 belongsTomorphTo 關聯。舉例來說,若想在建立貼文時建立新使用者,可以像這樣:

1use App\Models\User;
2 
3/**
4 * Define the model's default state.
5 *
6 * @return array
7 */
8public function definition()
9{
10 return [
11 'user_id' => User::factory(),
12 'title' => fake()->title(),
13 'content' => fake()->paragraph(),
14 ];
15}
1use App\Models\User;
2 
3/**
4 * Define the model's default state.
5 *
6 * @return array
7 */
8public function definition()
9{
10 return [
11 'user_id' => User::factory(),
12 'title' => fake()->title(),
13 'content' => fake()->paragraph(),
14 ];
15}

若該關聯的欄位仰賴定義其的 Factory,則可以在屬性中放入閉包。該閉包會收到該 Factory 取值結果的屬性陣列:

1/**
2 * Define the model's default state.
3 *
4 * @return array
5 */
6public function definition()
7{
8 return [
9 'user_id' => User::factory(),
10 'user_type' => function (array $attributes) {
11 return User::find($attributes['user_id'])->type;
12 },
13 'title' => fake()->title(),
14 'content' => fake()->paragraph(),
15 ];
16}
1/**
2 * Define the model's default state.
3 *
4 * @return array
5 */
6public function definition()
7{
8 return [
9 'user_id' => User::factory(),
10 'user_type' => function (array $attributes) {
11 return User::find($attributes['user_id'])->type;
12 },
13 'title' => fake()->title(),
14 'content' => fake()->paragraph(),
15 ];
16}

在關聯上回收使用現有的 Model

若有多個 Model 與另一個 Model 共用一個共同的關聯,則可以使用 `recycle`(回收) 方法來確保 Factory 所建立的關聯都重複使用此 Model 的某個單一實體:

舉例來說,假設有 `Airline`(航空公司)`Fligh`(航班)`Ticket`(機票) 三個 Model,其中,Ticket 隸屬於 (BelongsTo) Airline 與 Flight,而 Flight 也同時隸屬於 Airline。在建立 Ticket 時,我們可能會想在 Ticket 與 Flight 上都使用同一個 Airline。因此,我們可以將 Airline 實體傳給 recycle 方法:

1Ticket::factory()
2 ->recycle(Airline::factory()->create())
3 ->create();
1Ticket::factory()
2 ->recycle(Airline::factory()->create())
3 ->create();

如果你的 Model 都隸屬於 (BelongsTo) 一組相同的使用者或團隊,那麼就很適合使用 recycle 方法。

也可傳入一組現有 Model 的 Collection 給 recycle 方法。傳入 Collection 給 recycle 方法時,當 Factory 需要此類型的 Model 時,就會從此 Collection 中隨機選擇一個 Model:

1Ticket::factory()
2 ->recycle($airlines)
3 ->create();
1Ticket::factory()
2 ->recycle($airlines)
3 ->create();
翻譯進度
100% 已翻譯
更新時間:
2023年2月11日 上午10:28:00 [世界標準時間]
翻譯人員:
  • cornch
幫我們翻譯此頁

留言

尚無留言

“Laravel” is a Trademark of Taylor Otwell.
The source documentation is released under MIT license. See laravel/docs on GitHub for details.
The translated documentations are released under MIT license. See cornch/laravel-docs-l10n on GitHub for details.