Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
A pageable Collection implementation for Laravel

Paginated Collections in Laravel

Use Case

Laravel provides pagination out of the box for Eloquent Collections, but you can't use that by default on ordinary Collections.

Collections do have the forPage() method, but it's more low-level, so it doesn't generate pagination links.

So you have to create a LengthAwarePaginator instance. But what if you want the behaviour to be the same as an Eloquent collection? Then use this macro!

The benefit of this is that the syntax and output is almost identical to the Eloquent Collection paginate() method and so it can (relatively) easily be swapped out for an Eloquent Collection when testing.

Installation

Feel free to copy the most relevant code into your project. You're free to use and adapt as you need.

Usage

There are 2 approaches below. Which one you use is up to you, but you don't need both. I personally prefer the macro method as I feel it's cleaner and works well with minimal effort, but it's not so good at working with your IDE (code hints etc) and can feel a little detached in some cases.

The macro way

If you prefer, add the Collection macro to a Service Provider. That way you can call paginate() on any collection:

collect([ ... ])->paginate( 20 );

See AppServiceProvider.php for a sample implementation.

The subclass way

Where you want a "pageable" collection that is distinct from the standard Illuminate\Support\Collection, implement a copy of Collection.php in your application and simply replace your use Illuminate\Support\Collection statements at the top of your dependent files with use App\Support\Collection:

// use Illuminate\Support\Collection
use App\Support\Collection;

$items = [];
$collection = (new Collection($items))->paginate(20);

Note that this method doesn't work with the collect() helper function.

<?php
namespace App\Providers;
use Illuminate\Support\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
class AppServiceProvider extends ServiceProvider
{
public function boot()
{
/**
* Paginate a standard Laravel Collection.
*
* @param int $perPage
* @param int $total
* @param int $page
* @param string $pageName
* @return array
*/
Collection::macro('paginate', function($perPage, $total = null, $page = null, $pageName = 'page') {
$page = $page ?: LengthAwarePaginator::resolveCurrentPage($pageName);
return new LengthAwarePaginator(
$this->forPage($page, $perPage),
$total ?: $this->count(),
$perPage,
$page,
[
'path' => LengthAwarePaginator::resolveCurrentPath(),
'pageName' => $pageName,
]
);
});
}
}
<?php
namespace App\Support;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection as BaseCollection;
class Collection extends BaseCollection
{
public function paginate($perPage, $total = null, $page = null, $pageName = 'page')
{
$page = $page ?: LengthAwarePaginator::resolveCurrentPage($pageName);
return new LengthAwarePaginator(
$this->forPage($page, $perPage),
$total ?: $this->count(),
$perPage,
$page,
[
'path' => LengthAwarePaginator::resolveCurrentPath(),
'pageName' => $pageName,
]
);
}
}
@hesammoosapour
Copy link

hesammoosapour commented Dec 8, 2020

@simonhamp Is there a way to move the code from AppServiceProvider to somewhere else to make it more clean ?
Thank You

@simonhamp
Copy link
Author

simonhamp commented Dec 8, 2020

@hesammoosapour can you define an objective level of ‘more clean’?

This is just a code snippet that you can put wherever you like. I’ve only suggested to put in your AppServiceProvider as this is likely to be available for most Laravel applications from a default install.

If you want to load some other file into the boot up sequence of your application to register this macro, you’re welcome to do so - it is your application after all 😊

I have tried to get this PR’d into Laravel core, but it was rejected. I believe it’s available in a Spatie package along with a bunch of other potentially-useful macros - that feels quite ‘clean’.

@simonhamp
Copy link
Author

simonhamp commented Dec 8, 2020

@terremoth 🤣

@terremoth
Copy link

terremoth commented Dec 9, 2020

FOR SAKE OF GOD
IS THIS DEFAULT ALREADY?

@12cassie34
Copy link

12cassie34 commented Dec 10, 2020

Hi Simon,

Thank you sooo much for sharing these awesome codes!

I implemented it successfully, I also wrote {{$myVariable->links()}} in my view.

It works perfectly until I added two {{$myVariable->links()}} in the same view.
The error message is: Method Illuminate\Support\Collection::links does not exist.

Currently, I only added one {{$myVariable->links()}}, and it works.
I'm not sure where the problem is, but maybe you would like to know this situation?

Thanks anyway for this nice solution!

@zaxwebs
Copy link

zaxwebs commented Jan 23, 2021

Thank you, @simonhamp! Was looking for simplePaginate().

@webtamizhan
Copy link

