如何在 Laravel 中去构建部门树形结构 API

@这是小豪的第八篇文章

这两天在折腾组织架构的人员选择器,很难受啊,每次遇到这种需要用到递归的计算脑袋就转不过来,不过好在还是折腾出来了,今天给大家介绍一下到底折腾出来了啥,优不优雅,哈哈。

准备

做什么?组织架构的人员选择器的 API 接口,其中组织架构的层级是无下限的,姑且当做是无限极的吧。。。无限极最近貌似有点火呀,哈哈。

我们先来看一下表结构,直接从模型中看吧:

class Department extends Model
{
    use SoftDeletes;

    protected $fillable = [
        'parent_id', 'name', 'alias', 'level', 
    ];

    /**
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
     */
    public function children()
    {
        return $this->hasMany(__CLASS__, 'parent_id');
    }

    /**
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
     */
    public function users()
    {
        return $this->hasMany(User::class);
    }
}

模型中声明了两种模型关联,部门子集以及部门员工,如果对模型关联不太熟悉的建议看一下之前的文章,《如何更快的找到自己所需的模型关联类型?》

再来看一下控制器:

class DepartmentController extends Controller
{
    /**
     * Display a listing of the resource.
     *
     * @param \Illuminate\Http\Request $request
     *
     * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection|\Illuminate\Support\Collection
     */
    public function index(Request $request)
    {
        if ($request->has('tree')) {
            return DepartmentResource::tree();
        }

        return new DepartmentResource(Department::where($request->all())->get());
    }
}

因为树形结构不是必选项,所以在有接收到 tree 参数的时候才进行处理。

最后看一下 API 资源:

class DepartmentResource extends JsonResource
{

}

不熟悉 API 资源的,建议看一下之前的又一篇文章,哈哈。《如何优雅的去处理 API 数据格式》,我都佩服自己打广告的能力了,哈哈。

开始了噢

1. 我们先在 DepartmentResource 中定义一个静态方法 tree,并获取所有部门数据

public static function tree()
{
    // 我这里默认把 users 给加上去了,大家可以根据自己的需求来决定
    $departments = Department::with('users')->get();
}

2. 现在我们需要对部门所有的数据进行处理了,这是最头疼的。。。 我们来看一下小豪是如何处理的

/**
 * @param \Illuminate\Support\Collection $departments
 * @param \Illuminate\Support\Collection $parents
 *
 * @return \Illuminate\Support\Collection
 */
protected static function departmentsTree(Collection $departments, Collection $parents)
{
    $departments->map(function ($department, $key) use ($departments, $parents) {
        $department->children = \collect([]);

        if (empty($department->parent_id)) {
            $parents->push($department);

            $departments->forget($key);
        }

        $parents->map(function ($parentDepartment, $parentKey) use ($key, $department, $departments, $parents) {
            if ($department->parent_id == $parentDepartment->id) {
                $parents->get($parentKey)->children->push($department);

                $departments->forget($key);
            }
        });
    });

    $parents->map(function ($parentDepartment, $key) use ($departments, $parents) {
        if ($parentDepartment->children->isNotEmpty()) {
            $parents->get($key)->children = self::departmentTree($departments, $parentDepartment->children);
        }
    });

    return $parents;
}

因为 Department::with('users')->get() 获取的是一个集合,所以我们都是用集合的方式进行处理的。

  • 我们接收了两个参数:$departments$parents,第一个参数就不多说了,第二个参数用来装最外层的部门,也就是一级部门(父级 id 为 0 的部门)。

  • 我们先对所有部门进行遍历,给每一个部门初始化一个 children 空集合,然后将一级部门装进 $parents 中,并从 $departments 剔除已经使用过的 department

  • $parents 进行遍历,为 department 找到指定父级,并 pushchildren 集合中,同样剔除已经使用过的 department

  • 上面的操作只进行了两层,现在到了关键点了,我们对已经经过一层筛选的 $parents 进行遍历,将那些存在 children 的挑出来,然后继续进行上面的操作,同时第二个参数为 $parentDepartment->children ,反复处理之后,就能得到最终的结果啦。

不知道说清楚没有,哈哈。

3. 现在来调用一下:

/**
 * @return \Illuminate\Support\Collection
 */
public static function tree()
{
    $departments = Department::with(\request()->includes())->get();

    return self::departmentTree($departments, \collect([]));
}

4. 大公告成,哈哈。

不过还没完,我们来看一下基于模型的树状结构该怎么去写,你想不到的优雅,哈哈。

优雅的方式开始

我们在模型中添加这样一个方法:

/**
 * @return \Illuminate\Database\Eloquent\Relations\HasMany
 */
public function subDepartments()
{
    return $this->children()->with('subDepartments');
}

Api 资源中处理:

class DepartmentResource extends JsonResource
{
    public function __construct(\Illuminate\Database\Eloquent\Model $resource)
    {
        parent::__construct($resource);

        return $resource->subDepartment;
    }
}

调用:

class DepartmentController extends Controller
{
    /**
     * Display a listing of the resource.
     *
     * @param \Illuminate\Http\Request $request
     *
     * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection|\Illuminate\Support\Collection
     */
    public function index(Request $request)
    {
        return DepartmentResource::collection(Department::where($request->all())->get());
    }
}

这样就 ok 了,是不是大开眼界,反正我是的,哈哈。不过这做有个弊病就是在不指定部门的时候,所有的部门全部展示出来了,不过都可以灵活的运用啦。

结束语

写的很简陋,大家如果看出可以优化的,或者有错误的地方,指正一下哈,共同进步呀,哈哈。

本作品采用《CC 协议》,转载必须注明作者和本文链接
finecho # Lhao
本帖由系统于 5年前 自动加精
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 14

这个好 优雅 很laravel

5年前 评论

$department->children = \collect([]);这种写法第一次见啊,\collect([])是啥意思啊

5年前 评论
finecho

@mamahaha 哈哈,就是定义一个空集合

5年前 评论
finecho

@jake666 哈哈,是吧,刚开始看到这种处理方式的时候,我觉得真的是厉害。

5年前 评论

好高端啊,$departments->map()这个能解释下吗

5年前 评论

@Lhao 多谢,我去研究下,还没用过集合

5年前 评论

@Lhao 多谢,我去研究下,还没用过集合

5年前 评论
Toiu

为何不直接获取一级(顺便带上是否有下级的标识), 然后点开的时候再加载下级呢

5年前 评论
guanhui07

laravel集合 还是挺不错的

5年前 评论

之前写过一个扩展:https://packagist.org/packages/betterde/tr...
部门、菜单这种需要有树形结构的都可以!

5年前 评论
puzzle9 3年前
GeorgeKing (作者) 3年前
yybawang

Model 里写个递归就好了
public function child($parent_id){
return User::where('parent_id', $parent_id)->get()->each(function($v){
$v->children = $this->children($v->id);
})
}
事实上是应该拆分成两个方法的,调用一个,递归自己是一个,这只是个示例 :grinning:

4年前 评论

Department

class Department {
    public function children(){
        return $this->hasMany(self::class,'parent_id');
    }

    public function childrenTree(){
        return $this->children()->with('children');
    }

    public function users(){
        return $this->hasMany(User::class);
    }

    public function scopeTopLevel($query){
        return $query->where('parent_id',0);
    }
}

获取

Department::topLevel()->with(['users','childrenTree'])->all();获取层级嵌套关系(无限极)

或者

Department::topLevel()->with(['users'])->all();获取顶层数据

4年前 评论
萧潇 4年前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!