Laravel でポリモフィック関連の Factory を作成してみた

## はじめに

実務でポリモフィック関連を扱うことになり、

「ポリモフィック関連ってなんや？」

となったので、思考整理のためにこちらの記事を作成しました。

:::note warn
ポリモフィック関連がアンチパターンということは理解しています。
今後キャッチアップしたいと思いますm(_ _)m
ご理解いただけますと幸いです。
:::

もし、「間違って理解しているよ」という部分がありましたら、**優しく** 教えてくださると嬉しいです。



## この記事でできるようになること

:::note info
- ポリモフィック関連を理解できる
- Laravel で１対１と１対多のポリモフィック関連を使える
:::



## ポリモフィック関連とは

子供が複数の親を持ち、互いに参照するすることができるものです。


例えば、  
ブログの画像と投稿とユーザーを想像してみて下さい。

画像（子）が、投稿（親）とユーザー（親）を持っている。ような関係になります。

もう少し具体的に表現すると

- ある投稿に紐付いている画像
- あるユーザーに紐付いている画像

というふうに複数の親に子が紐付いているのがポリモフィック関連になります。


ER図を見ていただけると理解しやすいかと思います。

```plantuml
@startuml PolymorphicOneToOneTables
entity posts as "posts\n[投稿（親）]" {
  + id [PK]
  --
  user_id [FK]
  title [タイトル]
  body [本文]
  created_at [作成日]
  updated_at [更新日]
}

entity users as "users\n[ユーザー（親）]" {
  + id [PK]
  --
  name [名前]
  email [メールアドレス]
  email_verified_at [確認用メールアドレス]
  password [パスワード]
  created_at [作成日]
  updated_at [更新日]
}

entity images as "images\n[画像（子）]" {
  + id [PK]
  --
  url [URL]
  imageable_id
  imageable_type
}

posts ||-d-o| images
users ||-d-o| images

@enduml
```



## 実装の流れ

ざっくりとポリモフィック関連が理解できたかと思いますので、  
次のような流れでポリモフィック関連の Factory を作って仮データを作成したいと思います。

1. Migration を作成
2. Migration にカラムと型を追加
3. Model を作成
4. Model にポリモフィック関連先のデータを取得するメソッド実装
5. Factory を作成
6. Seeder を作成
7. DB を確認

## 1対１のポリモフィック関連の場合

冒頭に記載してた画像、投稿、ユーザーをサンプルにして作成します。

関係性は以下の通りです

- 画像（子）と投稿（親）の関係
  - ある画像（子）は１つの投稿（親）に所属している
  - ある投稿（親）は１つの画像（子）をもっている
- 画像（子）とユーザー（親）の関係
  - ある画像（子）はあるユーザー（親）に所属している
  - あるユーザー（親）は１つの画像（子）をもっている

ER図で表すとこんな感じです（冒頭のER図と同じです）

```plantuml
@startuml PolymorphicOneToOneTables
entity posts as "posts\n[投稿（親）]" {
  + id [PK]
  --
  user_id [FK]
  title [タイトル]
  body [本文]
  created_at [作成日]
  updated_at [更新日]
}

entity users as "users\n[ユーザー（親）]" {
  + id [PK]
  --
  name [名前]
  email [メールアドレス]
  email_verified_at [確認用メールアドレス]
  password [パスワード]
  created_at [作成日]
  updated_at [更新日]
}

entity images as "images\n[画像（子）]" {
  + id [PK]
  --
  url [URL]
  imageable_id
  imageable_type
}

posts ||-d-o| images
users ||-d-o| images

@enduml
```

### Migration 作成

下記のコマンドで posts, users, images の　マイグレーションを作成し、カラムを追加

```zsh:
php artisan make:migration create_tablename_table
```

#### posts_table （親）

```php:○○○○_○○_○○_○○○○○○_create_posts_table.php
<?php

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

return new class extends Migration
{
    public function up()
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id()->comment('id');
            $table->string('title', 100)->comment('タイトル');
            $table->text('body')->comment('本文');
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('posts');
    }
};
```

#### users_table （親）

```php:○○○○_○○_○○_○○○○○○_create_users_table.php
<?php

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

return new class extends Migration
{
    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->id();
            $table->string('name')->comment('名前');
            $table->string('email')->unique()->comment('メールアドレス');
            $table->timestamp('email_verified_at')->nullable()->comment('確認用メールアドレス');
            $table->string('password')->comment('パスワード');
            $table->rememberToken();
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('users');
    }
};
```

#### images_table （子）