webtamizhan commented Jan 28, 2021

I am using JetStream with Livewire stack on Laravel, this is working good. Thanks!

@karakhanyans
Copy link

karakhanyans commented Feb 12, 2021

If someone needs a "Laravel API Resource" way of this solution, here is the Gist for it:

@andymnc
Copy link

andymnc commented Mar 4, 2021

@simonhamp thanks for the macro solution. very usefull.
Is there a way to make the '?page=' disappaiar and have instead a custom url (or at least to not have it visible on url)?

@terremoth
Copy link

terremoth commented Mar 4, 2021

@andymnc maybe you can put the "page" data into Session?
Something like

YourCollection::paginate(10, null, session()->get('page'))

Can you test that if it works?

@andymnc
Copy link

andymnc commented Mar 5, 2021

@andymnc maybe you can put the "page" data into Session?
Something like

YourCollection::paginate(10, null, session()->get('page'))

Can you test that if it works?

not working. on page 2, on url I see "?page=2"

@terremoth
Copy link

terremoth commented Mar 5, 2021

@andymnc, oh, right,
I think you gonna need to change the pagination links view to not add ?page=x in

<a href="?page=1">1</a> 
<a href="?page=2">2</a> 
<a href="?page=3">3</a> 
...

to something like a

<button class="btn btn-default" onclick="changeToPage(1)">1</button>
<button class="btn btn-default" onclick="changeToPage(2)">2</button>
<button class="btn btn-default" onclick="changeToPage(3)">3</button>
... and so on

That calls that JS function

function changeToPage(number) {
    // do an Ajax/fetch to your backend, sending this number to the backend set the new page into session
    // then ...
    window.location.reload();
}

in your backend

you going to do something like this:

//updating 
$page = $request->input('page');
session()->set('page', $page);

// then, in your controller:

YourCollection::paginate(10, null, session()->get('page'));

This might not be the BEST OF ALL solutions, just the first one I thought now, but certainly works

@andymnc
Copy link

andymnc commented Mar 9, 2021

Thanks @terremoth , you gave me ideas.
I managed to solve (so to have "/page/pagenumber" instead of "?page=pagenumber" in url and links) the way below.
As a reminder, I'm using the MACRO way.
First, php artisan vendor:publish --tag=laravel-pagination
in this way you can find the blade section of the paginator in resources/views/vendor/pagination/

In my case (Laravel 7.2) i cloned the bootstrap-4.blade.php into andymnc.com.blade in the same folder.
Then edited the new blade file like this:
$paginator->previousPageUrl() becomes: str_replace('?page=', '/page/',$paginator->previousPageUrl())
$element in str_replace('?page=', '/page/',$element)
$url in str_replace('?page=', '/page/',$url)
and $paginator->nextPageUrl() becomes str_replace('?page=', '/page/',$paginator->nextPageUrl()).

Then, you have to make a route (in routes/web.php) to manage it. I did something like:
Route::get('/myroute/page/{page}', ['as' => 'myroute-page', 'uses' => 'MyController@index']);

In the MyController:

