Laravel8で完成されたModelFactoryの使い方

[Laravel Advent Calendar 2020 - Qiita](https://qiita.com/advent-calendar/2020/laravel) の 8日目 の記事です。
昨日は @okdyy75 さんの[Laravelを触って1年経ったのでTIPS](https://qiita.com/okdyy75/items/e0ebde94dd0e69df1c45)の記事でした！
明日は @kaino5454 さんの[ポケモンAPIを叩いて使用率ランキングを取得してみた](https://qiita.com/kaino5454/items/e12a0214c76aea6cdc2b)の記事です！

## 概要

Laravel8からModelFactoryの機能が刷新されました。
新しい書き方についてまとめたいと思います。

- [Laravel5.5でほぼ完成されたModelFactoryの使い方(7系まで](https://qiita.com/fagai/items/7f63d651cbed3664b758)

こちらの記事をリスペクトしてタイトル名に使わせていただきました🙏🙏🙏

- [Laravel Eloquent Model Factory の今昔](https://qiita.com/nunulk/items/37cfe88b5fa999c71ea1)

こちらの記事は新しい書き方と古い書き方を比較して紹介してます。

## 予備知識

Laravelのデータベース関連の用語として、Migration、Seeder、ModelFactory、Fakerの4つあります。

### Migration(マイグレーション)

https://readouble.com/laravel/8.x/ja/migrations.html

マイグレーションはデータベースのテーブルの定義を書きます。
英語で「移動・移行」という意味になります。

カラムの作成、変更、削除、索引の追加、外部キー制約の追加等行えます。
そういえったテーブル変遷をソースコードで管理できます。

またロールバック機能もあり、過去の状態に戻すことができます。
(変更と反対の定義を書いていく必要はあります。)

新しいメンバーが参画した際にも簡単に同じテーブルの状態を再現できます。

#### artisan コマンド使用例

```
$ php artisan make:migration create_posts_table # マイグレーションのテンプレート生成
$ php artisan migrate # マイグレーション実行
$ php artisan migrate:fresh # 全テーブル削除、全マイグレーション再実行
$ php artisan migrate:fresh --seed # シーディングも行う
```

同じ名前のマイグレーションファイルはエラーになるので、注意して命名していく必要があります。

1ファイルで複数テーブルのマイグレーション定義は書かないほうが良いです。
理由としては問題が発生してロールバックする際に手間が増えたり、ファイル名で変更内容が一致しなくなるためです。
開発中のマイグレーションが頻発して変わる時期であればまとめて定義してもいいかなと思います。

公式でもLaravel8から[マイグレーションスカッシング機能](https://qiita.com/ucan-lab/items/2f00191dcf8371347d13)が用意されてます。

### Seeder(シーダー)

https://readouble.com/laravel/8.x/ja/seeding.html

シーダーはデータベースにダミーデータを一斉に挿入できる機能です。
英語で「種をまく人」という意味になります。(Seeding/シーディングとも表記されます)

#### artisan コマンド使用例

```shell-session
$ php artisan make:seeder PostsSeeder # シーダーのテンプレート生成
$ php artisan db:seed # DatabaseSeeder を実行
$ php artisan db:seed --class=PostsSeeder # クラス指定して実行
```

テーブル単位やデータのまとまり毎に書くと良いでしょう。

### ModelFactory(モデルファクトリ)

https://readouble.com/laravel/8.x/ja/database-testing.html#creating-factories

モデルファクトリはEloquentモデルの各フィールドに入る値を定義します。
モデルの工場といったところです。

主にシーダーやテストコードからモデルファクトリーは呼び出されます。

#### artisan コマンド使用例

```shell-session
$ php artisan make:factory PostFactory # モデルファクトリのテンプレート生成
```

### Faker(フェイカー)

- https://github.com/fakerphp/faker
- https://fakerphp.github.io
  - 詳細な使い方はこちら

フェイカーはダミーデータ生成用のライブラリです。
主にモデルファクトリを定義する際に使用します。

名前や住所、メールアドレスといったよく使われるテストデータをランダムに生成してくれる便利な機能です。

#### 補足: fzaninotto/Fakerのアーカイブ

最近の話(2020年10月21日)ですが、 [fzaninotto/Faker](https://github.com/fzaninotto/Faker) の本家のリポジトリがアーカイブされています。
詳細については、下記のリンクをご参照ください。

- https://github.com/fzaninotto/Faker
- https://marmelab.com/blog/2020/10/21/sunsetting-faker.html
- https://twitter.com/taylorotwell/status/1321091021342650377

今後は[フォーク先のリポジトリ](https://github.com/fakerphp/faker)でメンテナンスが続けられるそうです。
また、こちらの[プルリクエスト](https://github.com/laravel/framework/pull/33388/files#diff-d2ab9925cad7eac58e0ff4cc0d251a937ecf49e4b6bf57f8b95aab76648a9d34R125) で既にライブラリの変更が行われてます。

- v8.12.0 (2020-10-29)
- v7.29.0 (2020-10-29)
- v6.20.0 (2020-10-28)

上記のバージョン以降のLaravelをご利用であればフォーク先のリポジトリが使用されます。

## tinkerでお試し実行方法

ModelFactory, Fakerはtinker上で簡単にお試しできます。
どんなテストデータが生成されるのか気になる場合は、tinkerで試してみましょう！

### ModelFactoryのお試し実行方法

```shell-session
$ php artisan tinker

# make() だとインスタンスのみ返す
>>> App\Models\User::factory()->make()
=> App\Models\User {#3385
     name: "Miss Dolores Mueller III",
     email: "marcus19@example.org",
     email_verified_at: "2020-12-06 14:58:34",
   }

# create() だとデータベースに挿入してインスタンスを返す
>>> App\Models\User::factory()->create()
=> App\Models\User {#3387
     name: "Annabel Upton III",
     email: "kenton.klocko@example.org",
     email_verified_at: "2020-12-06 14:58:39",
     updated_at: "2020-12-06 14:58:39",
     created_at: "2020-12-06 14:58:39",
     id: 1,
   }
```

### Fakerのお試し実行方法

詳しい使い方はこちら https://fakerphp.github.io

```shell-session
$ php artisan tinker
>>> $faker = app()->make(Faker\Generator::class)

>>> $faker->name()
=> "中津川 和也"

# () なしでメソッドではなくプロパティへのアクセスでも実行可能
>>> $faker->name
=> "佐々木 学"

>>> $faker->safeEmail()
=> "kondo.yui@example.com"

>>> $faker->emoji()
=> "😔"

>>> $faker->imageUrl()
=> "https://via.placeholder.com/640x480.png/0066ee?text=aut"

>>> $faker->dateTimeBetween('-2weeks', '9 days')->format('Y-m-d H:i:s')
=> "2020-11-28 05:24:51"
```

- [Laravel tinker で Faker を試す方法](https://qiita.com/ucan-lab/items/bc421788d4a352afc6ea)

## 設定

### Faker 日本語化

`config/app.php` の `faker_locale` の値を変更してください。

```php:config/app.php
    /*
    |--------------------------------------------------------------------------
    | Faker Locale
    |--------------------------------------------------------------------------
    |
    | This locale will be used by the Faker PHP library when generating fake
    | data for your database seeds. For example, this will be used to get
    | localized telephone numbers, street address information and more.
    |
    */

//    'faker_locale' => 'en_US',
    'faker_locale' => 'ja_JP',
```

実行すると日本語名のデータが入るようになります。

```shell-session
>>> App\Models\User::factory()->make()->name
=> "田中 桃子"
```

---

# 本題

横道に逸れてしまいましたが、ここからが本題のModelFactoryです。

## シーダーからテストデータを挿入する

シーダーからテストデータをデータベースに挿入したい場合、こんな感じで生成することができます。

```php:database/seeders/DatabaseSeeder.php
<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;

class DatabaseSeeder extends Seeder
{
    public function run(): void
    {
        DB::table('users')->insert([
            'name' => Str::random(10),
            'email' => Str::random(10).'@gmail.com',
            'password' => Hash::make('password'),
        ]);
    }
}
```

数件程度であれば良いですが、開発が進むにつれて複雑なテストデータを作ろうとしていくとメンテナンスがとても大変になってしまいます...。

## デフォルトで用意されているモデル

```php:app/Models/User.php
<?php

namespace App\Models;

use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

class User extends Authenticatable
{
    use HasFactory, Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name',
        'email',
        'password',
    ];

    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
    protected $hidden = [
        'password',
        'remember_token',
    ];

    /**
     * The attributes that should be cast to native types.
     *
     * @var array
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
    ];
}
```

`Illuminate\Database\Eloquent\Factories\HasFactory` trait を use する必要があります。

## デフォルトで用意されているモデルファクトリ

```php:database/factories/UserFactory.php
<?php

namespace Database\Factories;

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

class UserFactory extends Factory
{
    /**
     * The name of the factory's corresponding model.
     *
     * @var string
     */
    protected $model = User::class;

    /**
     * Define the model's default state.
     *
     * @return array
     */
    public function definition()
    {
        return [
            'name' => $this->faker->name,
            'email' => $this->faker->unique()->safeEmail,
            'email_verified_at' => now(),
            'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
            'remember_token' => Str::random(10),
        ];
    }
}
```

### tinker からモデルファクトリーを呼び出して使う

```shell-session
$ php artisan tinker

App\Models\User::factory()->create()
=> App\Models\User {#3380
     name: "近藤 結衣",
     email: "uno.naoki@example.net",
     email_verified_at: "2020-12-06 15:43:28",
     updated_at: "2020-12-06 15:43:28",
     created_at: "2020-12-06 15:43:28",
     id: 3,
   }

// name 属性を上書きする
App\Models\User::factory()->create(['name' => 'ucan'])
=> App\Models\User {#4264
     name: "ucan",
     email: "sato.yuki@example.com",
     email_verified_at: "2020-12-06 15:43:46",
     updated_at: "2020-12-06 15:43:46",
     created_at: "2020-12-06 15:43:46",
     id: 4,
   }
```

- モデルファクトリを使うと[複数代入の保護](https://readouble.com/laravel/8.x/ja/eloquent.html#mass-assignment)を自動的に無効化します。

## Laravel7との違い

```php:database/factories/UserFactory.php
<?php

/** @var \Illuminate\Database\Eloquent\Factory $factory */

use App\User;
use Faker\Generator as Faker;
use Illuminate\Support\Str;

/*
|--------------------------------------------------------------------------
| Model Factories
|--------------------------------------------------------------------------
|
| This directory should contain each of the model factory definitions for
| your application. Factories provide a convenient way to generate new
| model instances for testing / seeding your application's database.
|
*/

$factory->define(User::class, function (Faker $faker) {
    return [
        'name' => $faker->name,
        'email' => $faker->unique()->safeEmail,
        'email_verified_at' => now(),
        'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
        'remember_token' => Str::random(10),
    ];
});
```

呼び出し方

```
factory(App\User::class)->create();
```

以前は `$factory->define()` 関数を使って登録、 `factory()` 関数を使って呼び出ししていく形でしたが、Laravel 8からクラスベースとなりました。

## 基本の流れ

### posts テーブル

テーブルがないとテストデータを入れれないので、下記のようにマイグレーションファイルを作っておきます。

```shell-session
$ php artisan make:migration create_posts_table
```

`database/migrations/YYYY_MM_DD_XXXXXX_create_posts_table.php` といったマイグレーションファイルが生成されるので、下記のように追記しておきます。

```php:database/migrations/2020_12_08_000000_create_posts_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreatePostsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->foreignId('user_id');
            $table->string('title');
            $table->text('content');
            $table->string('published_at')->nullable();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('posts');
    }
}
```

```shell-session
$ php artisan migrate
```

### モデル、モデルファクトリークラスの生成

新しく自分でファイルを作成しても良いですが、テンプレートを生成する便利コマンドがあるので使いましょう。

```shell-session
# モデルとファクトリー個別に生成する
$ php artisan make:model Post
$ php artisan make:factory PostFactory --model=Post

or

# モデルとファクトリーを生成する
$ php artisan make:model Post --factory
$ php artisan make:model Post -f # 省略オプション

or

# モデルとファクトリー、マイグレーション、シーダー、コントローラを生成する
$ php artisan make:model Post --all
$ php artisan make:model Post -a # 省略オプション
```

#### 生成されるファイル

- `app/Models/Post.php`
- `database/factories/PostFactory.php`

```php:app/Models/Post.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    use HasFactory;
}
```

```php:database/factories/PostFactory.php
<?php

namespace Database\Factories;

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

class PostFactory extends Factory
{
    /**
     * The name of the factory's corresponding model.
     *
     * @var string
     */
    protected $model = Post::class;

    /**
     * Define the model's default state.
     *
     * @return array
     */
    public function definition()
    {
        return [
            //
        ];
    }
}
```

`database/factories/PostFactory.php` の `definition()` を書き換えてみます。

```php:database/factories/PostFactory.php
    public function definition()
    {
        return [
            'user_id' => \App\Models\User::factory(),
            'title' => $this->faker->title,
            'content' => $this->faker->paragraph,
        ];
    }
```

シーダーへ追記します。

```php:database/seeders/DatabaseSeeder.php
    public function run(): void
    {
        \App\Models\Post::factory()->count(3)->create();
    }
```

```
$ php artisan migrate:fresh --seed
$ php artisan tinker

>>> App\Models\User::count();
=> 3

>>> App\Models\Post::count();
=> 3

>>> App\Models\User::first();
=> App\Models\User {#4044
     id: 1,
     name: "斉藤 里佳",
     email: "yamamoto.asuka@example.org",
     email_verified_at: "2020-12-06 16:43:25",
     created_at: "2020-12-06 16:43:25",
     updated_at: "2020-12-06 16:43:25",
   }

>>> App\Models\Post::first();
=> App\Models\Post {#3639
     id: 1,
     user_id: 1,
     title: "Dr.",
     content: "Aspernatur maiores qui neque iure cupiditate. Facilis voluptas consequatur mollitia placeat ea. Aperiam neque qui quae placeat debitis nobis minima.",
     published_at: null,
     created_at: "2020-12-06 16:43:25",
     updated_at: "2020-12-06 16:43:25",
   }
```

Laravelではこんな流れでテストデータを作成していきます。

## 詳細な使い方

https://readouble.com/laravel/8.x/ja/database-testing.html

公式ドキュメントに詳しい使い方が記載されているので詳細はこちらを参考すると良いです。

## 補足

公式ドキュメントに書かれてないけど、知ってると便利そうなことをいくつかご紹介します。

### Fakerに独自のメソッドを追加する

`FakerServiceProvider` を作成します。

```shell-session
$ php artisan make:provider FakerServiceProvider
```

`FakerServiceProvider` を登録します。

```php:config/app.php
    'providers' => [
        App\Providers\FakerServiceProvider::class,
    ],
```

`FakerServiceProvider` を登録します。

```php:app/Providers/FakerServiceProvider.php
<?php declare(strict_types=1);

namespace App\Providers;

use App\Faker\Planet;
use Faker\Generator;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Support\ServiceProvider;

class FakerServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap services.
     *
     * @return void
     * @throws BindingResolutionException
     */
    public function boot(): void
    {
        $faker = $this->app->make(Generator::class);
        $faker->addProvider(new Planet($faker));
    }
}
```

```php:app/Faker/Planet.php
<?php declare(strict_types=1);

namespace App\Faker;

use Faker\Provider\Base;

/**
 * 惑星をランダムに返す
 */
class Planet extends Base
{
    protected array $planet = [
        '水星',
        '金星',
        '地球',
        '火星',
        '木星',
        '土星',
        '天王星',
        '海王星',
    ];

    /**
     * @return string
     */
    public function planet(): string
    {
        return $this->generator->randomElement($this->planet);
    }
}
```

```shell-session
$ php artisan tinker
>>> $faker = app()->make(Faker\Generator::class)

>>> $faker->planet()
=> "海王星"
```

### モデルファクトリーのリレーション定義

- [ファクトリのリレーション | Laravel 8.x データベースのテスト](https://readouble.com/laravel/8.x/ja/database-testing.html#factory-states)

ファクトリークラスにリレーションの定義も行えます。
ただ私個人としては、ファクトリークラスはなるべくシンプルに保ち、シーダークラスに生成ルールを書いた方が読みやすいかなと思ってます。

### 連番を生成する

連番のテストデータを作りたい時は、staticなプロパティを作ると良いです。

```php
class UserFactory extends Factory
{
    private static $sequence = 1;

    public function definition()
    {
        return [
            'name' => sprintf('No. %d %s', self::$sequence++, $this->faker->name),
        ];
    }
```

実行するとこんな感じです。

```php
>>> App\Models\User::factory()->make()->name
=> "No. 1 青山 明美"

>>> App\Models\User::factory()->make()->name
=> "No. 2 工藤 七夏"

>>> App\Models\User::factory()->make()->name
=> "No. 3 渡辺 稔"
```

### $faker->email は使うな！ $faker->safeEmail を使え！

```shell-session
$ php artisan tinker
>>> $faker = app()->make(Faker\Generator::class)

>>> $faker->email
=> "kyosuke.sasada@gmail.com"

>>> $faker->safeEmail
=> "kondo.yui@example.com"
```

実行結果を見るとわかりますが、 `@gmail.com` など実在するドメインのテストデータが生成されます。
開発環境でメールの誤送信を防止するため `$faker->safeEmail` を使うようにしましょう。

### Faker nullデータをランダムで入れたい

`->optional()` を使うとランダムでnull値が入ります！

```php
    public function definition()
    {
        return [
            // ランダムでnullを入れたい時
            'completed_at' => $this->faker->boolean ? $this->faker->dateTime : null,
            // Fakerのoptional()を使うとランダムでnullを入れてくれます(オススメ)
            'completed_at' => $this->faker->optional()->dateTime,
            // 80%の確率で日付データが入り、20%の確率でnullが入ります
            'completed_at' => $this->faker->optional(0.8)->dateTime,
        ];
    }
```

### 状態に応じたテストデータを作成する

- [ファクトリステート](https://readouble.com/laravel/8.x/ja/database-testing.html#factory-states)

公式サイトに詳しく記述されているので、ここで書く必要ないですがとても便利な機能なのでご紹介します。

※お試しする場合はマイグレーションファイルに下記を追記して、`php artisan migrate:fresh` してください。

```php:database/migrations/2014_10_12_000000_create_users_table.php
            $table->string('account_status');
```

```php:database/factories/UserFactory.php
<?php

namespace Database\Factories;

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

class UserFactory extends Factory
{
    /**
     * The name of the factory's corresponding model.
     *
     * @var string
     */
    protected $model = User::class;

    /**
     * Define the model's default state.
     *
     * @return array
     */
    public function definition()
    {
        return [
            'name' => $this->faker->name,
            'account_status' => 'init',
            'email' => $this->faker->unique()->safeEmail,
            'email_verified_at' => now(),
            'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
            'remember_token' => Str::random(10),
        ];
    }

    /**
     * そのユーザーが有効化(activated)されていることを表す
     *
     * @return Factory
     */
    public function activated(): Factory
    {
        return $this->state(function () {
            return [
                'account_status' => 'activated',
            ];
        });
    }

    /**
     * そのユーザーが資格保留(suspended)されていることを表す
     *
     * @return Factory
     */
    public function suspended(): Factory
    {
        return $this->state(function () {
            return [
                'account_status' => 'suspended',
            ];
        });
    }
}
```

```shell-session
$ php artisan tinker

# 仮登録(init)状態のユーザーを作成
>>> App\Models\User::factory()->create()
=> App\Models\User {#3346
     name: "中津川 あすか",
     account_status: "init",
     email: "tanaka.momoko@example.org",
     email_verified_at: "2020-12-06 18:30:21",
     updated_at: "2020-12-06 18:30:21",
     created_at: "2020-12-06 18:30:21",
     id: 7,
   }

# 有効化(activated)状態のユーザーを作成
>>> App\Models\User::factory()->activated()->create()
=> App\Models\User {#3343
     name: "石田 京助",
     account_status: "activated",
     email: "harada.yuta@example.com",
     email_verified_at: "2020-12-06 18:30:23",
     updated_at: "2020-12-06 18:30:23",
     created_at: "2020-12-06 18:30:23",
     id: 8,
   }

# 資格保留(activated)状態のユーザーを作成
>>> App\Models\User::factory()->suspended()->create()
=> App\Models\User {#4090
     name: "伊藤 稔",
     account_status: "suspended",
     email: "taichi.sugiyama@example.com",
     email_verified_at: "2020-12-06 18:30:24",
     updated_at: "2020-12-06 18:30:24",
     created_at: "2020-12-06 18:30:24",
     id: 9,
   }
```

モデルの状態によって必要な埋めたいデータは異なるので、状態に応じてモデルファクトリを定義できるのはとても便利です！

### db:seed でテストデータをクリアする

先週書いた記事ですが、こちらもやっておくと便利です。

- [Laravel 全テーブルをTruncateしてからシーディングする方法](https://qiita.com/ucan-lab/items/55ec5c341fb5c228fe14)

### Laravel8 シーダー、モデルファクトリー 配列から属性を上書きしてデータを作成する

新しい記事を書きました！

- [Laravel8 シーダー、モデルファクトリー 配列から属性を上書きしてデータを作成する](https://qiita.com/ucan-lab/items/ddec1f2ec85901006bdd)

### Laravel 8 のリリース記事

Laravel 8の新しい機能を知りたい方はこちらの記事をご覧ください。

- [Laravel 8 Release !](https://qiita.com/ucan-lab/items/e329af0bd01dda179d31#%E3%83%A2%E3%83%87%E3%83%AB%E3%83%95%E3%82%A1%E3%82%AF%E3%83%88%E3%83%AA%E3%82%AF%E3%83%A9%E3%82%B9)