```php:○○○○_○○_○○_○○○○○○_create_images_table.php
<?php

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

return new class extends Migration
{
    public function up()
    {
        Schema::create('images', function (Blueprint $table) {
            $table->id()->comment('id');
            $table->string('url')->comment('URL');
            $table->unsignedBigInteger('imageable_id')->comment('imageableのid');
            $table->string('imageable_type')->comment('imageableのtype');
            $table->timestamps();

        });
    }

    public function down()
    {
        Schema::dropIfExists('images');
    }
};
```

親の `posts table` や `users table` は特に変わったカラムはありませんが、  
`images table` に `imageable_id` や `imageable_type` といったカラムが存在します。


この `imageable_id` や `imageable_type` が親の情報になります。

:::note info
- imageable_id: 親の id を取得
- imageable_type: 親のリレーション先のクラスを取得
:::


詳細は後ほど説明しますが、DBでは下記のようなデータとなります

![morphOnePost.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/756415/8600ba00-aec5-5106-a51f-635c67aad64e.jpeg)

![morphOne.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/756415/bd863cb8-be10-928e-79d0-4e169f2ae28c.jpeg)

:::note warn
※ 今回は image テーブルのため `imageable_〇〇` としましたが、任意のカラム名を命名すれば OK です 
:::

### Model 作成

下記のコマンドで Post, User, Image の　Model を作成し、  
ポリモフィック関連先のデータを取得するメソッド実装

```zsh:
php artisan make:model Model名 
```

#### PostModel （親） 

```php:Post.php
<?php declare(strict_types=1);

namespace App\Models;

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

final class Post extends Model
{
    use HasFactory;

    protected $fillable = [
        'title',
        'url',
    ];
    /**
     * 投稿の画像を取得
     */
    public function image(): MorphOne
    {
        return $this->morphOne(Image::class, 'imageable');
    }
}
```

#### UserModel （親）

```php:User.php
<?php

declare(strict_types=1);

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\MorphOne;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;

final class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable;

    protected $fillable = [
        'name',
        'email',
        'password',
    ];

    protected $hidden = [
        'password',
        'remember_token',
    ];

    protected $casts = [
        'email_verified_at' => 'datetime',
    ];

    /**
     * ユーザーの画像を取得
     */
    public function image(): MorphOne
    {
        return $this->morphOne(Image::class, 'imageable');
    }
}
```

#### ImageModel （子）

```php:Image.php
<?php
declare(strict_types=1);

namespace App\Models;

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

final class Image extends Model
{
    use HasFactory;

    protected $fillable = [
        'url',
        'imageable_id',
        'imageable_type',
    ];

    /**
     * 親のimageableなモデル（ユーザー／投稿）の取得
     */
    public function imageable(): MorphTo
    {
        return $this->morphTo();
    }
}
```

各モデルに見慣れない `morphOne` や `morphTo` というメソッドを使っていますが、  
これらのメソッドでポリモフィック関連先のデータを取得することができます。

親モデルに下記の `親から子のデータを取得するメソッド` を記述します。

```php:
    public function image(): MorphOne
    {
        return $this->morphOne(Image::class, 'imageable');
    }
```

子モデルに下記の `子から親のデータを取得するメソッド` を記述します。

```php:
    public function imageable(): MorphTo
    {
        return $this->morphTo();
    }
```

これだけでポリモフィック関連先のデータを取得することができます。

簡単ですね。


### Factory 作成

下記のコマンドで Post, User, Image の　Factory を作成し、  
ポリモフィック関連先のデータを取得するメソッド実装

```zsh:
php artisan make:factory Model名
```

#### PostFactory （親）

```php:PostFactory.php
<?php

declare(strict_types=1);

namespace Database\Factories;

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

final class PostFactory extends Factory
{
    protected $model = Post::class;

    public function definition(): array
    {
        return [
            'title'      => $this->faker->title(),
            'body'       => $this->faker->text(),
            'created_at' => CarbonImmutable::now(),
            'updated_at' => CarbonImmutable::now(),
        ];
    }
}
```

#### UserFactory （親）

```php:UserFactory.php
<?php declare(strict_types=1);

namespace Database\Factories;

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

final class UserFactory extends Factory
{
    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),
        ];
    }

    public function unverified()
    {
        return $this->state(function (array $attributes) {
            return [
                'email_verified_at' => null,
            ];
        });
    }
}
```

#### ImageFactory （子）

```php:ImageFactory.php
<?php declare(strict_types=1);

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;

final class ImageFactory extends Factory
{
    public function definition()
    {
        return [
            'url' => $this->faker->url(),
        ];
    }
}
```

### Seeder 作成

Factory を用いてい Seed データを作成します

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

declare(strict_types=1);

namespace Database\Seeders;

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

