laravelで階層構造を扱うプラグインを使ってみた

## laravel-nestedとやらを利用
github: https://github.com/lazychaser/laravel-nestedset
他にhttps://github.com/etrepat/baum が同じようなプラグインだったけど(スター数も多い)
開発が終わってそうなのでやめた。
laravel-nestedは、v4でlaravel5.2 / 5.3を追っかけてるのも好印象。

__階層構造の表現は入れ子集合を使っている__
(cakeのtree behaviorも入れ子集合らしい)

## 環境
__php5.6.27__
__laravel5.1系__

なので、laravel-nestedのv3を使ってみる。

## インストール

```bash
$ php composer.phar require kalnoy/nestedset:3.1.3
```

## migration作成

### 階層構造をもつカテゴリーを実装することにする

### migrationファイル作成

```bash
$ php artisan make:migration create_categories_table
```

### 編集

```php:*_create_categories_table.php

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
use Kalnoy\Nestedset\NestedSet;

class CreateCategoriesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('categories', function (Blueprint $table) {
            NestedSet::columns($table);
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('categories', function (Blueprint $table) {
            NestedSet::dropColumns($table);
        });
    }
}
```

### migration実行

```bash
$ php artisan migrate
```

### テーブル確認

```
mysql> show columns from categories;
+-----------+------------------+------+-----+---------+-------+
| Field     | Type             | Null | Key | Default | Extra |
+-----------+------------------+------+-----+---------+-------+
| _lft      | int(10) unsigned | NO   | MUL | NULL    |       |
| _rgt      | int(10) unsigned | NO   |     | NULL    |       |
| parent_id | int(10) unsigned | YES  |     | NULL    |       |
+-----------+------------------+------+-----+---------+-------+
```

入れ子集合に最低限必要な部分しか作ってくれないのでやりなおし
migration追加

```php:
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('categories', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name', 100);
            $table->timestamps();
        });
    }
```

migration実行し、ひとまずこんな感じで

```
mysql> show columns from categories;
+------------+------------------+------+-----+---------------------+----------------+
| Field      | Type             | Null | Key | Default             | Extra          |
+------------+------------------+------+-----+---------------------+----------------+
| id         | int(10) unsigned | NO   | PRI | NULL                | auto_increment |
| name       | varchar(100)     | NO   |     | NULL                |                |
| _lft       | int(10) unsigned | NO   | MUL | NULL                |                |
| _rgt       | int(10) unsigned | NO   |     | NULL                |                |
| parent_id  | int(10) unsigned | YES  |     | NULL                |                |
| created_at | timestamp        | NO   |     | 0000-00-00 00:00:00 |                |
| updated_at | timestamp        | NO   |     | 0000-00-00 00:00:00 |                |
+------------+------------------+------+-----+---------------------+----------------+
8 rows in set (0.00 sec)
```

## 試しに使ってみる

ビュー作るのはめんどくさいのでコマンドで見ていく

```bash
$ php artisan make:console TestCommand --command="batchtest"
$ php artisan batchtest
```

コマンドに名前付けられるんだ。。

### Category Model

EloquentModelは使わない。

```php:app/Models/Category.php
use Kalnoy\Nestedset\Node;

class Category extends Node
{
    /**
     * モデルに関連付けるデータベースのテーブルを指定
     *
     * @var string
     */
    protected $table = 'categories';

    /**
     * createメソッド実行時に、入力を許可するカラムの指定
     *
     * @var array
     */
    protected $fillable = array('name');
}
```

### バッチ

ひとまずデータが保存されるのか階層構造を作って保存してみる

```php:app/Console/Commands/TestCommand.php
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use App\Models\Category;

class TestCommand extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'batchtest';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Command description';

    /**
     * App\Models\Category
     *
     * @var string
     */
    protected $Category;

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
        $this->Category = new Category();
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        //Foo - Bar - Bazの階層構造
        $node = Category::create([
            'name' => 'Foo',

            'children' => [
                [
                    'name' => 'Bar',

                    'children' => [
                        [ 'name' => 'Baz' ],
                    ],
                ],
            ],
        ]);
    }
}
```

