Laravel return all the ids of descendants

5.6k views Asked by At

How do I return all the ids of AllSubSections (all levels)

class Section extends Model
{
    public function Ads()
    {
        return $this->hasMany(Ad::class);
    }

    public function AllSubSections()
    {
        return $this->SubSections()->with('AllSubSections');
    }

    public function SubSections()
    {
        return $this->hasMany(Section::class);
    }

    public function Parent()
    {
        return $this->belongsTo(Section::class);
    }
}

what am currently doing is :

$section = Section::where('name', 'Properties')->first();
$subSections = $section->AllSubSections;
$subSections->pluck('id')

but it only returns the 1st level not all the levels.

2

There are 2 answers

1
Hammerbot On BEST ANSWER

Here is what I came with:

use Illuminate\Support\Collection;

class Section
{
    public function children ()
    {
        return $this->hasMany(Section::class,);
    }

    public function parent ()
    {
        return $this->belongsTo(Section::class);
    }

    public function getAllChildren ()
    {
        $sections = new Collection();

        foreach ($this->children as $section) {
            $sections->push($section);
            $sections = $sections->merge($section->getAllChildren());
        }

        return $sections;
    }
}

As you can see, getAllChildren is a recursive function. It contains a loop over the section children that adds to the collection the current child and calls itself again on this child.

You can then use:

$section->getAllChildren()->pluck('id');

And you will get all your children ids.

I hope I am responding to the question!

0
seedme On

Per @Hammerbot's answer in above, for these who gets resulted in performance issues, the most likely reason is that you have infinite loop (incest) in your relationship, for example

$child_id 3 has parent_id 2,
$child_id 2 has parent_id 3.

In this situation using the above recursive function getAllChildren() will end up in dead loop and cause the server throwing 500 error. This is well documented here by the @Community and @BakerStreetSystems.

Then how to fix this?

After several hours search and research, I found in general there are two approaches apply to resolve a recursive relationship collection (also called 'tree' collection of laravel) for getting values of nominated keys.

One is the approach here to built a new collection in one dimension by iterating through each child to get their whatever attributes. Only use above answer by @Hammerbot where your relationship database is not massive and there is certainly no incest relationship.

The other one is to play with the eager loaded collection and try to parse it to either Json string or array and then recursively retrieve each descendant's whatever attributes want, in both cases will be through deeply nested. Many thanks to @Pedro's idea of combining Laravel pipe method and php array_walk_recursive.

Here is a final solution for me, hopefully will help you as well.

In your Category Model, define the relationship including parent to children and child to child,

public function parent()
{
    return $this->belongsTo(self::class, 'parentCategoryNodeId', 'categoryNodeId');
}

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

public function getAscendantsAttribute()
{
    $parents = collect([]);
    $parent = $this->parent;
    while(!is_null($parent)) {
        $parents->push($parent);
        $parent = $parent->parent;
    }
    return $parents;
}

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

And in your Controller, you may call an array of all sub-category ids like this,

$descendantsIds = $category->descendants->pipe(function ($collection){
    $array = $collection->toArray();
    $ids = [];
    array_walk_recursive($array, function ($value, $key) use (&$ids) {
        if ($key === 'categoryNodeId') {
            $ids[] = $value;
        };
    });
    return $ids;
});

If you need an array of a chained ascendants of current $category, simply call the accessor defined in the Category Model,

$ascendantsIds = $category->ascendants->pluck('categoryNodeId');

As you can see this returns also in a format of array, like,

//[sub-sub-subcategory, sub-subcategory, subcategory, category]

If you want to loop through this for a view like in the Nav bar, you would want it in a reverse order, therefore, do

$category->ascendants->reverse()

then you can display something like:

//category > subcategory > sub-subcategory > sub-sub-subcategory

Let me know by comments if you have further issue or better solutions.