Skip to content

Instantly share code, notes, and snippets.

@simonhamp
Last active October 8, 2024 15:01
Show Gist options
  • Save simonhamp/549e8821946e2c40a617c85d2cf5af5e to your computer and use it in GitHub Desktop.
Save simonhamp/549e8821946e2c40a617c85d2cf5af5e to your computer and use it in GitHub Desktop.
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;

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

Note that this approach won't work with the collect() helper function.

More Collection goodies

Spatie have create an awesome Composer package of loads of useful Collection macros. Go check it out!

<?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'): LengthAwarePaginator {
$page = $page ?: LengthAwarePaginator::resolveCurrentPage($pageName);
return new LengthAwarePaginator(
$this->forPage($page, $perPage)->values(),
$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'): LengthAwarePaginator
{
$page = $page ?: LengthAwarePaginator::resolveCurrentPage($pageName);
return new LengthAwarePaginator(
$this->forPage($page, $perPage)->values(),
$total ?: $this->count(),
$perPage,
$page,
[
'path' => LengthAwarePaginator::resolveCurrentPath(),
'pageName' => $pageName,
]
);
}
}
@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

Thanks a lot. 👍

@miroslav-zdravkovic
Copy link

@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

Thank you for this!.

@agungprsty
Copy link

thanks brother

@talha-my-glu
Copy link

Thanks Bro 100% working!

@mortezapiri
Copy link

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

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

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

its AMAZING 🚀🚀🚀

@kkloster7
Copy link

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

@Pedro-HR
Copy link

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 !!!!

@edwardalgorist
Copy link

This is great, man!
Thank you.

@seyionifade
Copy link

Thank you for this!

For anyone seeing this, you could also add it as helper class.

@B-Carcamo
Copy link

B-Carcamo commented Nov 26, 2022

I have a problem with livewire when using links

//Error
Undefined variable $data

public function render()
    {
        $data = (new Collection($this->managers))->paginate(6);

        return view('livewire.admin.create-student',compact('data'));
    }

//livewire component

 <div class="pt-5">
            {{ $data->links() }}
 </div>

@terremoth
Copy link

terremoth commented Nov 26, 2022

I have a problem with livewire when using links

//Error Undefined variable $data

public function render()
{
    $data = (new Collection($this->managers))->paginate(6);

    return view('livewire.admin.create-student',compact('data'));
}

//livewire component

 <div class="pt-5">
     {{ $data->links() }}
 </div>

@B-Carcamo DId you try:

return view('livewire.admin.create-student', ['data' => $data]);

instead of compact('data') ?

@gilcecler
Copy link

I do not know why

image
page 2 goes from 95 to 250 m2
image
if I put more than 10 in $collection = (new Collection($data))->paginate(15);
does not show the pagination bar

@yaddly
Copy link

yaddly commented Jan 15, 2023

Good day, I tried all the above and nothing worked. I stumbled across this article on Stack Overflow and it used a method called toQuery(). This method, returns an Eloquent query builder instance containing a whereIn constraint on the collection model's primary keys. Invoke paginate(10) method from it.

$movies= Movie::get()->toQuery()->paginate(20);

@patrykszady
Copy link

patrykszady commented Jan 29, 2023

Hi, works great with Livewire. How can I use this with simplePaginate so only next and previous buttons are shown in the links() ?

Edit: looks like simplePaginate works at the mobile breakpoint. Using Livewire and Tailwind.

@yaddly
Copy link

yaddly commented Jan 31, 2023

Hi, works great with Livewire. How can I use this with simplePaginate so only next and previous buttons are shown in the links() ?

Edit: looks like simplePaginate works at the mobile breakpoint. Using Livewire and Tailwind.

@patrykszady, In order to use simplePaginate() method, kindly change the above sample code to the following:

$movies= Movie::get()->toQuery()->simplePaginate(20);

To customize your links please read the documentation here first and also look at a tutorial here.

@mrtorks
Copy link

mrtorks commented Feb 17, 2023

An update to this Anyone with multiple Paginators on a page running into an error should go with this approach. In my case i have Livewire components and trying to paginate a custom collection was a pain until I implemented the solution below.
Use the macro function with all the corrections up until this point. If possible, rename the function to avoid conflicts.
Do this to set a custom page name which would allow you reset and not affect other paginators
return view('foo.bar', ['foo' => ,(your-custom-collection)->paginateCustom(3, null,null, 'barPage')]);

@simonhamp
Copy link
Author

I've updated the code to reflect some of the comments and adjustments that others have shared in earlier comments here.

@jd-bowling
Copy link

Just what I've been looking for. This will allow me to search custom model attributes in my paginated table.

@devmount
Copy link

Wow, thank you @simonhamp! This really helped me out on collections containing different eloquent models 😍

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