public function index($page=null){
...
MyCollection::paginate(10, null, $page);
...

And finally I changed a little bit the AppServiceProvider.php made by @simonhamp

Collection::macro('paginate', function($perPage, $total = null, $page = null, $pageName = 'page') {
                $page = $page ?: LengthAwarePaginator::resolveCurrentPage($pageName);

                $currentPath    = LengthAwarePaginator::resolveCurrentPath();
                if (strpos($currentPath,'/page/') !== false){
                    list($currentPath,)    = explode('/page/',$currentPath);
                }
                return new LengthAwarePaginator(
                    $this->forPage($page, $perPage),
                    $total ?: $this->count(),
                    $perPage,
                    $page,
                    [
                        'path' => $currentPath, /*LengthAwarePaginator::resolveCurrentPath(),*/
                        'pageName' => $pageName,
                    ]

                );
            });

Maybe it can be done in a better way, anyway I hope it can help

@acirinelli
Copy link

acirinelli commented Mar 11, 2021

Change
$this->forPage($page, $perPage)
to
$this->forPage($page, $perPage)->values()

Thank you! I wasted hours trying to figure this out.

@lnfel
Copy link

lnfel commented Mar 25, 2021

Passing a collection in livewire component using a public property i. e. $this->data = collect(['name' => 'some name'])->paginate(10) causes some issues such as:

Livewire component's public property must be of type: [numeric, string, array, null, or boolean]. Only protected or private properties can be set as other types because JavaScript doesn't need to access them.

And turning the collection into an array is also a no no, since there is no links() method on the array function. Till I came upon some golden answer from Snapey: Livewire component;s public prop...

pass the object to the view in the render() method, along with pagination
it does not need to be a public prop

do it like this:

return view('livewire.equipment.food-processing', ['equipments' => collect(['name' => 'some name'])->paginate(10);

Cheers!

Edit: this alternative does not work with livewire's WithPagination trait

Don't forget to include @livewireStyles and @livewireScripts on the layout file

@sallmin
Copy link

sallmin commented Mar 29, 2021

I am so happy for this :) :)

@ChrisChoTW
Copy link

ChrisChoTW commented Apr 8, 2021

Thanks a lot. 👍

@miroslav-zdravkovic
Copy link

miroslav-zdravkovic commented Apr 25, 2021

@simonhampI spent hours trying to find the reason for the issue this implementation has, but I couldn't find it. I should go to the core of Laravel but I didn't. The problem persists and it's weird. ONLY for the first page data meta tag contains an array while for other pages 2, 3, 4, ... it just returns indexed record.

@jamesmacxy
Copy link

jamesmacxy commented Jul 11, 2021

Thank you for this!.

@agungprsty
Copy link

agungprsty commented Jul 13, 2021

thanks brother

@talha-my-glu
Copy link

talha-my-glu commented Jul 26, 2021

Thanks Bro 100% working!

@mortezapiri
Copy link

mortezapiri commented Aug 4, 2021

in page 2 in data collection data show object and object but it should give array and object
look here

@HoangNguyenNCC
Copy link

HoangNguyenNCC commented Aug 5, 2021

@mortezapiri

in page 2 in data collection data show object and object but it should give array and object
look here

I have same issue, and I use array_values for temporary :D

@PH7-Jack
Copy link

PH7-Jack commented Sep 23, 2021

AMAZING

@matthieumota
Copy link

matthieumota commented Oct 17, 2021

Thanks @simonhamp, I've some reviews on method if you want :

Collection::macro('paginate', function ($perPage, $total = null, $page = null, $pageName = 'page') {
    $page = $page ?: LengthAwarePaginator::resolveCurrentPage($pageName);

    return new LengthAwarePaginator(
        $total ? $this : $this->forPage($page, $perPage)->values(),
        $total ?: $this->count(),
        $perPage,
        $page,
        [
            'path' => LengthAwarePaginator::resolveCurrentPath(),
            'pageName' => $pageName,
        ]
    );
});

So, $total arg is useful only if you pass entire collection to Paginator (No call ->forPage), it suppose you already have slice your collection.
Also, ->values() is important because when slice collection, keys are preserved, we don't want that for paginator.

@h22k
Copy link

h22k commented Oct 30, 2021

hi simon, first page return result as expected.
{ "current_page": 1, "data": [ { "ID": 451, "post_author": 3, "post_date": "2017-11-12 11:39:22" },
but The data field return objects instead of array for page >1
{ "current_page": 2, "data": { "3": { "ID": 196, "post_author": 3, "post_date": "2017-11-03 08:45:17", "post_date_gmt": "2017-11-03 08:45:17",

Change $this->forPage($page, $perPage) to $this->forPage($page, $perPage)->values()

awesome. thx a lot!

@ossycodes
Copy link

ossycodes commented Nov 13, 2021

hi simon, first page return result as expected.
{ "current_page": 1, "data": [ { "ID": 451, "post_author": 3, "post_date": "2017-11-12 11:39:22" },
but The data field return objects instead of array for page >1
{ "current_page": 2, "data": { "3": { "ID": 196, "post_author": 3, "post_date": "2017-11-03 08:45:17", "post_date_gmt": "2017-11-03 08:45:17",

Change $this->forPage($page, $perPage) to $this->forPage($page, $perPage)->values()

thanks man!!!

@mnaviddoost
Copy link

mnaviddoost commented Jan 10, 2022

its AMAZING 🚀🚀🚀

@kkloster7
Copy link

kkloster7 commented Feb 24, 2022

Gracias barrilete cosmico!!!!!!, ídolo crack genio de la vida, acabas de salvar una institución en argentina.

@Pedro-HR
Copy link

Pedro-HR commented May 12, 2022

So, $total arg is useful only if you pass entire collection to Paginator (No call ->forPage), it suppose you already have slice your collection. Also, ->values() is important because when slice collection, keys are preserved, we don't want that for paginator.

Thanks !!!!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment