laravel-adminにはデフォルトでツリー構造のモデルを扱うための機能がついていますが、これはナイーブツリー向けのものであるためlaravel-nestedsetにそのまま使用することができません。
本記事はそれを解決するための手順メモです。
バージョン
使用したライブラリのバージョンは以下の通り。
- laravel: v6.2.0
- laravel-admin: v1.7.7
- laravel-nestedset: v5.0.0
方針
laravel-adminにはナイーブツリー向けのEncore\Admin\Traits\ModelTree
traitがあるため、これを参考にlaravel-nestedset向けのtraitを作る。
ツリーの取り扱いには以下が参考になる。
- laravel-nestedsetのREADME
- laravel-adminのリファレンス
-
Encore\Admin\Controllers\MenuController
まわりのソース
※ ただし、2020/01/08現在、laravel-adminのリファレンスのmodel-treeの項は非推奨な機能を用いているため注意
laravel-nestedset向けのtraitを追加する
以下のtraitを追加する。
<?php
namespace App\Admin\Traits;
use Encore\Admin\Tree;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Request;
trait ModelNestedSetTree
{
/**
* @var string
*/
protected $titleColumn = 'title';
/**
* @var \Closure
*/
protected $queryCallback;
/**
* @return string
*/
public function getParentColumn()
{
return $this->getParentIdName();
}
/**
* Get title column.
*
* @return string
*/
public function getTitleColumn()
{
return $this->titleColumn;
}
/**
* Set title column.
*
* @param string $column
*/
public function setTitleColumn($column)
{
$this->titleColumn = $column;
}
/**
* Get order column name.
*
* @return string
*/
public function getOrderColumn()
{
return $this->getLftName();
}
/**
* Set query callback to model.
*
* @param \Closure|null $query
*
* @return $this
*/
public function withQuery(\Closure $query = null)
{
$this->queryCallback = $query;
return $this;
}
/**
* Format data to tree like array.
*
* @return array
*/
public function toTree()
{
$self = new static();
if ($this->queryCallback instanceof \Closure) {
$self = call_user_func($this->queryCallback, $self);
}
return $self->get()->toTree()->toArray();
}
/**
* Get all elements.
*
* @return array
*/
public function allNodes()
{
return static::defaultOrder()->get()->toArray();
}
/**
* Save tree order from a tree like array.
*
* @param array $tree
*/
public static function saveOrder($tree = [])
{
static::rebuildTree($tree);
}
/**
* Get options for Select field in form.
*
* @param \Closure|null $closure
* @param string $rootText
*
* @return array
*/
public static function selectOptions(\Closure $closure = null, $rootText = 'ROOT')
{
$options = (new static())->withQuery($closure)->buildSelectOptions();
return collect($options)->prepend($rootText, 0)->all();
}
/**
* Build options of select field in form.
*
* @param array $nodes
* @param int $parentId
* @param string $prefix
* @param string $space
*
* @return array
*/
protected function buildSelectOptions(array $nodes = [], $parentId = 0, $prefix = '', $space = ' ')
{
$prefix = $prefix ?: '┝'.$space;
$options = [];
if (empty($nodes)) {
$nodes = $this->allNodes();
}
foreach ($nodes as $index => $node) {
if ($node[$this->getParentColumn()] == $parentId) {
$node[$this->titleColumn] = $prefix.$space.$node[$this->titleColumn];
$childrenPrefix = str_replace('┝', str_repeat($space, 6), $prefix).'┝'.str_replace(['┝', $space], '', $prefix);
$children = $this->buildSelectOptions($nodes, $node[$this->getKeyName()], $childrenPrefix);
$options[$node[$this->getKeyName()]] = $node[$this->titleColumn];
if ($children) {
$options += $children;
}
}
}
return $options;
}
/**
* {@inheritdoc}
*/
protected static function boot()
{
parent::boot();
static::saving(function (Model $branch) {
$parentColumn = $branch->getParentColumn();
if (Request::has($parentColumn) && Request::input($parentColumn) == $branch->getKey()) {
throw new \Exception(trans('admin.parent_select_error'));
}
if (Request::has('_order')) {
$order = Request::input('_order');
Request::offsetUnset('_order');
$tree = new Tree(new static());
$tree->saveOrder($order);
return false;
}
return $branch;
});
}
}
ModelTree
traitから主に変更したのはtoTree
とallNodes
、saveOrder
あたり。
ModelTree
ではboot
の中でstatic::tree()
を用いているが、これを定義しているAdminBuilder
traitは非推奨であるため、new Tree(new static())
に変更した。
ModelNestedSetTree
の使い方
ModelNestedSetTree
は基本的にModelTree
と同様に使える。
- ツリーのモデルに
ModelNestedSetTree
traitをつける(AdminBuilder
は不要) - コンストラクタ内で
$this->setTitleColumn()
を用いてタイトルとなるカラム名を指定する - コントローラ内で
new Tree(new ModelClass())
して使う
※ laravel-adminのリファレンスで使っているModelForm
traitは非推奨であるため注意
以下例
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Kalnoy\Nestedset\NodeTrait;
use App\Admin\Traits\ModelNestedSetTree;
class Category extends Model
{
use NodeTrait;
use ModelNestedSetTree;
protected $fillable = [
'parent_id',
'name',
];
public function __construct(array $attributes = [])
{
parent::__construct($attributes);
$this->setTitleColumn('name');
}
}
<?php
namespace App\Admin\Controllers;
use Encore\Admin\Controllers\AdminController;
use Encore\Admin\Tree;
use Encore\Admin\Form;
use Encore\Admin\Show;
use App\Models\Category;
class CategoryController extends AdminController
{
/**
* Title for current resource.
*
* @var string
*/
protected $title = 'カテゴリ';
/**
* Make a grid builder.
*
* @return Tree
*/
protected function grid()
{
$tree = new Tree(new Category());
$tree->branch(function ($branch) {
return $branch['name'];
});
return $tree;
}
...
/**
* Make a form builder.
*
* @return Form
*/
protected function form()
{
$form = new Form(new Category());
$form->select('parent_id')->options(Category::selectOptions());
$form->text('name');
return $form;
}
}