LoginSignup
1

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-06-25

はじめに

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

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

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

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

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

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

  • ポリモフィック関連を理解できる
  • Laravel で1対1と1対多のポリモフィック関連を使える

ポリモフィック関連とは

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

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

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

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

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

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

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

実装の流れ

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

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

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

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

関係性は以下の通りです

  • 画像(子)と投稿(親)の関係
    • ある画像(子)は1つの投稿(親)に所属している
    • ある投稿(親)は1つの画像(子)をもっている
  • 画像(子)とユーザー(親)の関係
    • ある画像(子)はあるユーザー(親)に所属している
    • あるユーザー(親)は1つの画像(子)をもっている

ER図で表すとこんな感じです(冒頭のER図と同じです)

Migration 作成

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

php artisan make:migration create_tablename_table

posts_table (親)

○○○○_○○_○○_○○○○○○_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 (親)

○○○○_○○_○○_○○○○○○_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 (子)

○○○○_○○_○○_○○○○○○_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 tableusers table は特に変わったカラムはありませんが、
images tableimageable_idimageable_type といったカラムが存在します。

この imageable_idimageable_type が親の情報になります。

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

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

morphOnePost.jpg

morphOne.jpg

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

Model 作成

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

php artisan make:model Model名 

PostModel (親)

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 (親)

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 (子)

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

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

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

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

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

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

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

簡単ですね。

Factory 作成

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

php artisan make:factory Model名

PostFactory (親)

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 (親)

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 (子)

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 データを作成します

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 からポリモフィック関連の子データを作成する場合

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

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

↓↓↓ Post のデータ ↓↓↓
morphOnePost.jpg

↓↓↓ Image のデータ ↓↓↓
morphOneImage.jpg

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

となります。

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

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

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

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

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

↓↓↓ Post のデータ ↓↓↓
morphOnePost2.jpg

↓↓↓ Image のデータ ↓↓↓
morohpOneImage2.jpg

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

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

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

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

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

Migration 作成

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

php artisan make:migration create_tablename_table

videos_table (親)

○○○○_○○_○○_○○○○○○_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 (子)

○○○○_○○_○○_○○○○○○_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_idcommetnable_type とします。

Model 作成

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

php artisan make:model Model名 

VideoModel (親)

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 (子)

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 (子)

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 の記述方法と同じです。

1対1のとき

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

1対多のとき

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

Factory 作成

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

php artisan make:factory Model名

VideoFactory (親)

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 (子)

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 データを作成します

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 からポリモフィック関連の子データを作成する場合

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

Migration と Seed データを実行

php artisan migrate:fresh --seed

↓↓↓ Video のデータ ↓↓↓
morphManyVideo.jpg

↓↓↓ Comment のデータ ↓↓↓
morphManyComment.jpg

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

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

↓↓↓ Video のデータ ↓↓↓
morphManyVideo2.jpg

↓↓↓ Comment のデータ ↓↓↓
morphManyComment2.jpg

さいごに

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

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

参考

公式ドキュメント:Polymorphic Relationships

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1