> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# Eloquent Factories

> 說明如何運用模型工廠，為測試與 seeding 高效地產生假資料。

## 什麼是工廠

在測試或資料庫 seeding 時，我們常需要在資料庫中插入紀錄。
與其手動指定每個欄位的值，你可以透過 Laravel 的**模型工廠**，為每個 Eloquent 模型定義一組預設屬性。

所有新的 Laravel 應用程式都會附上 `database/factories/UserFactory.php`。

```php theme={null}
namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;

class UserFactory extends Factory
{
    // 避免每次重新對相同密碼做雜湊，將它快取
    protected static ?string $password;

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

透過 `fake()` 輔助函式，可使用 [Faker](https://github.com/FakerPHP/Faker) 函式庫生成各種隨機資料。

```mermaid theme={null}
flowchart LR
    F["Factory<br>database/factories/UserFactory"] -->|"make() / create()"| M["Model Instance<br>App\\Models\\User"]
    M -->|"create()"| DB[("Database")]
    F -->|"Faker"| R["隨機資料<br>name, email, ..."]
    R --> F
```

<Info>
  Faker 的 locale 可透過 `config/app.php` 的 `faker_locale` 選項變更。
</Info>

## 產生工廠

以 `make:factory` Artisan 指令建立工廠。

```shell theme={null}
php artisan make:factory PostFactory
```

新工廠類別會被放到 `database/factories` 目錄。

### 模型與工廠的自動偵測

若模型使用 `HasFactory` trait，Laravel 會自動找到對應的工廠。
它會在 `Database\Factories` 命名空間中，尋找模型名稱後綴 `Factory` 的類別。

若命名規則不符，可用 `UseFactory` 屬性明確指定。

```php theme={null}
use Illuminate\Database\Eloquent\Attributes\UseFactory;
use Database\Factories\Administration\FlightFactory;

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

## 定義工廠

### `definition()` 方法

工廠的核心方法 `definition()` 定義預設屬性。

```php theme={null}
namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;

class PostFactory extends Factory
{
    public function definition(): array
    {
        return [
            'user_id' => \App\Models\User::factory(),
            'title' => fake()->sentence(),
            'content' => fake()->paragraphs(3, true),
            'published_at' => fake()->optional()->dateTimeBetween('-1 year', 'now'),
        ];
    }
}
```

Faker 常用方法一覽：

| 方法                              | 生成資料範例             |
| ------------------------------- | ------------------ |
| `fake()->name()`                | `John Doe`         |
| `fake()->email()`               | `john@example.com` |
| `fake()->sentence()`            | 隨機一句話              |
| `fake()->paragraph()`           | 隨機一段文字             |
| `fake()->numberBetween(1, 100)` | `1`〜`100` 的整數      |
| `fake()->dateTime()`            | 隨機日期時間             |
| `fake()->boolean()`             | `true` / `false`   |

## 工廠狀態（States）

透過狀態方法可以為工廠定義個別變化。

```php theme={null}
use Illuminate\Database\Eloquent\Factories\Factory;

class UserFactory extends Factory
{
    public function definition(): array
    {
        return [
            'name' => fake()->name(),
            'email' => fake()->unique()->safeEmail(),
            'account_status' => 'active',
        ];
    }

    // 停用使用者的狀態
    public function suspended(): static
    {
        return $this->state(fn (array $attributes) => [
            'account_status' => 'suspended',
        ]);
    }

    // 管理員使用者的狀態
    public function admin(): static
    {
        return $this->state(fn (array $attributes) => [
            'is_admin' => true,
        ]);
    }
}
```

狀態可以組合使用。

```php theme={null}
$user = User::factory()->suspended()->create();
$admin = User::factory()->admin()->create();
```

### 軟刪除的狀態

若模型支援軟刪除，可以使用內建的 `trashed()` 狀態。

```php theme={null}
$user = User::factory()->trashed()->create();
```

## 工廠回呼（Callbacks）

可透過 `afterMaking` 與 `afterCreating` 定義模型生成後的額外處理。

