はじめに
実務でポリモフィック関連を扱うことになり、
「ポリモフィック関連ってなんや?」
となったので、思考整理のためにこちらの記事を作成しました。
ポリモフィック関連がアンチパターンということは理解しています。
今後キャッチアップしたいと思いますm(_ _)m
ご理解いただけますと幸いです。
もし、「間違って理解しているよ」という部分がありましたら、優しく 教えてくださると嬉しいです。
この記事でできるようになること
- ポリモフィック関連を理解できる
- Laravel で1対1と1対多のポリモフィック関連を使える
ポリモフィック関連とは
子供が複数の親を持ち、互いに参照するすることができるものです。
例えば、
ブログの画像と投稿とユーザーを想像してみて下さい。
画像(子)が、投稿(親)とユーザー(親)を持っている。ような関係になります。
もう少し具体的に表現すると
- ある投稿に紐付いている画像
- あるユーザーに紐付いている画像
というふうに複数の親に子が紐付いているのがポリモフィック関連になります。
ER図を見ていただけると理解しやすいかと思います。
実装の流れ
ざっくりとポリモフィック関連が理解できたかと思いますので、
次のような流れでポリモフィック関連の Factory を作って仮データを作成したいと思います。
- Migration を作成
- Migration にカラムと型を追加
- Model を作成
- Model にポリモフィック関連先のデータを取得するメソッド実装
- Factory を作成
- Seeder を作成
- DB を確認
1対1のポリモフィック関連の場合
冒頭に記載してた画像、投稿、ユーザーをサンプルにして作成します。
関係性は以下の通りです
- 画像(子)と投稿(親)の関係
- ある画像(子)は1つの投稿(親)に所属している
- ある投稿(親)は1つの画像(子)をもっている
- 画像(子)とユーザー(親)の関係
- ある画像(子)はあるユーザー(親)に所属している
- あるユーザー(親)は1つの画像(子)をもっている
ER図で表すとこんな感じです(冒頭のER図と同じです)
Migration 作成
下記のコマンドで posts, users, images の マイグレーションを作成し、カラムを追加
php artisan make:migration create_tablename_table
posts_table (親)
<?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
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
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
が親の情報になります。
- imageable_id: 親の id を取得
- imageable_type: 親のリレーション先のクラスを取得
詳細は後ほど説明しますが、DBでは下記のようなデータとなります
※ 今回は image テーブルのため imageable_〇〇
としましたが、任意のカラム名を命名すれば OK です
Model 作成
下記のコマンドで Post, User, Image の Model を作成し、
ポリモフィック関連先のデータを取得するメソッド実装
php artisan make:model Model名
PostModel (親)
<?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
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
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
というメソッドを使っていますが、
これらのメソッドでポリモフィック関連先のデータを取得することができます。
親モデルに下記の 親から子のデータを取得するメソッド
を記述します。
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 (親)
<?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 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 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
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つずつ作成します。
- imageable_id: 親の id を取得
- imageable_type: 親のリレーション先のクラスを取得
となります。
imageable_id は Post の id となっています。
子の Factory からポリモフィック関連の親データを作成する場合
Image::factory()
->count(3)
->for(
Post::factory(),
'imageable'
)->create();
こ場合は、ある Post に紐づく Image レコードを3つ作成します
なので、Post のレコードは1つになります。
1対多のポリモフィック関連の場合
Model のポリモフィック関連先のデータを取得するメソッドの記述方法が違うくらいで、
実装大きな流れは 1対1 のときと同じです。
1対多は、動画と投稿のコメントを例とします。
- コメント(子)と動画(親)の関係
- あるコメント(子)はある動画(親)に所属している
- ある動画(親)は複数のコメント(子)をもっている
- コメント(子)と投稿(親)の関係
- あるコメント(子)はある投稿ー(親)に所属している
- ある投稿(親)は複数のコメント(子)をもっている
ER図で表すとこんな感じです
Migration 作成
下記のコマンドで videos, comments の マイグレーションを作成し、カラムを追加
※ posts は上記と同じため省略
php artisan make:migration create_tablename_table
videos_table (親)
<?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
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 を作成し、
ポリモフィック関連先のデータを取得するメソッド実装
php artisan make:model Model名
VideoModel (親)
<?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
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
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 (親)
<?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 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
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
子の Factory からポリモフィック関連の親データを作成する場合
Comment::factory()
->count(3)
->for(
Video::factory(),
'commentable'
)->create();
さいごに
ポリモフィック関連って最初はわけわからんと思っていましたが、
大きな流れは、通常のリレーションの使い方と変わらないようです。
ただ、ポリモフィック関連はアンチパターンなようなので、
この辺もまたキャッチアップしたいと思います。