ぬにょす(挨拶)。
表題の通りですが、PHPフレームワークであるところのLaravelを使ってみようということで、やったこと・できたこと・できなかったこと等を自分の備忘録として残していきます。
#9の今回は laravel-nestedset パッケージを使って木構造のデータを処理してみます。
やりたいこと
前回の Category モデルに親―子の階層構造を持たせたいと思います。パッと思いつくのは parent_id のようなカラムを追加することですが、子・孫を拾い出すのに再帰処理が必要になります。わざわざ車輪の再発明をするメリットはないので、素直にパッケージを利用して実現してみます。
インストール
composer を使ってインストールします。
Code language: plaintext (plaintext)composer require kalnoy/nestedset
マイグレーション
テーブル作成処理に 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 を利用することで、簡単に階層構造を持つデータを扱うことができました。ただ、この手のデータは不整合が起こりやすそうです。パッケージ側でもエラー処理してると思いますが、自前でも親データの存在チェックなどを行う必要があるでしょう。
コメント