How do I flatten laravel recursive relationship collection (tree collections)?
It's a bit late, but I'm going to post what I wish I had been able to find before I ended up writing it myself.
Similar to the original post, I have a recursive parent/child relationship in my categories
table (but this could apply to any table with a self-referencing parent_id
column). You can set up your Model like this:
Category.php
<?phpnamespace App\Models;use Illuminate\Database\Eloquent\Model;class Category extends Model { // Relationships public function parent() { return $this->belongsTo('App\Models\Category', 'parent_id'); } public function children() { return $this->hasMany('App\Models\Category', 'parent_id'); } public function nested_ancestors() { return $this->belongsTo('App\Models\Category', 'parent_id')->with('parent'); } public function nested_descendants() { return $this->hasMany('App\Models\Category', 'parent_id')->with('children'); } // Attributes public function getFlatAncestorsAttribute() { return collect(flat_ancestors($this)); } public function getFlatDescendantsAttribute() { return collect(flat_descendants($this)); }}
Then somewhere in your application, you need to have a place to put some global helper functions. You could follow the instructions found here, and then just paste in the following helper functions:
Helpers.php
function flat_ancestors($model) { $result = []; if ($model->parent) { $result[] = $model->parent; $result = array_merge($result, flat_ancestors($model->parent)); } return $result;}function flat_descendants($model) { $result = []; foreach ($model->children as $child) { $result[] = $child; if ($child->children) { $result = array_merge($result, flat_descendants($child)); } } return $result;}
The code above will then allow you to use $category->flat_ancestors
, which will produce a flat collection of all the category's ancestors, no matter how many there are. Similarly, using $category->flat_descendants
will yield a flat collection of all the child categories, and the child's children categories, and so on until all the posterity categories have been accounted for.
Some things to be careful of:
- This type of approach could potentially lead to an infinite loop ifyou have
Category 1
referencingCategory 2
as its parent, andthenCategory 2
hasCategory 1
as its parent. Just be carefulthat parent/child relationships are incest free :-) - This type of approach also isn't very efficient. It'll be fine for a bunch ofparent/child recursive relationships, but especially for the
flat_descendants
functions, the number of database queries growsexponentially for each generation level.
I didn't find any builtin method into theLaravel
collection either. You may try something like this (Use it as a global function or as a dedicated class method, it's up to you. here is the idea):
function flatten($array) { $result = []; foreach ($array as $item) { if (is_array($item)) { $result[] = array_filter($item, function($array) { return ! is_array($array); }); $result = array_merge($result, flatten($item)); } } return array_filter($result);}
Then use it like this:
// When available into global scope as a function$flattenArray = flatten($arrayFromTheCollection);
This will will recursively flatten. It doesn't prevent duplicates though, so you'll need to filter them out if that's an issue.
In your AppServiceProvider::boot
method
use Illuminate\Support\Collection;//...Collection::macro('flattenTree', function ($childrenField) { $result = collect(); foreach ($this->items as $item) { $result->push($item); if ($item->$childrenField instanceof Collection) { $result = $result->merge($item->$childrenField->flattenTree($childrenField)); } } return $result;});
Then
$flattened = $myCollection->flattenTree('childrenRecursive');// or in the case of the question$flattened = $model->childrenRecursive->flattenTree('childrenRecursive');