### 実行

```bash
$ php artisan batchtest
```

```
select * from categories;

+------+------+-----------+----+------+--------------------+---------------------+
| _lft | _rgt | parent_id | id | name | created_at          | updated_at          |
+------+------+-----------+----+------+--------------------+---------------------+
|    1 |    6 |      NULL |  1 | Foo  | 2016-11-07 21:19:00 | 2016-11-07 21:19:00 |
|    2 |    5 |         1 |  2 | Bar  | 2016-11-07 21:19:00 | 2016-11-07 21:19:00 |
|    3 |    4 |         2 |  3 | Baz  | 2016-11-07 21:19:00 | 2016-11-07 21:19:00 |
+------+------+-----------+----+------+----------+---------------------+----------+
```

保存されてる、賢い！

## 使い方を見ていく

### カテゴリの追加

```php
/**
 * ex.)
 * $attributes = ['name' => 'hoge']
 */
$this->Category->create($attributes);
```

### あるカテゴリを一番上に分類する

```php
//上記のid2のデータに対して実行
$node = $this->Category->find(2);
$node->saveAsRoot();
```

```
+------+------+-----------+----+------+---------------------+---------------------+
| _lft | _rgt | parent_id | id | name |  created_at          | updated_at          |
+------+------+-----------+----+------+----------------------+---------------------+
|    1 |    2 |      NULL |  1 | Foo  |  2016-11-07 21:19:00 | 2016-11-07 21:19:00 |
|    3 |    6 |      NULL |  2 | Bar  |  2016-11-07 21:19:00 | 2016-11-08 10:45:01 |
|    4 |    5 |         2 |  3 | Baz  |  2016-11-07 21:19:00 | 2016-11-07 21:19:00 |
+------+------+-----------+----+------+----------------------+---------------------+

parent_idがnullになってることを確認できた(親を持たなくなった)
id2が持っていた子の紐づけも維持されている
```

### 既存のカテゴリを親カテゴリに紐づける(Barの子カテゴリにFooを紐づける)

```php
$parent = $this->Category->find(2); //Bar
$children = $this->Category->find(1); //Foo

$parent->appendNode($children);
```

```
+------+------+-----------+----+------+---------------------+---------------------+
| _lft | _rgt | parent_id | id | name | created_at          | updated_at          |
+------+------+-----------+----+------+---------------------+---------------------+
|    4 |    5 |         2 |  1 | Foo  | 2016-11-07 21:19:00 | 2016-11-08 11:13:39 |  //紐づいた
|    1 |    6 |      NULL |  2 | Bar  | 2016-11-07 21:19:00 | 2016-11-08 10:45:01 |
```

挙動は同じだけど子側からやる場合はこう

①

```php
$parent = $this->Category->find(2);
$children = $this->Category->find(1);

$children->appendTo($parent)->save();
```

②

```php
$parent = $this->Category->find(2);
$children = $this->Category->find(1);

$children->parent_id = $parent->id;
$children->save();
```

### 親に子を追加

```php
$parent = $this->Category->create(['name' => 'FUGA']);
//子を追加
$parent->children()->create(['name' => 'HOGE']);
```

```
+------+------+-----------+----+------+---------------------+---------------------+
| _lft | _rgt | parent_id | id | name | created_at          | updated_at          |
+------+------+-----------+----+------+---------------------+---------------------+
|    1 |    4 |      NULL |  1 | FUGA | 2016-11-08 11:27:08 | 2016-11-08 11:27:08 |
|    2 |    3 |         1 |  2 | HOGE | 2016-11-08 11:27:08 | 2016-11-08 11:27:08 |
+------+------+-----------+----+------+---------------------+---------------------+
```

