Eloquent:Factory
簡介
在測試專案或為資料庫填充資料時,我們可能會需要先插入一些資料到資料庫內。比起在建立這個測試資料時手動指定各個欄位的值,在 Laravel 中,我們可以使用 Model Factory 來為各個 Eloquent Model 定義一系列的預設屬性。
若要看看如何撰寫 Factory 的範例,請參考專案中的 database/factories/UserFactory.php
。該 Factory 包含在所有的 Laravel 新專案內,裡面有下列 Factory 定義:
1namespace Database\Factories;23use Illuminate\Database\Eloquent\Factories\Factory;4use Illuminate\Support\Facades\Hash;5use Illuminate\Support\Str;67/**8 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>9 */10class UserFactory extends Factory11{12 /**13 * The current password being used by the factory.14 */15 protected static ?string $password;1617 /**18 * Define the model's default state.19 *20 * @return array<string, mixed>21 */22 public function definition(): array23 {24 return [25 'name' => fake()->name(),26 'email' => fake()->unique()->safeEmail(),27 'email_verified_at' => now(),28 'password' => static::$password ??= Hash::make('password'),29 'remember_token' => Str::random(10),30 ];31 }3233 /**34 * Indicate that the model's email address should be unverified.35 */36 public function unverified(): static37 {38 return $this->state(fn (array $attributes) => [39 'email_verified_at' => null,40 ]);41 }42}
1namespace Database\Factories;23use Illuminate\Database\Eloquent\Factories\Factory;4use Illuminate\Support\Facades\Hash;5use Illuminate\Support\Str;67/**8 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>9 */10class UserFactory extends Factory11{12 /**13 * The current password being used by the factory.14 */15 protected static ?string $password;1617 /**18 * Define the model's default state.19 *20 * @return array<string, mixed>21 */22 public function definition(): array23 {24 return [25 'name' => fake()->name(),26 'email' => fake()->unique()->safeEmail(),27 'email_verified_at' => now(),28 'password' => static::$password ??= Hash::make('password'),29 'remember_token' => Str::random(10),30 ];31 }3233 /**34 * Indicate that the model's email address should be unverified.35 */36 public function unverified(): static37 {38 return $this->state(fn (array $attributes) => [39 'email_verified_at' => null,40 ]);41 }42}
如上所示,最基礎的 Factory 格式就像這樣,只需繼承 Laravel 的基礎 Factory 類別並定義一個 definition
方法。definition
方法應回傳一組預設的屬性值,會在使用 Factory 建立 Model 時被套用到該 Model 上。
通過 fake
輔助函式,Factory 就可以存取 Faker PHP 函式庫。該函式庫可用來方便地產生各種類型的隨機資料以進行測試或資料填充。
You can change your application's Faker locale by updating the faker_locale
option in your config/app.php
configuration file.
定義 Model Factory
產生 Factory
若要建立 Factory,請執行 make:factory
Artisan 指令:
1php artisan make:factory PostFactory
1php artisan make:factory PostFactory
新的 Factory 類別會被放在 database/factories
目錄內。
Model and Factory Discovery Conventions
定義好 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;23/**4 * Create a new factory instance for the model.5 */6protected static function newFactory()7{8 return FlightFactory::new();9}
1use Database\Factories\Administration\FlightFactory;23/**4 * Create a new factory instance for the model.5 */6protected static function newFactory()7{8 return FlightFactory::new();9}
接著,在對應的 Factory 上定義一個 model
屬性:
1use App\Administration\Flight;2use Illuminate\Database\Eloquent\Factories\Factory;34class FlightFactory extends Factory5{6 /**7 * The name of the factory's corresponding model.8 *9 * @var class-string<\Illuminate\Database\Eloquent\Model>10 */11 protected $model = Flight::class;12}
1use App\Administration\Flight;2use Illuminate\Database\Eloquent\Factories\Factory;34class FlightFactory extends Factory5{6 /**7 * The name of the factory's corresponding model.8 *9 * @var class-string<\Illuminate\Database\Eloquent\Model>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 所定義的原始屬性。該閉包應回傳一組陣列,期中包含要修改的屬性:
1use Illuminate\Database\Eloquent\Factories\Factory;23/**4 * Indicate that the user is suspended.5 */6public function suspended(): Factory7{8 return $this->state(function (array $attributes) {9 return [10 'account_status' => 'suspended',11 ];12 });13}
1use Illuminate\Database\Eloquent\Factories\Factory;23/**4 * Indicate that the user is suspended.5 */6public function suspended(): Factory7{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;23$user = User::factory()->trashed()->create();
1use App\Models\User;23$user = User::factory()->trashed()->create();
Factory 回呼
Factory 回呼使用 afterMaking
與 afterCreating
方法來註冊,能讓你在產生或建立 Model 時執行額外的任務。要註冊這些回呼,應在 Factory 類別上定義一個 configure
方法。Laravel 會在 Factory 初始化後自動呼叫這個方法:
1namespace Database\Factories;23use App\Models\User;4use Illuminate\Database\Eloquent\Factories\Factory;56class UserFactory extends Factory7{8 /**9 * Configure the model factory.10 */11 public function configure(): static12 {13 return $this->afterMaking(function (User $user) {14 // ...15 })->afterCreating(function (User $user) {16 // ...17 });18 }1920 // ...21}
1namespace Database\Factories;23use App\Models\User;4use Illuminate\Database\Eloquent\Factories\Factory;56class UserFactory extends Factory7{8 /**9 * Configure the model factory.10 */11 public function configure(): static12 {13 return $this->afterMaking(function (User $user) {14 // ...15 })->afterCreating(function (User $user) {16 // ...17 });18 }1920 // ...21}
也可以在 State 方法中註冊 Factory 回呼以在執行一些特定 State 才會用到的任務:
1use App\Models\User;2use Illuminate\Database\Eloquent\Factories\Factory;34/**5 * Indicate that the user is suspended.6 */7public function suspended(): Factory8{9 return $this->state(function (array $attributes) {10 return [11 'account_status' => 'suspended',12 ];13 })->afterMaking(function (User $user) {14 // ...15 })->afterCreating(function (User $user) {16 // ...17 });18}
1use App\Models\User;2use Illuminate\Database\Eloquent\Factories\Factory;34/**5 * Indicate that the user is suspended.6 */7public function suspended(): Factory8{9 return $this->state(function (array $attributes) {10 return [11 'account_status' => 'suspended',12 ];13 })->afterMaking(function (User $user) {14 // ...15 })->afterCreating(function (User $user) {16 // ...17 });18}
使用 Factory 來建立 Model
產生 Model
定義好 Factory 後,就可以使用 Illuminate\Database\Eloquent\Factories\HasFactory
trait 提供給 Model 的 factory
靜態方法來產生用於該 Model 的 Factory 實體。來看看一些建立 Model 的範例。首先,我們先使用 make
方法來在不儲存進資料庫的情況下建立 Model:
1use App\Models\User;23$user = User::factory()->make();
1use App\Models\User;23$user = User::factory()->make();
可以使用 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();
大量賦值保護 會在使用 Factory 建立 Model 時自動禁用。
保存 Model
create
方法會產生 Model 實體並使用 Eloquent 的 save
方法來將其永久保存於資料庫內:
1use App\Models\User;23// Create a single App\Models\User instance...4$user = User::factory()->create();56// Create three App\Models\User instances...7$users = User::factory()->count(3)->create();
1use App\Models\User;23// Create a single App\Models\User instance...4$user = User::factory()->create();56// Create three App\Models\User instances...7$users = User::factory()->count(3)->create();
可以通過將一組屬性陣列傳入 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
欄位的值為 Y
或 N
:
1use App\Models\User;2use Illuminate\Database\Eloquent\Factories\Sequence;34$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;34$users = User::factory()5 ->count(10)6 ->state(new Sequence(7 ['admin' => 'Y'],8 ['admin' => 'N'],9 ))10 ->create();
在上面的範例中,有五個使用者會以 admin
值 Y
建立,另外五個使用者將以 admin
值 N
建立。
若有需要,也可以提供閉包作為序列的值。該閉包會在每次序列需要新值是被叫用:
1use Illuminate\Database\Eloquent\Factories\Sequence;23$users = User::factory()4 ->count(10)5 ->state(new Sequence(6 fn (Sequence $sequence) => ['role' => UserRoles::all()->random()],7 ))8 ->create();
1use Illuminate\Database\Eloquent\Factories\Sequence;23$users = User::factory()4 ->count(10)5 ->state(new Sequence(6 fn (Sequence $sequence) => ['role' => UserRoles::all()->random()],7 ))8 ->create();
在 Sequence 閉包中,可以在注入到閉包中的 Sequence 實體上存取 $index
與 $count
屬性。$index
屬性包含了該 Sequence 到目前為止所進行的迭代數,而 $count
屬性則代表了該 Sequence 總過將被叫用幾次:
1$users = User::factory()2 ->count(10)3 ->sequence(fn (Sequence $sequence) => ['name' => 'Name '.$sequence->index])4 ->create();
1$users = User::factory()2 ->count(10)3 ->sequence(fn (Sequence $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 中定義了對 Post
的 hasMany
關聯。我們可以使用 Laravel Factory 提供的 has
方法來建立一個有三篇貼文的使用者。這個 has
方法接受一個 Factory 實體:
1use App\Models\Post;2use App\Models\User;34$user = User::factory()5 ->has(Post::factory()->count(3))6 ->create();
1use App\Models\Post;2use App\Models\User;34$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;34$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;34$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();23$posts = Post::factory()4 ->count(3)5 ->for($user)6 ->create();
1$user = User::factory()->create();23$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();
Many to Many Relationships
與 HasMany 關聯,「多對多」關聯也可以通過 has
方法建立:
1use App\Models\Role;2use App\Models\User;34$user = User::factory()5 ->has(Role::factory()->count(3))6 ->create();
1use App\Models\Role;2use App\Models\User;34$user = User::factory()5 ->has(Role::factory()->count(3))6 ->create();
Pivot 表屬性
若有需要為這些 Model 定義關聯 Pivot/中介資料表上的屬性,則可使用 hasAttached
方法。這個方法接受一個陣列,其中包含 Pivot 資料表上的屬性名稱,第二個引數則為其值:
1use App\Models\Role;2use App\Models\User;34$user = User::factory()5 ->hasAttached(6 Role::factory()->count(3),7 ['active' => true]8 )9 ->create();
1use App\Models\Role;2use App\Models\User;34$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();23$user = User::factory()4 ->count(3)5 ->hasAttached($roles, ['active' => true])6 ->create();
1$roles = Role::factory()->count(3)->create();23$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;23$post = Post::factory()->hasComments(3)->create();
1use App\Models\Post;23$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();
Polymorphic Many to Many Relationships
要建立多型的「多對多」(morphyToMany
/ morphedByMany
) 關聯,就與其他非多型的「多對多」關聯一樣:
1use App\Models\Tag;2use App\Models\Video;34$videos = Video::factory()5 ->hasAttached(6 Tag::factory()->count(3),7 ['public' => true]8 )9 ->create();
1use App\Models\Tag;2use App\Models\Video;34$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 實體。一般是使用「相反」的關聯來處理,如 belongsTo
與 morphTo
關聯。舉例來說,若想在建立貼文時建立新使用者,可以像這樣:
1use App\Models\User;23/**4 * Define the model's default state.5 *6 * @return array<string, mixed>7 */8public function definition(): array9{10 return [11 'user_id' => User::factory(),12 'title' => fake()->title(),13 'content' => fake()->paragraph(),14 ];15}
1use App\Models\User;23/**4 * Define the model's default state.5 *6 * @return array<string, mixed>7 */8public function definition(): array9{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<string, mixed>5 */6public function definition(): array7{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<string, mixed>5 */6public function definition(): array7{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}
Recycling an Existing Model for Relationships
若有多個 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();