PHP
laravel
Eloquent

Laravel 5.5でポリモーフィックリレーションを使った画像添付できるコメント付き掲示板を作成する

はじめに

Laravelによるポリモーフィックリレーションのサンプルです。

ポリモーフィックリレーションとは、親が複数ある親子関係を表すときに使います。
あまり日本語の文献がないので書き起こしてみました。

screen.gif

今回は投稿とコメントの両方とも画像を添付できる掲示板を作成します。画像のテーブルは投稿テーブルかコメントテーブルのどちらかが親になります。このような場合、ポリモーフィックリレーションを使うとシンプルに定義できます。

GitHub: https://github.com/naga3/laravel-sample-polymorphic

プロジェクト作成

Laravelのプロジェクトを適当な名前で新規作成してください。

その後、ファイルストレージをウェブ上から見るためにシンボリックリンクを張ります。

php artisan storage:link

モデル作成

投稿用のモデルPost、コメント用のモデルComment、添付画像用のモデルImageを作成します。

php artisan make:model Post -m
php artisan make:model Comment -m
php artisan make:model Image -m

-mオプションでマイグレーションファイルも同時に作成します。

テーブル作成

作成されたマイグレーションファイルを編集します。

xxxx_create_posts_table.php

投稿テーブルです。titleが題名、bodyが本文になります。

    public function up()
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->increments('id');
            $table->string('title');
            $table->text('body');
            $table->timestamps();
        });
    }

xxxx_create_comments_table.php

コメントテーブルです。post_idが投稿ID、bodyがコメント本文となります。

    public function up()
    {
        Schema::create('comments', function (Blueprint $table) {
            $table->increments('id');
            $table->integer('post_id');
            $table->text('body');
            $table->timestamps();
        });
    }

xxxx_create_images_table

添付画像テーブルです。$table->morphs('target');によってtarget_idtarget_typeの2カラムを定義しています(Thx to @nunulk 様)。target_idは画像が添付されている投稿IDまたはコメントID、target_typeは画像が添付されているモデルのクラス名(App\PostまたはApp\Comment)となります。このような形式にすることでポリモーフィックリレーションを設定しやすくしています。filenameは画像のファイル名です。

    public function up()
    {
        Schema::create('images', function (Blueprint $table) {
            $table->increments('id');
            $table->morphs('target');
            $table->string('filename');
            $table->timestamps();
        });
    }

リレーションの設定

モデルクラスにリレーションを設定します。

Post.php

class Post extends Model
{
    public function images()
    {
        return $this->morphMany(Image::class, 'target');
    }
    public function comments()
    {
        return $this->hasMany(Comment::class);
    }
}

imagesメソッド内のmorphManyメソッドでポリモーフィックリレーションを設定しています。2番目の引数のtargetが重要で、これによってtarget_idが投稿ID、target_typeの投稿モデルのクラス名となり、画像がどの投稿の添付であるかを判別します。

commentsメソッド内のhasManyメソッドは1対多のリレーションです。投稿に紐付いているコメントを全て取得します。

Comment.php

class Comment extends Model
{
    public function images()
    {
        return $this->morphMany(Image::class, 'target');
    }
}

こちらも、morphManyメソッドでポリモーフィックリレーションを設定しています。target_idのコメントIDとtarget_typeのコメントモデルのクラス名により、画像がどのコメントの添付であるかを判別します。

Image.php

class Image extends Model
{
    protected $guarded = ['id']; 
}

Imageモデルのリレーションは設定する必要はありません。一括createするのでMass assignmentの設定だけしています。

ルートの設定

web.phpを以下のようにします。

Route::get('/', function () {
    return view('home', ['posts' => App\Post::latest('id')->get()]);
});

// Post
Route::post('/', function () {
    $post = new App\Post();
    $post->title = request('title');
    $post->body = request('body');
    $post->save();

    $files = request('files');
    if ($files) foreach ($files as $file) {
        $file->store('public');
        $post->images()->create(['filename' => $file->hashName()]);
    }
    return redirect('/');
});

// Comment
Route::post('/comments', function () {
    $comment = new App\Comment();
    $comment->post_id = request('post_id');
    $comment->body = request('body');
    $comment->save();

    $files = request('files');
    if ($files) foreach ($files as $file) {
        $file->store('public');
        $comment->images()->create(['filename' => $file->hashName()]);
    }
    return redirect('/');
});

// Postのところが投稿部分です。

$file->storeメソッドでファイルをストレージに保存しています。

$post->images()->createメソッドで、投稿に紐付いた画像レコードを生成しています。hashNameは上記storeメソッドで保存されたファイル名です。target_idtarget_typeは自動的に入ります。

// Commentのところがコメント部分です。投稿とほぼ同じ処理です。

ビューの設定

ビューファイルhome.blade.phpを作成して以下のようにします。

<!doctype html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <title>BBS</title>
</head>
<body>
    <form method="post" enctype="multipart/form-data">
        {{ csrf_field() }}
        <p>題名<br><input type="text" name="title" size="50"></p>
        <p>本文<br><textarea name="body" cols="50" rows="8"></textarea></p>
        <p>添付<br><input type="file" name="files[]" multiple></p>
        <button>投稿</button>
    </form>
    @foreach ($posts as $post)
        <hr>
        <p>{{ $post->title }}</p>
        <pre>{{ $post->body }}</pre>
        <p>
            @foreach ($post->images as $image)
                <img src="{{ 'storage/' . $image->filename }}">
            @endforeach
        </p>
        <blockquote>
            @foreach ($post->comments as $comment)
                <pre>{{ $comment->body }}</pre>
                <p>
                    @foreach ($comment->images as $image)
                        <img src="{{ 'storage/' . $image->filename }}">
                    @endforeach
                </p>
            @endforeach
            <form method="post" action="{{ url('/comments') }}" enctype="multipart/form-data">
                {{ csrf_field() }}
                <p>コメント<br><textarea name="body" cols="50" rows="3"></textarea></p>
                <p>添付<br><input type="file" name="files[]" multiple></p>
                <input type="hidden" name="post_id" value="{{ $post->id }}">
                <button>コメント投稿</button>
            </form>
        </blockquote>
    @endforeach
</body>
</html>

おわりに

ポリモーフィックリレーションを使うことによって、親が複数ある親子関係をスッキリと記述することができました。