### 削除

```php
$node = $this->Category->find(2);

$node->delete();
```

### 階層構造を出力

__data__

```
+------+------+-----------+----+---------------+---------------------+---------------------+
| _lft | _rgt | parent_id | id | name          | created_at          | updated_at          |
+------+------+-----------+----+---------------+---------------------+---------------------+
|    1 |   16 |      NULL |  1 | HOGE          | 2016-11-08 11:44:51 | 2016-11-08 11:44:51 |
|    2 |   13 |         1 |  2 | FUGA          | 2016-11-08 11:44:51 | 2016-11-08 11:44:51 |
|   17 |   22 |      NULL |  3 | HOGE_2        | 2016-11-08 11:45:07 | 2016-11-08 11:45:07 |
|   18 |   21 |         3 |  4 | FUGA_2        | 2016-11-08 11:45:07 | 2016-11-08 11:45:07 |
|   19 |   20 |         4 |  5 | HOGEFUGA_2    | 2016-11-08 11:52:51 | 2016-11-08 11:52:51 |
|    3 |    4 |         2 |  6 | HOGEFUGA      | 2016-11-08 12:29:33 | 2016-11-08 12:29:33 |
|   14 |   15 |         1 | 11 | HOGE_children | 2016-11-08 12:44:28 | 2016-11-08 12:44:28 |
+------+------+-----------+----+---------------+--------------------+---------------------+
```

```php
$results = $this->Category->get();
$tree = $results->toTree();

$traverse = function ($categories, $prefix = '-') use (&$traverse) {
    foreach ($categories as $category) {
        echo PHP_EOL.$prefix.' '.$category->name;

        $traverse($category->children, $prefix.'-');
    }
};

$traverse($tree);
```

__Result__

```
- HOGE
-- FUGA
--- HOGEFUGA
-- HOGE_children
- HOGE_2
-- FUGA_2
--- HOGEFUGA_2
```

その他まだ機能あるみたいですが最低限これくらい知っとけば使えるのかな
便利なのがあったら追記していきます

## 追記

### data

```
+----+--------------------------+-----------+------+------+
| id | name                     | parent_id | _lft | _rgt |
+----+--------------------------+-----------+------+------+
|  1 | testA                    |      NULL |    1 |   10 |
|  2 | testA_childA             |         1 |    2 |    9 |
|  3 | testA_childA_grandChildA |         2 |    3 |    4 |
|  5 | testA_childA_grandChildB |         2 |    5 |    6 |
|  6 | testA_childA_grandChildC |         2 |    7 |    8 |
+----+--------------------------+-----------+------+------+
```

### 子の取得

```php
$parent = $category->find(1);
$parent->ancestors()->get();//全ての子孫が取得される。上記の例でいうとid3,5,6の孫も含まれる
```

### 親の取得

```php
$grandchild = $category->find(3);
$grandchild->descendants()->get();//全ての親が取得される。
```

### 同じ階層にいるデータを持ってくる

```php
$grandchild = $category->find(3);
$grandchild->siblings()->get(); //id 5,6のデータを持ってくる
```

### 深さのプロパティを持つようにデータを取得

```php
$grandchild = $category->withDepth()->find(3);
dd($grandchild->depth);  //2

$parent = $category->withDepth()->find(1);
dd($parent->depth);  //0
```

### 特定の深さにいるデータを取得

```php
$grandchildren = $category->withDepth()->having('depth', '=', 2)->get();

dd($grandchildren); //id 3, 5, 6
```

### 必ず保存されている値の_lftでソート

```php
$category->defaultOrder()->get();

//inverse
$category->reversed()->get();
```

### deleteはrecursiveに行われる

```php
$category->delete()
```


子を持っている場合は削除される

### 現在のカテゴリが親カテゴリか

```php
$node->isRoot()
```

### 親子関係要素へのアクセス

```php
$node->children
$node->parent
```