belongsToMany(PpProject::class, 'pp_project_category', 'category_id', 'project_id'); } public function members() { return $this->belongsToMany(\App\User::class, 'pp_project_category_members', 'category_id', 'user_id'); } public function creator() { return $this->belongsTo(\App\User::class, 'created_by'); } public function parent() { return $this->belongsTo(self::class, 'parent_id'); } public function children() { return $this->hasMany(self::class, 'parent_id')->orderBy('position')->orderBy('name'); } public function scopeForBusiness($query, $business_id) { return $query->where('business_id', $business_id); } public function scopeRoots($query) { return $query->whereNull('parent_id'); } public function isRoot(): bool { return empty($this->parent_id); } public function hasChildren(): bool { if ($this->relationLoaded('children')) { return $this->children->isNotEmpty(); } return $this->children()->exists(); } /** * @return int[] */ public static function descendantIds(int $business_id, int $category_id): array { $by_parent = self::forBusiness($business_id)->get(['id', 'parent_id'])->groupBy('parent_id'); $ids = []; $queue = [$category_id]; while (! empty($queue)) { $current = array_shift($queue); foreach ($by_parent->get($current, collect()) as $child) { $ids[] = $child->id; $queue[] = $child->id; } } return $ids; } /** * @return Collection */ public static function breadcrumb(int $business_id, ?int $category_id): Collection { if (empty($category_id)) { return collect(); } $map = self::forBusiness($business_id)->get(['id', 'parent_id', 'name'])->keyBy('id'); $trail = collect(); $current = $map->get($category_id); while ($current) { $trail->prepend($current); $current = $current->parent_id ? $map->get($current->parent_id) : null; } return $trail; } public static function treeDropdown(int $business_id, ?int $exclude_id = null, ?int $prefix_id = null): array { $roots = self::forBusiness($business_id)->roots()->orderBy('position')->orderBy('name')->get(); $all = self::forBusiness($business_id)->orderBy('position')->orderBy('name')->get()->groupBy('parent_id'); $exclude = $exclude_id ? array_merge([$exclude_id], self::descendantIds($business_id, $exclude_id)) : []; $options = []; $walk = function ($nodes, $depth = 0) use (&$walk, &$options, $all, $exclude) { foreach ($nodes as $node) { if (in_array($node->id, $exclude, true)) { continue; } $options[$node->id] = str_repeat('— ', $depth).$node->name; $walk($all->get($node->id, collect()), $depth + 1); } }; $walk($roots); return $options; } public static function dropdown($business_id) { return collect(self::treeDropdown($business_id)); } /** * @return Collection */ public static function treeForBusiness(int $business_id): Collection { $all = self::forBusiness($business_id)->orderBy('position')->orderBy('name')->get(); return self::buildTree($all); } public static function buildTree(Collection $categories, ?int $parent_id = null): Collection { return ($parent_id === null ? $categories->whereNull('parent_id') : $categories->where('parent_id', $parent_id)) ->values() ->map(function ($category) use ($categories) { $category->setRelation('children', self::buildTree($categories, $category->id)); return $category; }); } }