In this article, I'll show you how to calculate which page a specific model record sits within in Laravel, even if you're dealing with nested relationships.
We'll use the example of paginated comments. We want to be able to:
?commentId=3489
in the URL's query string). This should redirect us to somewhere like /comments?page=65
.#comment-3489
) to jump down to that comment.This functionality is handy if you have a notification system within your application and need the ability to jump directly to a model, regardless of which page it's on.
Some basic setup will help you understand the rest of the article and how everything fits together. We'll also discuss finding the page where a reply to a comment sits.
The comment schema allows us to reference a parent_id
for comment replies:
Schema::create('comments', function (Blueprint $table) {
$table->id();
$table->text('body');
$table->foreignId('parent_id')->nullable()->constrained('comments');
$table->timestamps();
});
And here's the comment model with the children
relationship for replies:
class Comment extends Model
{
public function children()
{
return $this->hasMany(Comment::class, 'parent_id', 'id')
->oldest();
}
}
I've created a simple route to demonstrate everything, passing down paginated comments:
Route::get('/comments', function (Request $request) {
$comments = Comment::with('children')->latest()->whereNull('parent_id');
return view('comments', [
'comments' => $comments->simplePaginate(10)
]);
})
->name('comments');
And here's what the view looks like:
<div>
@foreach($comments as $comment)
<x-comment-item :comment="$comment" />
@foreach($comment->children as $child)
<div style="margin-left: 20px;">
<x-comment-item :comment="$child" />
</div>
@endforeach
@endforeach
<div>
{{ $comments->links() }}
</div>
</div>
This shows all top-level comments and replies (children) to comments. The comment-item
Blade component is pretty simple:
<div id="comment-{{ $comment->id }}">
<h3>#{{ $comment->id }}</h3>
<p>{{ $comment->body }}</p>
</div>
Your setup may be different, but I hope that's given you a good idea of what we're working with.
Let's start to detect and redirect to the page a specific model is on!
We'll begin with simple, top-level detection and redirection and take it step-by-step.
First, we'll find the index of a commentId
we pass into the URL. If this was the first comment in the list, we'd get 0 as the index. If it were 11th in the list (potentially on page 2 if we were splitting pages by 10), the index would be 10.
Route::get('/comments', function (Request $request) {
$comments = Comment::with('children')->latest()->whereNull('parent_id');
if ($commentId = (int) $request->get('commentId')) {
$index = $comments->get()->search(function (Comment $comment) use ($commentId) {
return $comment->id === $commentId;
});
dd($index); // the index position of the commentId
}
return view('comments', [
'comments' => $comments->simplePaginate(10)
]);
})
->name('comments');
So, we have the index, how do we work out the page?
With knowing how many records we're displaying per page, the calculation is simple:
Route::get('/comments', function (Request $request) {
$comments = Comment::with('children')->latest()->whereNull('parent_id');
if ($commentId = (int) $request->get('commentId')) {
$index = $comments->get()->search(function (Comment $comment) use ($commentId) {
return $comment->id === $commentId;
});
$page = ceil(($index + 1) / 10);
}
return view('comments', [
'comments' => $comments->simplePaginate(10)
]);
})
->name('comments');
That's it! $page
will now contain the page the given comment lives on.
Lastly, just redirect the user to that page:
Route::get('/comments', function (Request $request) {
$comments = Comment::with('children')->latest()->whereNull('parent_id');
if ($commentId = (int) $request->get('commentId')) {
$index = $comments->get()->search(function (Comment $comment) use ($commentId) {
return $comment->id === $commentId;
});
$page = ceil(($index + 1) / 10);
return redirect(
Uri::route('comments')
->withQuery(['page' => $page])
->withFragment('comment-' . $commentId)
);
}
return view('comments', [
'comments' => $comments->simplePaginate(10)
]);
})
->name('comments');
Here, I use the Uri
helper in Laravel to build up a Uri to redirect to. I also include the fragment (e.g. #comment-68
), which will jump to that comment.
So now, a page like /comments?commentId=4569
will redirect to something like /comments?page=65#comment-4569
.
If that's all you need, we're done! You can refactor/move this functionality away from your controller if required.
Let's now look at finding the page a nested comment (reply) sits on.
To find a nested comment's page, we need to adjust the lookup of the $index
we're looking for.
This logic will completely vary based on what you're working with, so adjust the code as needed.
Route::get('/comments', function (Request $request) {
$comments = Comment::with('children')->latest()->whereNull('parent_id');
if ($commentId = (int) $request->get('commentId')) {
$index = $comments->get()->search(function (Comment $comment) use ($commentId) {
return $comment->id === $commentId ||
$comment->children()->oldest()->get()->contains('id', $commentId);
});
$page = ceil(($index + 1) / 10);
return redirect(
Uri::route('comments')
->withQuery(['page' => $page])
->withFragment('comment-' . $commentId)
);
}
return view('comments', [
'comments' => $comments->simplePaginate(10)
]);
})
->name('comments');
So, the only change required is looking inside the children
of comments to see if the given comment exists as a reply.
If that's the case, we return the page for the top-level comment and redirect as normal, and we still jump to the nested comment with the fragment.
I've used this technique for finding and redirecting to the page a model sits on multiple times, and it works well.
This will slow down for very large data collections since we're using search
on a Laravel collection and not checking at the database level. I wouldn't worry about this initially — just monitor it.