final class DatabaseSeeder extends Seeder
{
    public function run(): void
    {
        // Post
        Post::factory()
            ->count(3)
            ->has(Image::factory())
//            ->hasImages(3) //  ← こちらの表現でも同じように factory 作成できるできる
            ->create();

        // User
        User::factory()
            ->count(3)
            ->has(Image::factory())
            ->create();

        // Image
        Image::factory()
            ->count(3)
            ->for(
                Post::factory(),
                'imageable'
            )->create();

        Image::factory()
            ->count(3)->for(
                User::factory(),
                'imageable'
            )->create();
    }
}
```


#### 親の Factory からポリモフィック関連の子データを作成する場合

```php:
        Post::factory()
            ->count(3)
            ->has(Image::factory())
            ->create();
```

この場合は、Post と Image を3つずつ作成します。


↓↓↓ Post のデータ ↓↓↓
![morphOnePost.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/756415/3b0c4e8d-524e-f354-f3be-a78f50c5eb2c.jpeg)


↓↓↓ Image のデータ ↓↓↓
![morphOneImage.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/756415/dc922aa9-4e3c-af71-154d-4e57d63ec08b.jpeg)

- imageable_id: 親の id を取得
- imageable_type: 親のリレーション先のクラスを取得

となります。

imageable_id は Post の id となっています。


#### 子の Factory からポリモフィック関連の親データを作成する場合

```php:
        Image::factory()
            ->count(3)
            ->for(
                Post::factory(),
                'imageable'
            )->create();
```

こ場合は、ある Post に紐づく Image レコードを３つ作成します

なので、Post のレコードは１つになります。

↓↓↓ Post のデータ ↓↓↓
![morphOnePost2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/756415/aaf421dd-41b5-420e-d050-bbe2863cdbdf.jpeg)


↓↓↓ Image のデータ ↓↓↓
![morohpOneImage2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/756415/5dfb6028-fe18-1f00-469b-b4530d67c2b1.jpeg)


## 1対多のポリモフィック関連の場合

Model のポリモフィック関連先のデータを取得するメソッドの記述方法が違うくらいで、  
実装大きな流れは 1対1 のときと同じです。

１対多は、動画と投稿のコメントを例とします。

- コメント（子）と動画（親）の関係
  - あるコメント（子）はある動画（親）に所属している
  - ある動画（親）は複数のコメント（子）をもっている
- コメント（子）と投稿（親）の関係
  - あるコメント（子）はある投稿ー（親）に所属している
  - ある投稿（親）は複数のコメント（子）をもっている

ER図で表すとこんな感じです

```plantuml
@startuml PolymorphicOneToManyTable
entity posts as "posts\n[投稿（親）]" {
  + id [PK]
  --
  title [タイトル]
  body [本文]
  created_at [作成日]
  updated_at [更新日]
}

entity videos as "videos\n[動画（親）]" {
  + id [PK]
  --
  title [タイトル]
  url [URL]
  created_at [作成日]
  updated_at [更新日]
}

entity comments as "comments\n[コメント（子）]" {
  + id [PK]
  --
  body [本文]
  commentable_id
  commentable_type
}

