はじめに
マルチログインを実装した画像投稿アプリを作っている中で、ポリモーフィックリレーションについての記事があまりなくて悩んだので、忘備録として残しておきます。内容はモデルのリレーションの話と、実装方法のみ。Laravelのインストール〜モデルの作成、マルチログイン実装などは参考になる記事がたくさんあったのでそちらをご参照ください。
今回のユースケースの解説
マルチログインを実装した時に、usersテーブルとgroupsテーブルを使ってそれぞれ別のログインページからログインさせて、middlewareを使ってログイン後の画面も別々で管理していました。group側からは写真の投稿ができたりマイページで自分の投稿を一覧で見たりする機能がありますが、user側からは投稿の閲覧とコメントしかできないように設計しています。
今回ポリモーフィックを使った箇所はコメント機能の実装です。コメントはuser側からもgroup側からも可能で、誰がコメントしたか分かるようにします。この時コメント(comment)はuserもしくはgroupどちらかに所属することになります。これがポリモーフィック関係です。
テーブル構造
users //親その1
id - integer
name - string
nickname - string
email - string
///省略
groups //親その2
id - integer
name - string
email - string
establish - date
///省略
comments //userかgroupいずれかにひも付く
id - integer
post_id - unsignedInteger //外部キーでpostにひも付く
body - text
commentable_id - integer //userかgroupのidが入る
commentable_type - string //モデル名:App\UserかApp\Groupが入る
commentsテーブルだけ書き方が特殊です。構造見れば分かりますが、commentsテーブルにuser_id
とgroup_id
を外部キーとして持たせておけば、別にポリモーフィックを使わずともそれぞれ一対多のリレーションで処理できるのです。
ということでポリモーフィックは実際使う人が少なくて記事がなかったんだろうなと思っています。(私はせっかく機能としてあるなら使いたい!ということで使いました)
それと、ポリモーフィック自体がアンチパターンでもあるという記事も見かけましたので置いておきますね。
SQLアンチパターンを読んで (ポリモーフィック関連について)
実際に書いていく
前提はここまでにして、いざ書いていきます。まずは、各モデルにリレーションの定義をしていきます。
各モデルのリレーション定義
class User extends Authenticatable
{
/**
* Commentに対してmorphMany
*/
public function comments()
{
return $this->morphMany('App\Comment', 'commentable');
}
}
class Group extends Authenticatable
{
/**
* Userと同じ
*/
public function comments()
{
return $this->morphMany('App\Comment', 'commentable');
}
}
class Comment extends Model
{
/**
* 忘れずに$fillableを定義する
*/
protected $fillable = [
'post_id',
'body',
'commentable_id',
'commentable_type'
];
/**
* コメントはPost(投稿)にひも付く(今回は解説しません)
*/
public function post()
{
return $this->belongsTo('App\post');
}
/**
* 所有しているcommentableモデルの全取得
*/
public function commentable()
{
return $this->morphTo();
}
}
で、ここまではだいたい調べれば書いてあるので、大丈夫だと思います。
次は、リレーションが定義できているかを調べるため、まずはtinkerでダミーのデータを登録しておきます。
tinkerでダミーデータの登録
php artisan tinker
>>> $com = new App\Comment
=> App\Comment {#2972}
>>> $com->post_id = 1
=> 1
>>> $com->body = 'testtesttest'
=> "testtesttest"
>>> $com-> commentable_id = 1
=> 1
>>> $com->commentable_type = 'App\Group'
=> "App\Group"
>>> $com->save();
=> true
>>> exit
mysql> select * from comments;
+----+---------+--------------+----------------+------------------+---------------------+---------------------+
| id | post_id | body | commentable_id | commentable_type | created_at | updated_at |
+----+---------+--------------+----------------+------------------+---------------------+---------------------+
| 1 | 1 | testtesttest | 1 | App\Group | 2019-05-21 05:51:03 | 2019-05-21 05:51:03 |
+----+---------+--------------+----------------+------------------+---------------------+---------------------+
1 row in set (0.00 sec)
ちゃんと登録されました。
投稿者に関連するコメントを取り出す
先ほど登録した情報を取り出します。コメントの投稿者はGroupモデルのid=1
としたので、Groupモデルからコメントのデータを取って来られれば成功です。
php artisan tinker
>>> $comment = App\Group::find(1)->comments->all();
[
App\Comment {#2988
id: 1,
post_id: 1,
body: "testtesttest",
commentable_id: 1,
commentable_type: "App\Group",
created_at: "2019-05-21 05:51:03",
updated_at: "2019-05-21 05:51:03",
},
]
//ちなみに逆の時、つまりcommentからuserもしくはgroupを取得する時はこんな感じ
>>> $auth = App\Comment::find(1)->commentable->all();
=> App\Group {#2989
id: 1,
name: "グループname",
cover_img: "sample.jpeg",
icon_img: "sample.jpeg",
//・・・省略
}
よしっ!!!ページに実装だ!
・・・と思ってview側からコメントの表示はできたのですが、保存は???となりました。
コメントの保存
結論から言うと、めちゃくちゃ簡単でした。この辺が調べた時見たやつです。
commentsテーブルのcommentable_id
とcommentable_type
は定義せずともいい感じにやって頂けるようで。相変わらずかしこい。
public function store(Request $request)
{
/**ログインユーザーの情報取得*/
$auth = Auth::user();
/**ログインユーザーにひも付けてコメントを保存*/
$auth->comments()->create(
[
'post_id' => $request->post_id,
'body' => $request->body
]
);
//これで保存完了
/**リダイレクト先の振り分け*/
if (get_class($auth) == "App\Group") {
return redirect('/group/posts');
} elseif (get_class($auth) == "App\User") {
return redirect('/');
}
}
これでOKです!最後に一応、groupのview側からのコメント投稿箇所のコードも一部載せておきます。ルートの定義は、こんな感じでいけるはず。
Route::get('group/comments', 'CommentController@index')
Route::post('group/comments', 'CommentController@store');
@forelse ($post->comments as $comment)
<p>コメント</p>
<pre>{{ $comment->body }}</pre>
@empty
<p>コメントがまだありません</p>
@endforelse
<form method="post" action="{{ url('group/comments') }}">
@csrf
<p>コメント<br><textarea name="body" cols="40" rows="3"></textarea></p>
<input type="hidden" name="post_id" value="{{ $post->id }}">
<button type="submit">コメント投稿</button>
</form>
不備や、もっとこうしたほうがいい!等ご意見があればぜひコメントいただければと思います。