LaravelDocs(中文)

Eloquent:工廠 (Factories)

Model factories 提供方便的方式來產生測試資料

簡介 (Introduction)

在測試應用或生成資料庫資料時,你可能需要在資料庫中插入一些記錄。與其手動指定每一列的值,不如讓 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 庫,這允許你方便地為測試和生成種子生成各種隨機資料。

[!NOTE] 你可以通過更新 config/app.php 配置檔案中的 faker_locale 選項來更改應用的 Faker 語言環境。

定義模型工廠 (Defining Model Factories)

產生工廠 (Generating Factories)

要建立工廠,執行 make:factory Artisan 指令

php artisan make:factory PostFactory

新工廠類將放置在你的 database/factories 目錄中。

模型和工廠發現慣例 (Model and Factory Discovery Conventions)

定義工廠後,你可以使用由 Illuminate\Database\Eloquent\Factories\HasFactory 特性提供的靜態 factory 方法來實例化該模型的工廠實例。

HasFactory 特性的 factory 方法將使用慣例來確定該特性分配給的模型的適當工廠。具體來說,該方法將在 Database\Factories 命名空間中查找與模型名稱相符並以 Factory 為尾碼的類。如果這些慣例不適用於你的特定應用或工廠,你可以將 UseFactory 屬性新增到模型中以手動指定模型的工廠:

use Illuminate\Database\Eloquent\Attributes\UseFactory;
use Database\Factories\Administration\FlightFactory;

#[UseFactory(FlightFactory::class)]
class Flight extends Model
{
    // ...
}

或者,你可以在模型上覆寫 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;
}

工廠狀態 (Factory States)

狀態操作方法允許你定義可以以任何組合應用於模型工廠的離散修改。例如,你的 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',
        ];
    });
}

"已刪除"狀態 ("Trashed" State)

如果你的 Eloquent 模型可以是軟刪除,你可以叫用內建的 trashed 狀態方法來指示建立的模型應已是「軟刪除」。你無需手動定義 trashed 狀態,因為它自動可用於所有工廠:

use App\Models\User;

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

工廠回呼 (Factory Callbacks)

工廠回呼使用 afterMakingafterCreating 方法進行登記,允許你在建立或建立模型後執行其他任務。你應該通過在工廠類上定義 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) {
        // ...
    });
}

使用工廠建立模型 (Creating Models Using Factories)

實例化模型 (Instantiating Models)

定義工廠後,你可以使用由 Illuminate\Database\Eloquent\Factories\HasFactory 特性提供給模型的靜態 factory 方法來實例化該模型的工廠實例。讓我們看一下建立模型的幾個示例。首先,我們將使用 make 方法建立模型而不將其保存到資料庫:

use App\Models\User;

$user = User::factory()->make();

你可以使用 count 方法建立許多模型的集合:

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

應用狀態 (Applying States)

你也可以將任何 狀態 應用於模型。如果你想對模型應用多個狀態轉換,你可以簡單地直接呼叫狀態轉換方法:

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

覆寫屬性 (Overriding Attributes)

如果你想覆寫模型的某些預設值,你可以將值陣列傳遞給 make 方法。只有指定的屬性將被替換,而其餘屬性將保持工廠指定的預設值:

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

或者,可以在工廠實例上直接呼叫 state 方法來執行內聯狀態轉換:

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

[!NOTE] 使用工廠建立模型時,大量分配保護 會自動禁用。

保存模型 (Persisting Models)

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

序列 (Sequences)

有時你可能希望為每個建立的模型交替特定模型屬性的值。你可以通過將狀態轉換定義為序列來完成此操作。例如,你可能希望為每個建立的使用者交替 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();

在此示例中,五個使用者將使用 Yadmin 值建立,五個使用者將使用 Nadmin 值建立。

如果需要,你可以將閉包包含為序列值。每次序列需要新值時,閉包將被調用:

use Illuminate\Database\Eloquent\Factories\Sequence;

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

在序列閉包中,你可以訪問注入到閉包中的序列實例上的 $index 屬性。$index 屬性包含迄今為止通過序列發生的反覆運算次數:

$users = User::factory()
    ->count(10)
    ->state(new 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();

工廠關聯 (Factory Relationships)

一對多關聯 (Has Many Relationships)

接下來,讓我們探索使用 Laravel 流暢工廠方法建立 Eloquent 模型關聯。首先,讓我們假設我們的應用有一個 App\Models\User 模型和一個 App\Models\Post 模型。同時,假設 User 模型定義與 PosthasMany 關聯。我們可以建立一個有三個帖子的使用者,使用 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();

使用魔術方法 (Using Magic Methods)

為了方便起見,你可以使用 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();

從屬關聯 (Belongs To Relationships)

現在我們已經探索了如何使用工廠建立「一對多」關聯,讓我們探索關聯的反面。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();

使用魔術方法 (Using Magic Methods)

為了方便起見,你可以使用 Laravel 的魔術工廠關聯方法來定義「從屬」關聯。例如,下面的示例將使用慣例來確定三個帖子應屬於 Post 模型上的 user 關聯:

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

多對多關聯 (Many to Many Relationships)

一對多關聯 類似,「多對多」關聯可以使用 has 方法建立:

use App\Models\Role;
use App\Models\User;

$user = User::factory()
    ->has(Role::factory()->count(3))
    ->create();

中樞表屬性 (Pivot Table Attributes)

如果需要定義應在連接模型的中樞/中間表上設定的屬性,你可以使用 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();

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

使用魔術方法 (Using Magic Methods)

為了方便起見,你可以使用 Laravel 的魔術工廠關聯方法來定義多對多關聯。例如,下面的示例將使用慣例來確定相關模型應通過 User 模型上的 roles 關聯方法建立:

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

多態關聯 (Polymorphic Relationships)

多態關聯 也可以使用工廠建立。多態「形態多」關聯的建立方式與典型的「一對多」關聯相同。例如,如果 App\Models\Post 模型與 App\Models\Comment 模型有 morphMany 關聯:

use App\Models\Post;

$post = Post::factory()->hasComments(3)->create();

Morph To 關聯 (Morph To Relationships)

無法使用魔術方法建立 morphTo 關聯。相反,必須直接使用 for 方法並明確提供關聯的名稱。例如,假設 Comment 模型有一個定義 morphTo 關聯的 commentable 方法。在這種情況下,我們可以通過直接使用 for 方法建立屬於單個帖子的三個評論:

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

多態多對多關聯 (Polymorphic Many to Many Relationships)

多態「多對多」(morphToMany / morphedByMany) 關聯可以像非多態「多對多」關聯一樣建立:

use App\Models\Tag;
use App\Models\Video;

$video = Video::factory()
    ->hasAttached(
        Tag::factory()->count(3),
        ['public' => true]
    )
    ->create();

當然,魔術 has 方法也可用於建立多態「多對多」關聯:

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

在工廠中定義關聯 (Defining Relationships Within Factories)

要在模型工廠中定義關聯,你通常會將新工廠實例分配給關聯的外鍵。這通常為「反向」關聯(如 belongsTomorphTo 關聯)完成。例如,如果你想在建立帖子時建立新使用者,你可以執行以下操作:

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

為關聯回收現有模型 (Recycling an Existing Model for Relationships)

如果你有與另一個模型共享常見關聯的模型,你可以使用 recycle 方法來確保工廠建立的所有關聯中回收相關模型的單個實例。

例如,假設你有 AirlineFlightTicket 模型,其中機票屬於航空公司和航班,航班也屬於航空公司。建立機票時,你可能希望機票和航班都有相同的航空公司,所以你可以將航空公司實例傳遞給 recycle 方法:

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

你可能會發現 recycle 方法特別有用,如果你有屬於常見使用者或團隊的模型。

recycle 方法也接受現有模型的集合。當集合提供給 recycle 方法時,當工廠需要該類型的模型時,將從集合中選擇隨機模型:

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