posts ||-d-o{ comments
videos ||-d-o{ comments
@enduml

```


### Migration 作成

下記のコマンドで videos, comments の　マイグレーションを作成し、カラムを追加
※ posts は上記と同じため省略

```zsh:
php artisan make:migration create_tablename_table
```

#### videos_table （親）

```php:○○○○_○○_○○_○○○○○○_create_videos_table.php
<?php

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

return new class extends Migration
{
    public function up()
    {
        Schema::create('videos', function (Blueprint $table) {
            $table->id()->comment('id');
            $table->string('title', 100)->comment('タイトル');
            $table->string('url', 100)->comment('URL');
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('videos');
    }
};

```


#### comments_table （子）

```php:○○○○_○○_○○_○○○○○○_create_comments_table.php
<?php

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

return new class extends Migration
{
    public function up()
    {
        Schema::create('comments', function (Blueprint $table) {
            $table->id()->comment('id');
            $table->text('body')->comment('本文');
            $table->unsignedBigInteger('commentable_id')->comment('commentableのid');
            $table->string('commentable_type')->comment('commentableのtype');
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('comments');
    }
};
```

comment のポリモフィック関連なので `commentable_id` や `commetnable_type` とします。


### Model 作成

下記のコマンドで Video, Post, Comment の　Model を作成し、  
ポリモフィック関連先のデータを取得するメソッド実装

```zsh:
php artisan make:model Model名 
```

#### VideoModel （親） 

```php:Post.php
<?php

declare(strict_types=1);

namespace App\Models;

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

final class Video extends Model
{
    use HasFactory;

    protected $fillable = [
        'title',
        'url',
    ];

    /**
     * このビデオの全コメント取得
     */
    public function comments(): MorphMany
    {
        return $this->morphMany(Comment::class, 'commentable');
    }
}
```

#### PostModel （子）

```php:Post.php
<?php
declare(strict_types=1);

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Database\Eloquent\Relations\MorphOne;

final class Post extends Model
{
    use HasFactory;

    protected $fillable = [
        'title',
        'url',
    ];
  
    /**
     * このポストの全コメント取得
     */
    public function comments(): MorphMany
    {
        return $this->morphMany(Comment::class, 'commentable');
    }
}
```

#### CommentModel （子）

```php:Comment.php
<?php

declare(strict_types=1);

namespace App\Models;

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

final class Comment extends Model
{
    use HasFactory;

    protected $fillable = [
        'body',
        'commentable_id',
        'commentable_type',
    ];

    /**
     * ポリモフィック関連先の全コメント取得
     */
    public function commentable(): MorphTo
    {
        return $this->morphTo();
    }
}
```

1対多は、1対1 のときと異なり、`morphOne` の部分が `morphMany` となっています。  
それ以外は、1対1 の記述方法と同じです。


１対１のとき
```php:
    public function image(): MorphOne
    {
        return $this->morphOne(Image::class, 'imageable');
    }
```

1対多のとき

```php:
    public function comments(): MorphMany
    {
        return $this->morphMany(Comment::class, 'commentable');
    }
```


### Factory 作成

下記のコマンドで Video, Comment の　Factory を作成し、  
ポリモフィック関連先のデータを取得するメソッド実装
※ PostFactory は１対１と同じため省略

```zsh:
php artisan make:factory Model名
```

#### VideoFactory （親）

```php:UserFactory.php
<?php declare(strict_types=1);

namespace Database\Factories;

use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Factories\Factory;

final class VideoFactory extends Factory
{
    public function definition()
    {
        return [
            'title'      => $this->faker->title(),
            'url'        => $this->faker->url(),
            'created_at' => CarbonImmutable::now(),
            'updated_at' => CarbonImmutable::now(),
        ];
    }
}
```

#### CommentFactory （子）

```php:CommentFactory.php
<?php declare(strict_types=1);

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;

final class CommentFactory extends Factory
{
    public function definition()
    {
        return [
            'body' => $this->faker->text(),
            'created_at' => now(),
            'updated_at' => now(),
        ];
    }
}
```

### Seeder 作成

Factory を用いてい Seed データを作成します

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

declare(strict_types=1);

namespace Database\Seeders;

use App\Models\Comment;
use App\Models\Post;
use App\Models\Video;
use Illuminate\Database\Seeder;

final class DatabaseSeeder extends Seeder
{
    public function run(): void
    {
        // Post
        Post::factory()
            ->count(3)
            ->has(Comment::factory())
//            ->hasComments(3) //  ← こちらの表現でも同じように factory 作成できるできる
            ->create();

        // Video
        Video::factory()
            ->count(3)
            ->has(Comment::factory())
            ->create();
//
        // Comment
        Comment::factory()->for(
            Post::factory(), 
            'commentable'
        )->create();

        Comment::factory()->for(
            Video::factory(), 
            'commentable'
        )->create();
    }
}
```


#### 親の Factory からポリモフィック関連の子データを作成する場合

```php:
        Video::factory()
            ->count(3)
            ->has(Comment::factory())
            ->create();
```

### Migration と Seed データを実行

```zsh:
php artisan migrate:fresh --seed
```

↓↓↓ Video のデータ ↓↓↓
![morphManyVideo.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/756415/9e56a611-d4cd-e008-d168-a9e6c396d0fb.jpeg)


↓↓↓ Comment のデータ ↓↓↓
![morphManyComment.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/756415/29391e44-87f3-8cf9-40de-f22e0a6e79ac.jpeg)


#### 子の Factory からポリモフィック関連の親データを作成する場合

```php:
        Comment::factory()
            ->count(3)
            ->for(
                Video::factory(),
                'commentable'
            )->create();
```


↓↓↓ Video のデータ ↓↓↓
![morphManyVideo2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/756415/bd2b580d-efff-2a7c-0264-004d5642714c.jpeg)


↓↓↓ Comment のデータ ↓↓↓
![morphManyComment2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/756415/7b93cbf9-a806-c286-cdc4-b5acb51885fe.jpeg)


## さいごに

ポリモフィック関連って最初はわけわからんと思っていましたが、  
大きな流れは、通常のリレーションの使い方と変わらないようです。

ただ、ポリモフィック関連はアンチパターンなようなので、  
この辺もまたキャッチアップしたいと思います。


## 参考

[公式ドキュメント：Polymorphic Relationships](https://laravel.com/docs/9.x/database-testing#polymorphic-relationships)