Eloquent: Recursive hasMany Relationship with Unlimited Subcategories

Quite often in e-shops you can see many level of categories and subcategories, sometimes even unlimited. This article will show you how to achieve it elegantly with Laravel Eloquent.

We will be building a mini-project to views children shop sub-categories, five level deep, like this:


Database Migration

Here’s a simple schema of DB table:

Schema::create('categories', function (Blueprint $table) {
    $table->bigIncrements('id');
    $table->string('name');
    $table->unsignedBigInteger('category_id')->nullable();
    $table->foreign('category_id')->references('id')->on('categories');
    $table->timestamps();
});

We just have a name field, and then relationship to the table itself. So most parent category will have category_id = NULL, and every other sub-category will have its own parent_id.

Here’s our data in the database:


Eloquent Model and Relationships

First, in app/Category.php we add a simple hasMany() method, so category may have other subcategories:

class Category extends Model
{

    public function categories()
    {
        return $this->hasMany(Category::class);
    }

}

Now comes the biggest “trick” of the article. Did you know that you can describe recursive relationship? Like this:

public function childrenCategories()
{
    return $this->hasMany(Category::class)->with('categories');
}

So, if you call Category::with(‘categories’), it will get you one level of “children”, but Category::with(‘childrenCategories’) will give you as many levels as it could find.


Route and Controller method

Now, let’s try to show all the categories and subcategories, as in the example above.

In routes/web.php, we add this:

Route::get('categories', 'CategoryController@index');

Then, app/Http/CategoryController.php looks like this:

public function index()
{
    $categories = Category::whereNull('category_id')
        ->with('childrenCategories')
        ->get();
    return view('categories', compact('categories'));
}

As you can see, we’re loading only parent categories, with children as relationships. Simple, huh?


View and Recursive Sub-View

Finally, to the Views structure. Here’s our resources/views/categories.blade.php:

<ul>
    @foreach ($categories as $category)
        <li>{{ $category->name }}</li>
        <ul>
        @foreach ($category->childrenCategories as $childCategory)
            @include('child_category', ['child_category' => $childCategory])
        @endforeach
        </ul>
    @endforeach
</ul>

As you can see, we load the main categories, and then load children categories with @include.

The best part is that resources/views/admin/child_category.blade.php will use recursive loading of itself. See the code:

<li>{{ $child_category->name }}</li>
@if ($child_category->categories)
    <ul>
        @foreach ($child_category->categories as $childCategory)
            @include('child_category', ['child_category' => $childCategory])
        @endforeach
    </ul>
@endif

As you can see, inside of child_category.blade.php we have @include(‘child_category’), so the template is recursively loading children, as long as there are categories inside of the current child category.


And, that’s it! We have unlimited level of subcategories – in database, in Eloquent relationships, and in Views.

Like our articles?
Check out our Laravel online courses!

10 COMMENTS

  1. I know you are using category_id so it is the convention. But for me, it is not clear. I would call it parent_id or just parent

    $table->foreign(‘parent’)->references(‘id’)->on(‘categories’);

    Thank you for the article.

    • This is fine, but it breaks out of the Laravel defaults so if you are working with a team of devs, or pass it on as legacy, then it can be harder for them to know what’s happening.

      Also ‘parent’ is parent what? Id? Model? Just the work parent makes me think it would be a Model returned, but actually it is the Id, so calling it parent_id removes all ambiguity.

  2. Though it could provide a solution to n level of hierarchical data, it will consume little more memory and response load time get slow.

    Alternate solution is, Using query builder I can give solution.
    N level of hierarchy, One function, Onetime load using boot(), One query, one time will execute at the application bootstrap level. dynamically can manage category sub-category

    thanks and regards

  3. I use such notation:

    public function children()
    {
    return $this->hasMany(TriggerAction::class, ‘parent_id’);
    }

    public function childrenNested()
    {
    return $this->hasMany(TriggerAction::class, ‘parent_id’)->with(‘childrenNested’);
    }

    This way it is visible that nested relation is called “recursively”, thus having unlimited deep.

LEAVE A REPLY

Please enter your comment!
Please enter your name here