Laravelやってみる#9―laravel-nestedsetで木構造データを扱う

Laravel 7.x

ぬにょす(挨拶)。

表題の通りですが、PHPフレームワークであるところのLaravelを使ってみようということで、やったこと・できたこと・できなかったこと等を自分の備忘録として残していきます。

#9の今回は laravel-nestedset パッケージを使って木構造のデータを処理してみます。

やりたいこと

前回の Category モデルに親―子の階層構造を持たせたいと思います。パッと思いつくのは parent_id のようなカラムを追加することですが、子・孫を拾い出すのに再帰処理が必要になります。わざわざ車輪の再発明をするメリットはないので、素直にパッケージを利用して実現してみます。

インストール

composer を使ってインストールします。

composer require kalnoy/nestedset
Code language: plaintext (plaintext)

マイグレーション

テーブル作成処理に nestedset が使うカラムを追加します。

Schema::create('categories', function (Blueprint $table) { $table->id(); $table->string('name'); /*追加*/$table->nestedSet(); $table->timestamps(); });
Code language: PHP (php)

テーブルを再作成すると、_lft, _rgt, parent_id というカラムが追加されていました。

モデル

NodeTrait トレイトを追加します。また、テーブルカラムとして存在しない depth プロパティをガードに追加します。

use Kalnoy\Nestedset\NodeTrait; class Category extends Model { use NodeTrait; protected $guarded = [ 'id', 'created_at', 'updated_at', 'depth', ]; }
Code language: PHP (php)

コントローラー

結論から言うと、変更が必要になったのは全件取得する index アクションだけでした。他のアクションは変更なしに NodeTrait トレイトがよしなに計らってくれました。

public function index() { return Category::withDepth()->get()->toFlatTree(); } public function store(Request $request) { $category = new Category($request->all()); $category->save(); return response()->json(); } public function update(Request $request, Category $category) { $category->fill($request->all())->save(); return response()->json(); } public function destroy(Category $category) { $category->delete(); return response()->json(); }
Code language: PHP (php)

index アクションでは、withDepth() でルートからの深さを表す depth プロパティを持たせ、フラットな(ネストのない)配列として返却するようにしました。

ビュー

Bootstrap-Vueを使っていたり、axios周りで自前のmixinを使っていたりする点はご了承ください。

<template> <b-container> <h1>Manage Category</h1> <b-row> <b-col md="6"> <b-form @submit.prevent="onUpdate"> <b-form-group label="Name" label-for="name"> <b-form-input id="name" v-model="category.name" type="text" required /> </b-form-group> <b-form-group label="Parent" label-for="parent_id"> <b-form-select v-model="category.parent_id"> <template v-slot:first> <b-form-select-option :value="null">None</b-form-select-option> </template> <b-form-select-option v-for="item in categories" :key="item.id" :value="item.id" >{{ '―'.repeat(item.depth) + ' ' + item.name }}</b-form-select-option> </b-form-select> </b-form-group> <b-button type="submit" variant="primary">{{ category.id ? 'Update' : 'Add' }} Category</b-button> <b-button @click="onClear">Clear</b-button> </b-form> </b-col> <b-col md="6"> <b-table striped hover :items="categories" :fields="fields"> <template v-slot:cell(name)="data" >{{ '―'.repeat(data.item.depth) + ' ' + data.item.name }}</template> <template v-slot:cell(actions)="data"> <b-button size="sm" variant="success" @click="onEdit(data.item)">Edit</b-button> <b-button size="sm" variant="danger" @click="onRemove(data.item)">Remove</b-button> </template> </b-table> </b-col> </b-row> </b-container> </template> <script> function Category() { this.name = ""; this.parent_id = null; } export default { data: function() { return { category: {}, categories: [], fields: ["name", "actions"] }; }, mounted: function() { this.category = new Category(); this.getCategories(); }, methods: { onUpdate: async function() { let response; if (this.category.id) { response = await axios.patch( "/api/category/" + this.category.id, this.category ); } else { response = await axios.post("/api/category", this.category); } if (this.OK(response)) { this.category = new Category(); this.getCategories(); } else { console.log(response); } }, onEdit: function(item) { this.category = item; }, onRemove: async function(item) { const response = await axios.delete("/api/category/" + item.id); if (this.OK(response)) { this.getCategories(); } else { console.log(response); } }, onClear: function() { this.category = new Category(); }, getCategories: async function() { const response = await axios.get("/api/category"); if (this.OK(response)) { this.categories = response.data; } else { console.log(response); } } } }; </script>
Code language: HTML, XML (xml)

実行結果

途中で typo したのはご愛嬌。

まとめ

laravel-nestedset を利用することで、簡単に階層構造を持つデータを扱うことができました。ただ、この手のデータは不整合が起こりやすそうです。パッケージ側でもエラー処理してると思いますが、自前でも親データの存在チェックなどを行う必要があるでしょう。

コメント

タイトルとURLをコピーしました