Find the Page Number of a Model in Laravel's Pagination

March 3rd, 2025 • 4 minutes read time

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:

  1. Jump to the page a specific comment sits on (e.g. using ?commentId=3489 in the URL's query string). This should redirect us to somewhere like /comments?page=65.
  2. Redirect with a fragment (e.g. #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.

If you found this article helpful, you'll love our practical screencasts.
Author
Alex Garrett-Smith
Share :

Comments

No comments, yet. Be the first!