```php theme={null}
namespace Database\Factories;

use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;

class UserFactory extends Factory
{
    public function configure(): static
    {
        return $this->afterMaking(function (User $user) {
            // 於 make() 後執行（尚未儲存到 DB）
        })->afterCreating(function (User $user) {
            // 於 create() 後執行（已儲存到 DB）
        });
    }

    // ...
}
```

在狀態方法內也可以註冊回呼。

```php theme={null}
public function withProfile(): static
{
    return $this->state(fn (array $attributes) => [])
        ->afterCreating(function (User $user) {
            $user->profile()->create([
                'bio' => fake()->paragraph(),
            ]);
        });
}
```

## 建立模型

### make() — 不存入 DB

```php theme={null}
use App\Models\User;

// 建立 1 筆
$user = User::factory()->make();

// 建立 3 筆（以集合回傳）
$users = User::factory()->count(3)->make();
```

### create() — 存入 DB

```php theme={null}
// 建立 1 筆並存入 DB
$user = User::factory()->create();

// 建立 3 筆並存入 DB
$users = User::factory()->count(3)->create();
```

### 覆寫屬性

傳入陣列給 `make()` 或 `create()`，即可只覆寫特定屬性。

```php theme={null}
$user = User::factory()->make([
    'name' => '山田太郎',
]);

$user = User::factory()->create([
    'name' => '鈴木花子',
    'email' => 'hanako@example.com',
]);
```

也可以使用狀態進行 inline 覆寫。

```php theme={null}
$user = User::factory()->state([
    'name' => '田中一郎',
])->make();
```

<Info>
  工廠建立模型時，會自動停用 mass assignment 保護。
</Info>

### Sequence（序列）

若要在建立多個模型時輪流變換屬性，可使用 `Sequence`。

```php theme={null}
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Sequence;

$users = User::factory()
    ->count(10)
    ->state(new Sequence(
        ['admin' => 'Y'],
        ['admin' => 'N'],
    ))
    ->create();
// 5 筆為 admin='Y'、5 筆為 admin='N'
```

透過 `sequence()` 方法可寫得更簡潔。

```php theme={null}
$users = User::factory()
    ->count(2)
    ->sequence(
        ['name' => '第一位使用者'],
        ['name' => '第二位使用者'],
    )
    ->create();
```

也可以用閉包動態生成隨機值。

```php theme={null}
use Illuminate\Database\Eloquent\Factories\Sequence;

$users = User::factory()
    ->count(10)
    ->state(new Sequence(
        fn (Sequence $sequence) => ['name' => '使用者 ' . $sequence->index],
    ))
    ->create();
```

## 關聯的工廠

```mermaid theme={null}
flowchart TD
    UF["UserFactory"] -->|"has(PostFactory)"| PF["PostFactory"]
    PF -->|"for(UserFactory)"| UF
    UF -->|"hasAttached(RoleFactory)"| RF["RoleFactory"]

    UF -->|"create()"| U["User 模型"]
    PF -->|"create()"| P["Post 模型"]
    RF -->|"create()"| R["Role 模型"]

    U -->|"hasMany"| P
    U -->|"belongsToMany"| R
```

### Has Many（一對多）

透過 `has()` 方法建立擁有「一對多」關聯的模型。

```php theme={null}
use App\Models\Post;
use App\Models\User;

// 建立擁有 3 篇 Post 的 User
$user = User::factory()
    ->has(Post::factory()->count(3))
    ->create();
```

使用魔法方法可以更精簡（`has` + 關聯名稱複數形式）。

```php theme={null}
$user = User::factory()
    ->hasPosts(3)
    ->create();

// 覆寫屬性
$user = User::factory()
    ->hasPosts(3, ['published' => false])
    ->create();
```

### Belongs To（反向關聯）

透過 `for()` 指定所屬的父模型。

```php theme={null}
use App\Models\Post;
use App\Models\User;

// 建立 3 篇 Post 且屬於指定使用者
$posts = Post::factory()
    ->count(3)
    ->for(User::factory()->state(['name' => '田中花子']))
    ->create();

// 使用既有模型作為父模型
$user = User::factory()->create();
$posts = Post::factory()->count(3)->for($user)->create();
```

魔法方法版本：

```php theme={null}
$posts = Post::factory()
    ->count(3)
    ->forUser(['name' => '田中花子'])
    ->create();
```

### Many to Many（多對多）

透過 `hasAttached()` 方法處理多對多關聯，也可指定中介資料表屬性。

```php theme={null}
use App\Models\Role;
use App\Models\User;

$user = User::factory()
    ->hasAttached(
        Role::factory()->count(3),
        ['active' => true]  // 中介資料表屬性
    )
    ->create();
```

魔法方法版本：

```php theme={null}
$user = User::factory()
    ->hasRoles(1, ['name' => 'Editor'])
    ->create();
```

### 多型關聯

與一般的「一對多」相同，可用 `has()` 或魔法方法。

```php theme={null}
use App\Models\Post;

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

`morphTo` 關聯無法使用魔法方法，須以 `for()` 明確指定關聯名稱。

```php theme={null}
$comments = Comment::factory()->count(3)->for(
    Post::factory(), 'commentable'
)->create();
```

### 工廠內定義關聯

在 `definition()` 中將外鍵指派為另一個工廠，建立模型時會自動一起建立父模型。

```php theme={null}
class PostFactory extends Factory
{
    public function definition(): array
    {
        return [
            'user_id' => \App\Models\User::factory(),
            'title' => fake()->sentence(),
            'content' => fake()->paragraph(),
        ];
    }
}
```

### recycle() — 重複利用既有模型

若要在多個關聯中重複利用同一個模型實例，可使用 `recycle()`。

```php theme={null}
// 讓 Ticket 與 Flight 共用同一個 Airline
Ticket::factory()
    ->recycle(Airline::factory()->create())
    ->create();

// 從集合隨機挑選
$airlines = Airline::factory()->count(3)->create();
Ticket::factory()->count(10)->recycle($airlines)->create();
```

## Seeding 中的使用

在 `DatabaseSeeder` 或 seeder 類別中呼叫工廠。

```php theme={null}
namespace Database\Seeders;

use App\Models\User;
use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    public function run(): void
    {
        // 建立 10 位使用者，每位都有 3 篇貼文
        User::factory()
            ->count(10)
            ->hasPosts(3)
            ->create();
    }
}
```

執行 seeder：

```shell theme={null}
php artisan db:seed
```

## 測試中的使用

在測試中可搭配 `RefreshDatabase` trait 使用工廠。

```php theme={null}
namespace Tests\Feature;

use App\Models\User;
use App\Models\Post;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class PostTest extends TestCase
{
    use RefreshDatabase;

    public function test_user_can_view_their_posts(): void
    {
        $user = User::factory()->create();
        $posts = Post::factory()->count(3)->for($user)->create();

        $response = $this->actingAs($user)
            ->get('/dashboard');

        $response->assertOk();
        $response->assertSee($posts->first()->title);
    }

    public function test_suspended_user_cannot_post(): void
    {
        $user = User::factory()->suspended()->create();

        $response = $this->actingAs($user)
            ->post('/posts', ['title' => '測試']);

        $response->assertForbidden();
    }
}
```

`RefreshDatabase` 會在每次測試前重置資料庫，讓不同測試間資料互不干擾。

<Tip>
  在 Pest 中測試時，工廠 API 完全相同。
  請使用 `uses(RefreshDatabase::class)` 重置資料庫。
</Tip>

## 下一步

<Card title="資料庫 Seeding" icon="seedling" href="/zh-TW/seeding">
  瞭解如何結合 seeder 與工廠匯入初始資料。
</Card>

<Card title="測試入門" icon="flask" href="/zh-TW/testing">
  熟悉 Laravel 的測試功能與 RefreshDatabase trait 用法。
</Card>


## Related topics

- [資料庫 Seeding](/zh-TW/seeding.md)
- [Eloquent Bootable Traits](/zh-TW/advanced/eloquent-bootable-traits.md)
- [資料庫測試](/zh-TW/database-testing.md)
- [Contracts（契約）](/zh-TW/contracts.md)
- [PHP Attributes](/zh-TW/advanced/php-attributes.md)
