まいける's Tech Blog

LAMP関係のメモなどを不定期に掲載します

既存の Laravel プロジェクトに Closure Table を導入する

最初からツリー構造のデータを扱うことがわかっていれば、 franzose/closure-table を導入して、こちらの記事(
Laravel|Closure Tableで階層の深さが動的なカテゴリ構造を扱う - わくわくBank )などを参考にしながら設定すればよいのですが、後からコメントを階層化したいみたいなオーダーが出てきたときにどうすればよいか、というのが今回の記事のお話です。

1. 必要なライブラリをインストール

$ composer require franzose/closure-table

2. マイグレーションファイルの作成

Closure Table 用のテーブルを作成するとともに、元々のモデル用のテーブルに必要なカラムを追加します
(以下では、 comments テーブルをツリー化することを想定しています)

Closure Table 用のテーブルを作成

<?php

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

class CreateCommentClosuresTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('comment_closures', function (Blueprint $table) {
            $table->increments('closure_id');
            $table->integer('ancestor', false, true);
            $table->integer('descendant', false, true);
            $table->integer('depth', false, true);

            $table->foreign('ancestor')
                ->references('id')
                ->on('comments')
                ->onDelete('cascade');

            $table->foreign('descendant')
                ->references('id')
                ->on('comments')
                ->onDelete('cascade');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('comment_closures');
    }
}

既存の comments テーブルにカラムを追加

<?php

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

class AddClosureColumnsOnComments extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('comments', function (Blueprint $table) {
            $table->integer('parent_id')->unsigned()->nullable();
            $table->integer('position', false, true);
            $table->integer('real_depth', false, true);
            $table->foreign('parent_id')
                ->references('id')
                ->on('comments')
                ->onDelete('set null');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('comments', function (Blueprint $table) {
            $table->dropColumn('parent_id');
            $table->dropColumn('position');
            $table->dropColumn('real_depth');
        });
    }
}

3. モデルの作成

既存のモデル(Comment)を修正するほか、Closure Table 用のモデルを 3 つ追加します

Comment.php の修正

<?php
namespace App;

use Franzose\ClosureTable\Models\Entity;

class Comment extends Entity implements CommentInterface
{
    /**
     * The table associated with the model.
     *
     * @var string
     */
    protected $table = 'comments';

    /**
     * ClosureTable model instance.
     *
     * @var CommentClosure
     */
    protected $closure = 'App\CommentClosure';

    //複数代入を許可する項目
    protected $fillable = ['name', 'content'];

    (以下略)
}

use を追加するのと、$closure を追加するのが主な変更ですが、1点注意しなければならないのが複数代入の設定です。
ご存知のように、モデルには fillable か guarded のどちらかの属性を設定する必要がありますが
Closure Table を利用するためには、fillable で設定する必要があります。
というのも、Closure Table で利用するカラムに値を追加できるように

$this->fillable(array_merge($this->getFillable(), [$position, $depth]));

という処理を行っているためです。新規作成の場合にはこのあたり気にする必要はありませんが、既存のモデルを使う場合、注意が必要です。

CommentClosure.php の作成

<?php

namespace App;

use Franzose\ClosureTable\Models\ClosureTable;

class CommentClosure extends ClosureTable implements CommentClosureInterface
{
    /**
     * The table associated with the model.
     *
     * @var string
     */
    protected $table = 'comment_closures';
}

CommentClosureInterface.php の作成

<?php

namespace App;

use Franzose\ClosureTable\Contracts\ClosureTableInterface;

interface CommentClosureInterface extends ClosureTableInterface
{
}

CommentInterface.php の作成

<?php
namespace App;

use Franzose\ClosureTable\Contracts\EntityInterface;

interface CommentInterface extends EntityInterface
{
}

4. 初期データの投入

新規作成の場合は必要ないのですが、既存データを利用する場合、comments.position に値を入れるとともに、comment_closures テーブルに初期データを投入する必要があります。
comments.position は同じ階層にあるデータの並び順を表すデータで、他のデータと重複しないように設定する必要があります。
また、comment_closure テーブルのほうには、いったんすべてのデータが最上位の階層に並んでいるようなデータを投入します。
手っ取り早いのは、プライマリキーに設定されている comments.id を使う方法で

update comments set position = id;
insert into comment_closures (`ancestor`, `descendant`, depth) select id, id, 0 from comments;

こんな感じで設定しておけば大丈夫です

これで Closure Table が使えるようになると思います。それ専用に作られているだけあって、データの操作や取得はかなり便利に行うことができます。お試しください!