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.