📑

Laravelのfactoryで使用されるstateメソッドに関して学ぶ。

2023/12/20に公開

始めに

https://qiita.com/advent-calendar/2023/arsaga

今携わっている案件では沢山のテストデータを作成する必要があります。
けれど私はfactoryでデータを作成する際に、効率とかを考えずに、
結構適当にデータを作成していました。
しかしそんな時stateメソッドというものを使用して、データを作成している方がおられました。
なんだか便利そうなメソッドです。
今回はそのメソッドに関して、学びたいと思います。
誰かの参考になれば幸いです。

1.そもそもstateとは?

stateメソッドを使用すると、
モデルファクトリへ任意の組み合わせで適用できる個別の変更を定義できます。
Laravelのドキュメントでは例として、
Database\Factories\UserFactoryファクトリに、
デフォルトの属性値の1つを変更するsuspended状態(一時停止)メソッドが
含まれている場合を挙げています。
suspendedメソッドを呼び出すと、stateメソッドが呼び出され、
渡されたクロージャが実行されます。
クロージャは、ファクトリによって定義された生の属性の配列を受け取り、
それを基に変更したい属性の配列を返します。
この例では、'account_status' を 'suspended' に変更しています。
言い換えれば、「ユーザーのアカウントが一時停止された状態」を表しています。
つまりFactoryでこのメソッドを呼び出すと、
自動的に一時停止されたユーザーを作成することができますね。

/**
 * ユーザーが一時停止されていることを示す
 *
 * @return \Illuminate\Database\Eloquent\Factories\Factory
 */
public function suspended()
{
    return $this->state(function (array $attributes) {
        return [
            'account_status' => 'suspended',
        ];
    });
}

またもう一つの例である、Trashed Stateでは、
モデルが論理削除(softDelete)可能な場合、
作成されるモデルが既に「論理削除」されていることを示すために、
組み込みのtrashedステートメソッドを呼び出すことができます。
trashedステートを手動で定義する必要はなく、
これはすべてのファクトリに自動的に利用可能です。

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

いちいちモデルごとに状態を指定しないで、上のように書くだけで、
モデルが論理削除された状態で作成されるので、とても便利ですね。
またstateを使用するということに関してとてもわかりやすい例になっています。

2.リレーションのデータ作成でも使用できる

また何かのフラグを立てる以外でも、stateメソッドは使用できます!
今回はクリスマスも近いですので、例としてこのようなテーブルで考えてみましょう。

それではこれまでは、このテーブルのデータをどういう風に作成していたか。
日本のサンタクロースが配る値段が1万円以上のプレゼントのデータを、
例として作成したいとします。太っ腹ですね。

class SantaClausPresentFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition()
    {
        return [
            'santa_claus_id' => SantaClaus::first()->id,
            'present_id' => Present::first()->id,
        ];
    }
}
class SantaClausPresentSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        // countryが日本(japan)のサンタクロースIDのみが対象
        $santaClausIds = SantaClaus::where('country', 'japan')->pluck('id')->toArray();
	
               // priceが一万円以上のプレゼントIDのみが対象
                $tenThousandPresentIds = Present::where('price', '>=', 10000)->pluck('id')->toArray();
		
                foreach ($santaClausIds as $santaClausId) {
            $randomPresentId = $tenThousandPresentIds[array_rand($thousandPresentIds)];
            SantaClausPresent::factory()->create([
                'santa_claus_id' => $santaClausId,
                'present_id' => $randomPresentId,
            ]);
        }
    }

Factoryでわざわざ最初のデータを持ってくる必要がないですよね……。
結局Seederでデータを作成し直していますし。
Factoryの作成損な気がします。
Seederのみで、idを組み合わせて作成することも可能といえば可能ですが、
データ構造が変更されることを考えると、Factoryを使用して、
データを生成するようにしておきたいところです。

そこで今度はFactoryでstateメソッドを使って書き換えてみます。

class SantaClausPresentFactory extends Factory
{
   /**
     * Define the model's default state.
     *
     * @return array
     */
    public function definition(): array
    {
        return [];
    }
   
   /**
     * @param SantaClaus $santaClaus
     * @param Present $present
     * @return $this
     */
      public function fromRelatedData(
       SantaClaus $santaClaus,
       Present $present
   ): self {
       return $this->state([
            'santa_claus_id' => $santaClaus->id,
            'present_id' => $present->id,
       ]);
   }
}
class SantaClausPresentSeeder extends Seeder
{   /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
      // countryが日本(japan)のみのサンタクロースを全て取得
      $japanSantaClauses = SantaClaus::where('country', 'japan')->get()
      
       // priceが一万円以上のプレゼントを全て取得
             $tenThousandPresents = Present::where('price', '>=', 10000)->get();
      
      foreach ($japanSantaClauses as $japanSantaClaus) {
          $randomPresent = $tenThousandPresents->random();
	  // stateメソッドを使用して、データを生成
          SantaClausPresent::factory()
	      ->fromRelatedData($japanSantaClaus, $randomPresent)
              ->create();
      }   
    }
}

コードとしてだいぶスッキリしたと思います。
fromRelatedDataによってリレーションのデータを作成しているということも、
わかりやすくなりました。
また今回は簡単な設定(日本のみのサンタクロースのID)でしたが、
複雑なデータを取得する必要があっても、柔軟に対応できるようになります。

3.終わりに

テストデータの作成自体は、機能を作成することに比べると重要度は下がるかもしれませんが、
動作確認と検証において大事なものになってきます。
沢山のリレーションテーブルがあるような案件では、
特に効率よくデータを作成する必要があると思います。
今後はstateメソッドを使用して、テストデータ作成を効率化できれば良いなと思います。
ここまでお読み下さりありがとうございました。
誰かの知見になれば幸いです。
また明日は、@Amisakoさんの記事になります!お楽しみに!

4.参考文献

この記事は下記の記事を参考にして、書かせていただきました。
Eloquent: Factories

Arsaga Developers Blog

Discussion