Recursive Nested Data in Laravel, The Right Way

November 21st, 2021

Nested data is a pain point for a lot of developers. Relationships, eager loading and recursively iterating and displaying hierarchical data is enough to cause a headache.

In this post, I'm going to show you the easiest, cleanest (and most importantly, fastest) way to deal with nested data like this – and we're using the example of *categories, *much like you'd find in the navigation of an e-commerce store.

First up, create a fresh Laravel project so we can play around. Then add a route that renders a plain view. This is where we'll output our recursive category list.

Route::get('/categories', function () {
    return view('categories');
});

Now create a Category model alongside a migration and factory.

php artisan make:model Category -m -f

In the Category model migration, add the following columns. You can add more later depending on your needs, but for now this is all we'll need.

Schema::create('categories', function (Blueprint $table) {
    $table->id();
    $table->string('title');
    $table->string('slug')->unique();
    $table->unsignedBigInteger('parent_id')->nullable();
    $table->timestamps();
});

The parent_id here is what links each category to a parent. If the parent_id is null, it's a *root level *category. Otherwise, the parent_id will reference the parent. We can create an unlimited amount of nesting levels this way.

Run your migrations.

php artisan migrate

Now let's create some example categories. You can do this manually in your database client, or use the factory that was generated. Let's use the factory here.

In CategoryFactory, fill in the definition to include the title and slug.

public function definition()
{
    return [
        'title' => $title = $this->faker->unique()->sentence(4),
        'slug' => Str::slug($title),
    ];
}

Now run php artisan tinker on the command line and create a couple of *root level *categories.

App\Models\Category::factory()->times(2)->create();

Now create a child category for each of these categories, by passing in the parent_id of both the root categories.

App\Models\Category::factory()->create(['parent_id' => 1]);
App\Models\Category::factory()->create(['parent_id' => 2]);

Your database should look a little something like this now.

To test this, we really just need a couple more child categories for one of the child categories we already have, so create two more categories for (in this case) the category with an ID of 3.

You should now have a set of data similar to this.

Here's how this would look as a hierarchy, and how we would want to see it in the browser.

|- Et quisquam consequatur enim.
    |-- Aut itaque voluptas temporibus repellendus.
        |--- Aliquam dolorum qui molestiae dignissimos.
        |--- Natus aut numquam aliquam iusto dolor.

|- Et dolor quis dolorem dolore.
    |-- Sunt asperiores est qui. 

Of course, you could keep going with the nest, but we'll leave it at this for now.

You might have worked with or heard the term 'nested sets' before, but we're taking a different approach.

Adjacency lists provide recursive relationships using common table expressions (CTE). A CTE allows you to define a temporary set of data in the execution of an SQL statement. Essentially we can, using SQL, grab all of the nested data we need in one query, very quickly, and build it back up to be displayed as a tree. You can read more about this with a quick Google search.

If this sounds daunting, don't worry. Luckily for us, there's a brilliant package that handles all of this for us in Laravel specifically.

Have a peek at https://github.com/staudenmeir/laravel-adjacency-list, then let's get started on adding this to our Category model.

Install the package, first.

composer require staudenmeir/laravel-adjacency-list:"^1.0"

Now head to your Category model, and add the HasRecursiveRelationships trait from this package.

use Staudenmeir\LaravelAdjacencyList\Eloquent\HasRecursiveRelationships;

class Category extends Model
{
    use HasFactory;
    use HasRecursiveRelationships;
}

We defined the parent column as parent_id earlier, but you can change this if you need to (read those docs!).

Ideally what we need now is one query to fetch the entire hierarchy so we can recursively iterate and display each item.

Head back over to your routes and grab those categories.

Route::get('/categories', function () {
    $categories = Category::tree()->get()->toTree();

    return view('categories', [
        'categories' => $categories
    ]);
});

There's a lot you can do with this package, but here, we're getting the entire tree (roots, children, children of children, etc).

In the categories view, iterate over the $categories.

@foreach ($categories as $category)
    <div>
        {{ $category->title }} ({{ $category->id }})
    </div>
@endforeach

You should see something like this.

Those are our root categories. Let's quickly iterate over the children and see what happens (ignore the fact I'm using inline styles here!).

@foreach ($categories as $category)
    <div>
        {{ $category->title }} ({{ $category->id }})

        @foreach ($category->children as $child)
            <div style="margin-left: 20px;">
                {{ $child->title }} ({{ $child->id }})
            </div>
        @endforeach
    </div>
@endforeach

Now you should see the children.

But wait, we have more categories under those children too. Do we keep adding an inner @foreach loop for every level we want to see?

You could do, but this limits you in the future if more children are added. What we need is a recursive solution.

Because we have a potentially huge tree, we'll create a Blade component that we can render inside itself. This is a future-proof solution regardless of how many categories we end up creating.

First, create a Blade component to deal with showing a category.

php artisan make:component CategoryItem

Move the code inside the @foreach to the category-item.blade.php file.

<div>
    {{ $category->title }} ({{ $category->id }})

    @foreach ($category->children as $child)
        <div style="margin-left: 20px;">
            {{ $child->title }} ({{ $child->id }})
        </div>
    @endforeach
</div>

And then render this component inside the category loop, in the main view.

@foreach ($categories as $category)
    <x-category-item :category="$category" />
@endforeach

Make sure you update the CategoryItem class to accept the Category in the constructor, or delete the CategoryItem.php file.

class CategoryItem extends Component
{
    /**
     * Create a new component instance.
     *
     * @return void
     */
    public function __construct(public Category $category)
    {
        //
    }
    
    // ...
}

Take a look in the browser, and you should see exactly the same result as before.

All that's left to do is recursively render the category item Blade component. Update the category-item.blade.php file to the following.

<div>
    {{ $category->title }} ({{ $category->id }})

    @foreach ($category->children as $child)
        <div style="margin-left: 20px;">
            <x-category-item :category="$child" />
        </div>
    @endforeach
</div>

Now check out the result.

It's our entire category tree! If you like, try adding some more child categories. Let's add more children to category ID 6.

php artisan tinker
App\Models\Category::factory()->times(2)->create(['parent_id' => 6]);

And like magic, we have more nesting.

Usually with relationships like this, we need to consider eager loading to avoid n+1 problems. It's also impossible to eager load a potentially unlimited amount of children of a model, because we'd need to always know how many levels of hierarchy there were.

Let's install Laravel Debugbar and see what the queries for this look like.

composer require barryvdh/laravel-debugbar --dev

As you can see, we have exactly one query to deal with all of this. The package we're using harnesses CTE and gives us back a collection of every item in the hierarchy. Even if you added thousands of children, you'd still only have one query.

There's a load more you can do with this package depending on your needs. But to be honest, it's worth pulling in just for this tree building functionality alone.

Often a massive pain point in development, we've solved a potentially huge amount of category (or other model) nesting with very little code.

Thanks for reading! If you found this article helpful, you might enjoy our practical screencasts too.
Author
Alex Garrett-Smith
Share :

Comments

No coments, yet. Be the first to leave a comment.

Tagged under

Table of contents