Back to articles

How to Paginate a Laravel Collection

March 14th, 2024

If you're merging multiple models into a Laravel collection or just have a collection of data you need to paginate, here's an easy, clean way to do it.

By the end of this article, you'll be able to make use of a custom paginate method on any collection, like this.

collect([1, 2, 3, 4, 5])->paginate(2);

First, let's look at an example of merging two different models into one collection.

$articles = Article::get();
$courses = Course::get();

$items = collect($articles)->merge($courses)->sortByDesc('created_at');

Great, we can now iterate on the collection inside a view — but since there's no paginate method on a Laravel Collection, we're not able to paginate just yet.

Manually building a Laravel Paginator

Here's how we'd manually build up a paginator for this collection of models. We'll tidy this up shortly.

// Our merged collection from before
$items = collect($articles)->merge($courses)->sortByDesc('created_at');

$perPage = 2;
$page = LengthAwarePaginator::resolveCurrentPage('page');

return new LengthAwarePaginator($this->forPage($page, $perPage), $this->count(), $perPage, $page, [
    'path' => LengthAwarePaginator::resolveCurrentPath(),
    'query' => request()->query(),
]);

Ok, there's quite a bit going on here. Let's break it down.

  1. A LengthAwarePaginator is exactly what it sounds like. It's aware of the amount of items that need paginating to break them up. This is the same thing that gets returned when you use paginate on a normal Eloquent Builder query.
  2. The LengthAwarePaginator takes in 5 arguments. The items, the total number of items, how many items to show per page, the current page, and some options.
  3. LengthAwarePaginator::resolveCurrentPage('page'), $perPage) breaks up our manual collection into chunks (of 2 per page in this case). Because our data can't be queried at the database level, we have to break up the data manually.
  4. $items->count() just gives the total number of items in our collection so the paginator can work out how many pages to show.
  5. The options argument contains the current path of the page using LengthAwarePaginator::resolveCurrentPath(), and the query is the current request query, in case you want to merge anything into the request while still keeping your pagination.

Ok, hopefully, that makes sense. Let's put it into action.

<div>
    @foreach ($items as $item)
        @if (get_class($item) === App\Models\Course::class)
            <div>{{ $item->title }} ({{ $item->duration }})</div>
        @endif

        @if (get_class($item) === App\Models\Article::class)
            <div>{{ $item->title }}</div>
        @endif
    @endforeach

    {{ $items->links() }}
</div>

Since we're combining more than one model type, I've included the example of displaying different data for each type we've merged. Of course, the important part is the $items->links() at the bottom, which will now work!

Great, pagination is working. But we can tidy things up and make our collection pagination reusable.

Tidying up with a Collection macro

We don't want to copy and paste this code every time we need to paginate a collection. So, let's create a Collection macro.

In the AppServiceProvider boot method, add this.

public function boot(): void
{
    Collection::macro('paginate', function ($perPage = 10) {
        $page = LengthAwarePaginator::resolveCurrentPage('page');

        return new LengthAwarePaginator($this->forPage($page, $perPage), $this->count(), $perPage, $page, [
            'path' => LengthAwarePaginator::resolveCurrentPath(),
            'query' => request()->query(),
        ]);
    });
}

This is more or less the same code we used before, but it's now tucked away in a new method on the Collection class.

Let's update our controller.

$items = collect($articles)->merge($courses)->sortByDesc('created_at')->paginate(2);

Much cleaner. And, we now now reuse this paginate method globally to paginate any collection!

Quick warning

If you're dealing with a huge amount of records, it's important to mention that all of your data will be loaded into memory when you initially create a collection like this. With database pagination, only the records within the requested page are loaded into memory — which is one of the reasons for pagination!

However, for smaller collections of models or custom data that you need to paginate through, this solution works nicely.

Author
Alex Garrett-Smith

Comments

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