6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

laravel-adminでlaravel-nestedsetのツリーを扱う

Last updated at Posted at 2020-01-09

laravel-adminにはデフォルトでツリー構造のモデルを扱うための機能がついていますが、これはナイーブツリー向けのものであるためlaravel-nestedsetにそのまま使用することができません。

本記事はそれを解決するための手順メモです。

バージョン

使用したライブラリのバージョンは以下の通り。

  • laravel: v6.2.0
  • laravel-admin: v1.7.7
  • laravel-nestedset: v5.0.0

方針

laravel-adminにはナイーブツリー向けのEncore\Admin\Traits\ModelTreetraitがあるため、これを参考にlaravel-nestedset向けのtraitを作る。

ツリーの取り扱いには以下が参考になる。

※ ただし、2020/01/08現在、laravel-adminのリファレンスのmodel-treeの項は非推奨な機能を用いているため注意

laravel-nestedset向けのtraitを追加する

以下のtraitを追加する。

ModelNestedSetTree.php
<?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 = '&nbsp;')
    {
        $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;
        });
    }
}

ModelTreetraitから主に変更したのはtoTreeallNodessaveOrderあたり。

ModelTreeではbootの中でstatic::tree()を用いているが、これを定義しているAdminBuildertraitは非推奨であるため、new Tree(new static())に変更した。

ModelNestedSetTreeの使い方

ModelNestedSetTreeは基本的にModelTreeと同様に使える。

  1. ツリーのモデルにModelNestedSetTreetraitをつける(AdminBuilderは不要)
  2. コンストラクタ内で$this->setTitleColumn()を用いてタイトルとなるカラム名を指定する
  3. コントローラ内でnew Tree(new ModelClass())して使う

※ laravel-adminのリファレンスで使っているModelFormtraitは非推奨であるため注意

以下例

Category.php
<?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');
    }
}
CategoryController.php
<?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;
    }
}
6
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?