Laravel10でClosureTable(閉包テーブル)を使ってみた

## 実行環境

| framework | Laravel(10.20.0) |
| --- | --- |

## 本記事で得られる結果

- ClosureTable(閉包テーブル)をModelとした実装例

## 経緯

コメント機能実装時に編集や削除を考慮し、閉包テーブルとしようと思ったところ、当該ライブラリを見つけた。

使ってみるとあっさりとした実装ができて楽だったのでご紹介します。

(似たような記事がたくさんありますが、自己学習のためですのでお許しを)

## 参考資料

https://github.com/franzose/ClosureTable

## 事前準備

- [Laravel](https://laravel.com)の環境構築済みであること
  - Laravel Sail を利用する

https://qiita.com/naoto24kawa/items/6848dc89df1514efcea5

## 手順

1. 公式の通りにインストール

    Composerを使ってインストール

    ```shell
    sail composer require franzose/closure-table
    ```

    `config/app.php`に追加 (基本は自動で追記されているはず)

    ```diff_php:config/app.php
    return [
        'providers' => [
    +        Franzose\ClosureTable\ClosureTableServiceProvider::class
        ]
    ];
    ```

2. Modelの作成

    ```shell
    sail artisan closuretable:make Node
    ```
    > - Node はモデル名なので変更可
    > - 出力先のパスはオプションで指定可能です

   - 以下が自動で作成されます
       - Node (Model)
       - NodeClosure (Model)
       - 2023_00_00_000000_create_nodes_table_migration (Migration)
    
    なぜか、マイグレーションファイルにSchemaの定義が無いので追加しました

    ```diff_php:2023_00_00_000000_create_nodes_table_migration.php
        use Illuminate\Database\Schema\Blueprint;
        use Illuminate\Database\Migrations\Migration;
    +   use Illuminate\Support\Facades\Schema;
    ```


    さらに、必要なカラムを追加してあげてください

    ```diff_php:2023_00_00_000000_create_nodes_table_migration.php
    Schema::create('nodes', function (Blueprint $table) {
            $table->increments('id');
    +       $table->string('content');
    +       $table->integer('parent_model_id');
            $table->integer('parent_id')->unsigned()->nullable();
            $table->integer('position', false, true);
            $table->softDeletes();

    +       $table->foreign('parent_model_id')
    +           ->references('id')
    +           ->on('parents');
            ...
        });
    ```

    もし、他のテーブルを参照している場合にはモデルに関係を追加します

    ```diff_php:Node.php
    /* Entityはラッパークラスであることに注目したい */
    use Franzose\ClosureTable\Models\Entity;

    class Node extends Entity
    {
        ...

    +   public function ParentModel() : BelongsTo
    +   {
    +       return $this->belongsTo(ParentModel::class, 'parent_model_id');
    +   }
    }
    ```

3. 使い方

    あとは、それぞれ色々と操作できます

    ```php
    $nodes = [
        new Node(['id' => 1]),
        new Node(['id' => 2]),
        new Node(['id' => 3]),
        new Node(['id' => 4, 'parent_id' => 1])
    ];

    /** 取得 */
    Node::find(1)->isRoot(); // 自身は親であり、自身の親はいない
    Node::find(1)->isParent(); // 自身は親である

    Node::find(4)->hasAncestors(); // 自身の先祖がいる
    Node::find(4)->getAncestors(); // 自身の先祖
    Node::find(4)->ancestorsWithSelf() // 自身と先祖
    
    Node::find(1)->hasDescendants(); // 自身の子孫がいる
    Node::find(1)->getDescendants() // 自身の子孫
    Node::find(1)->descendantsWithSelf() // 自身と子孫

    Node::find(1)->hasChildren(); // 子がいるか
    Node::find(1)->getChildren(); // 子のみ

    // 他にもConutやNext, Prev, 兄弟の操作も可能

    /** 追加、削除 */
    Node::find(1)->addChild(new Node(['id' => 5])); // 子を追加
    Node::find(1)->removeChild(4); // 子を削除

    // 一括で追加(addChildren)、削除(removeChildren)も可能
    ```

## おまけ

これを仕組みから手作業で実装すると、冗長だし、面倒なので、非常に便利でした

その前に設計段階で投入する決断ができることのほうが大事かも