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

More than 1 year has passed since last update.

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を使ってみる。

インストール

$ php composer.phar require kalnoy/nestedset:3.1.3

migration作成

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

migrationファイル作成

$ php artisan make:migration create_categories_table

編集

*_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実行

$ 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追加

    /**
     * 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)

試しに使ってみる

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

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

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

Category Model

EloquentModelは使わない。

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

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

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

バッチ

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

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' ],
                    ],
                ],
            ],
        ]);
    }
}

実行

$ 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 |
+------+------+-----------+----+------+----------+---------------------+----------+

保存されてる、賢い!

使い方を見ていく

カテゴリの追加

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

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

//上記の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を紐づける)

$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 |

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

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

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

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

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

親に子を追加

$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 |
+------+------+-----------+----+------+---------------------+---------------------+

削除

$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 |
+------+------+-----------+----+---------------+--------------------+---------------------+
$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 |
+----+--------------------------+-----------+------+------+

子の取得

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

親の取得

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

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

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

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

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

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

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

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

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

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

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

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

deleteはrecursiveに行われる

$category->delete()

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

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

$node->isRoot()

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

$node->children
$node->parent