How can I paginate a merged collection in Laravel 5?
Asked Answered
H

10

45

I am creating a stream which contains two types of objects, BluePerson and RedPerson. To create the stream, I fetch all of both objects, then merge them into one collection. After doing so, I need to paginate them, however paginate is for eloquent models and DB queries, and not collections, it seems. I have seen a lot about manually creating a paginator, but the documentation, especially in the API is sparse (I can't even seem to find the arguments the Paginator class accepts.)

How can I paginate the results of merging my collections?

public function index()
{
    $bluePerson = BluePerson::all();
    $redPerson = RedPerson::all();

    $people = $bluePerson->merge($redPerson)->sortByDesc('created_at');


    return view('stream.index')->with('people', $people);
}
Heall answered 24/5, 2015 at 5:48 Comment(1)
K
45

however paginate is for eloquent models and DB queries, and not collections, it seems.

You are right. but there is ineed a paginator function for collections. forPage

Syntax:

Collection forPage(int $page, int $perPage)

Example:

Rest is simple.

public function foo()
{
    $collection = collect([1,2,3,4,5,6,7,8,9,0]);
    $items = $collection->forPage($_GET['page'], 5); //Filter the page var
    dd($items);
}
Kindergartner answered 24/5, 2015 at 9:13 Comment(3)
If you want similar syntax to the Eloquent Collection paginate() method, why not try one of these solutions? gist.github.com/simonhamp/549e8821946e2c40a617c85d2cf5af5eCwmbran
@Cwmbran great link was exactly what I needed.Vendor
Although simple, this approach expects that you have ALL the data in hand, thus it overloads the database query by getting excessive rows that will not be displayed at the moment. There are approches with better performance using the default paginate() method because it gets just the rows from database that will be displayed on the current page (through SKIP and TAKE).Keiko
G
35

If you want to use a LengthAwarePaginator simply instantiate one. As mentioned in the comments of a previous answer you will have to set the path for this. You will also need to make sure you resolve the "currentPage" and set the items to be returned before you instantiate the paginator. This can all be done before/on instantiation. So a function may look something like:

function paginateCollection($collection, $perPage, $pageName = 'page', $fragment = null)
{
    $currentPage = \Illuminate\Pagination\LengthAwarePaginator::resolveCurrentPage($pageName);
    $currentPageItems = $collection->slice(($currentPage - 1) * $perPage, $perPage);
    parse_str(request()->getQueryString(), $query);
    unset($query[$pageName]);
    $paginator = new \Illuminate\Pagination\LengthAwarePaginator(
        $currentPageItems,
        $collection->count(),
        $perPage,
        $currentPage,
        [
            'pageName' => $pageName,
            'path' => \Illuminate\Pagination\LengthAwarePaginator::resolveCurrentPath(),
            'query' => $query,
            'fragment' => $fragment
        ]
    );

    return $paginator;
}
Groome answered 13/10, 2016 at 15:23 Comment(2)
This solved my problem using laravel 5.3. I merged 2 collections as union caused problems and now pagination works +1Jandel
then how to use that ?Cryptic
N
21
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;
use Illuminate\Pagination\Paginator;

You can add the following code for Collection in the Providers/AppServiceProvider inside public function boot() .

    // Enable pagination
    if (!Collection::hasMacro('paginate')) {

        Collection::macro('paginate', 
            function ($perPage = 15, $page = null, $options = []) {
            $page = $page ?: (Paginator::resolveCurrentPage() ?: 1);
            return (new LengthAwarePaginator(
                $this->forPage($page, $perPage)->values()->all(), $this->count(), $perPage, $page, $options))
                ->withPath('');
        });
    }

Then, you can call paginate from a Collection, just like an Eloquent model. For example

$pages = collect([1, 2, 3, 4, 5, 6, 7, 8, 9])->paginate(5);
Novotny answered 7/2, 2019 at 22:28 Comment(5)
This code is amazing. With this you just have to add below mentioned paths in your AppServiceProvider boot() function. use Illuminate\Support\Collection; use Illuminate\Pagination\Paginator; use Illuminate\Pagination\LengthAwarePaginator;Cripps
This is the most "correct" answer, I think, if you're not going to use the built in Collection::forPage(). It is using the Collection data structure properly as opposed to simply putting all of this in some standalone function. Kudos++ to @NovotnyAmphichroic
Property [id] does not exist on the Eloquent builder instance.Safir
Laravel 5.2 BadMethodCallException: Method withPath does not exist. Comment ->withPath(''); CHANGE for ->setPath('')Actiniform
Amazing piece, just what neededBluma
S
8

You might try paginating both sets and merging them. You can find more information about pagination in the docs and the api. Here is an example of manually creating your own paginator...

$perPage = 20;
$blue = BluePerson::paginate($perPage / 2);
$red = RedPerson::paginate($perPage - count($blue));
$people = PaginationMerger::merge($blue, $red);

I have included the PaginationMerger class below.

use Illuminate\Pagination\LengthAwarePaginator;

class PaginationMerger
{
    /**
     * Merges two pagination instances
     *
     * @param  Illuminate\Pagination\LengthAwarePaginator $collection1
     * @param  Illuminate\Pagination\LengthAwarePaginator $collection2
     * @return Illuminate\Pagination\LengthAwarePaginator
     */
    static public function merge(LengthAwarePaginator $collection1, LengthAwarePaginator $collection2)
    {
        $total = $collection1->total() + $collection2->total();

        $perPage = $collection1->perPage() + $collection2->perPage();

        $items = array_merge($collection1->items(), $collection2->items());

        $paginator = new LengthAwarePaginator($items, $total, $perPage);

        return $paginator;
    }
}
Sammiesammons answered 16/9, 2015 at 22:34 Comment(2)
There is a problem with this approach. It resets the URL in the paginator, so you need to set it back using $paginator->setPath(request()->getPathInfo());Science
Also you can use first collection's URL with $paginator->setPath($collection1->resolveCurrentPath())Science
L
8

best way for paginate collection:

1- add this to boot function in \app\Providers\AppServiceProvider

       /*
         * use Illuminate\Support\Collection;
         * use Illuminate\Pagination\LengthAwarePaginator;
         *
         * 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,
                ]
            );
        });

2-From hereafter for all collection you can paginate like your code

$people->paginate(5)
Low answered 15/5, 2019 at 5:35 Comment(0)
K
3

I had to deal with something like that in a project i was working on, where in one of the pages i had to display two type of publication paginated and sorted by the created_at field. In my case it was a Post model and an Event Model (hereinafter referred to as publications).

The only difference is i didn't want to get all the publications from database then merge and sort the results, as you can imagine it would rise a performance issue if we have hundreds of publications.

So i figure out that it would be more convenient to paginate each model and only then, merge and sort them.

So here is what i did (based on answers and comments posted earlier)

First of all let me show you a simplified version of "my solution", then i will try to explain the code as much as i could.

use App\Models\Post;
use App\Models\Event;
use App\Facades\Paginator;


class PublicationsController extends Controller
{
    /**
     * Display a listing of the resource.
     *
     * @param \Illuminate\Http\Request $request
     * @return \Illuminate\Http\Response
     */
    public function index(Request $request)
    {
        $events       = Event::latest()->paginate(5);
        $posts        = Post::latest()->paginate(5);

        $publications = Paginator::merge($events, $posts)->sortByDesc('created_at')->get();

        return view('publications.index', compact('publications'));
    }
}

As you can guess it by now, the facade Paginator is the responsible of merging and sorting my paginators ($events & $posts)

To make this answer a little bit more clear and complete, i will show you how to create your own Facade.

You can choose to put your own facades anywhere you like, personally, i choose to put them inside Facades folder under the app folder, just like shown in this tree.

+---app
|   +---Console
|   +---Events
|   +---Exceptions
|   +---Exports
|   +---Facades
|   |   +---Paginator.php
|   |   +---...
|   +---Http
|   |   +---Controllers
.   .   +---...
.   .   .

Put this code inside app/Facades/Paginator.php

namespace App\Facades;

use Illuminate\Support\Facades\Facade;

class Paginator extends Facade
{
    /**
     * Get the registered name of the component.
     *
     * @return string
     */
    protected static function getFacadeAccessor()
    {
        return 'paginator';
    }
}

For more info, you can see How Facades Work

Next, bind paginator to service container, open app\Providers\AppServiceProvider.php

namespace App\Providers;

use App\Services\Pagination\Paginator;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        $this->app->bind('paginator', function ($app) {

            return new Paginator;
        });
    }
}

For more info, you can see The Boot Method

My Paginator class is under app/Services/Pagination/ folder. Again, you can put your classes wherever you like.

namespace App\Services\Pagination;

use Illuminate\Support\Arr;
use InvalidArgumentException;
use Illuminate\Support\Collection;
use Illuminate\Pagination\LengthAwarePaginator;

class Paginator
{
    /**
     * All of the items being paginated.
     *
     * @var \Illuminate\Support\Collection
     */
    protected $items;

    /**
     * The number of items to be shown per page.
     *
     * @var int
     */
    protected $perPage;

    /**
     * The total number of items before slicing.
     *
     * @var int
     */
    protected $total;

    /**
     * The base path to assign to all URLs.
     *
     * @var string
     */
    protected $path = '/';


    /**
     * Merge paginator instances
     *
     * @param  mixed $paginators
     * @param  bool  $descending
     * @return \Illuminate\Pagination\LengthAwarePaginator
     */
    function merge($paginators)
    {
        $paginators = is_array($paginators) ? $paginators : func_get_args();

        foreach ($paginators as $paginator) {
            if (!$paginator instanceof LengthAwarePaginator) {
                throw new InvalidArgumentException("Only LengthAwarePaginator may be merged.");
            }
        }

        $total   = array_reduce($paginators, function($carry, $paginator) {

            return $paginator->total();
        }, 0);

        $perPage = array_reduce($paginators, function($carry, $paginator) {

            return $paginator->perPage();
        }, 0);

        $items   = array_map(function($paginator) {

            return $paginator->items();

        }, $paginators);

        $items         = Arr::flatten($items);

        $items         = Collection::make($items);

        $this->items   = $items;
        $this->perPage = $perPage;
        $this->total   = $total;

        return $this;
    }

    /**
     * Sort the collection using the given callback.
     *
     * @param  callable|string  $callback
     * @param  int  $options
     * @param  bool  $descending
     * @return static
     */
    public function sortBy($callback, $options = SORT_REGULAR, $descending = false)
    {
        $this->items = $this->items->sortBy($callback, $options, $descending);

        return $this;
    }

    /**
     * Sort the collection in descending order using the given callback.
     *
     * @param  callable|string  $callback
     * @param  int  $options
     * @return static
     */
    public function sortByDesc($callback, $options = SORT_REGULAR)
    {
        return $this->sortBy($callback, $options, true);
    }

    /**
     * Get paginator
     *
     * @return \Illuminate\Pagination\LengthAwarePaginator
     */
    public function get()
    {
        return new LengthAwarePaginator(
            $this->items,
            $this->total,
            $this->perPage,
            LengthAwarePaginator::resolveCurrentPage(),
            [
                'path' => LengthAwarePaginator::resolveCurrentPath(),
            ]
        );
    }
}

Definitely there is room for improvements, so please if you see something that needs to be changed, leave a comment here or reach me on twitter.

Knowland answered 5/10, 2019 at 22:45 Comment(0)
E
1

Try following.

$arr = $pets->toArray();
$paginator->make($arr, count($arr), $perPage);
Etiquette answered 24/5, 2015 at 6:2 Comment(1)
How I can use the paginator object to get the proper items for the selected page?Rhamnaceous
E
1

You can change this like bellow:

public function index()
{
    $bluePerson = BluePerson::paginate();
    $redPerson = RedPerson::all();

    $people = $bluePerson->merge($redPerson)->sortByDesc('created_at');


    return view('stream.index')->with('people', $people);
}
Extensity answered 19/2, 2021 at 15:29 Comment(0)
S
0

It seems that pagination is no longer a part of the collection in laravel 8, so I used laravel's Illuminate\Pagination\Paginator class in order to paginate data, but was a problem, pagination related information was updating by paginating but the data did not paginate at all!

I have found the problem, laravel's Paginator class did not paginate data correctly, you can see the original method of the class.

/**
 * Set the items for the paginator.
 *
 * @param  mixed  $items
 * @return void
 */
protected function setItems($items)
{
    $this->items = $items instanceof Collection ? $items : Collection::make($items);

    $this->hasMore = $this->items->count() > $this->perPage;

    $this->items = $this->items->slice(0, $this->perPage);
}

So, I have built my own Paginator class and extended it from laravel's Paginator class, and fixed the problem as I have shown you below.

use Illuminate\Support\Collection;

class Paginator extends \Illuminate\Pagination\Paginator
{
    /**
     * Set the items for the paginator.
     *
     * @param  mixed  $items
     * @return void
     */
    protected function setItems($items)
    {
        $this->items = $items instanceof Collection ? $items : Collection::make($items);

        $this->hasMore = $this->items->count() > ($this->perPage * $this->currentPage);

        $this->items = $this->items->slice(
            ($this->currentPage - 1) * $this->perPage,
            $this->perPage
        );
    }
}

The usage of the class is like the bellow

(new Paginator(
    $items,
    $perPage = 10,
    $page = 1, [
        'path' => $request->url(),
    ]
))->toArray(),

Note: If you want to use the laravel's Paginator for views, you can use the render() method instead of the toArray() method.

My Pagination on data is working fine now.

I hope this be useful for you.

Sororicide answered 7/1, 2022 at 9:30 Comment(0)
H
0
use Illuminate\Support\Collection;

$collection = new Collection;

$collectionA = ModelA::all();
$collectionB = ModelB::all();

$merged_collection = $collectionA->merge($collectionB);

foreach ($merged_collection as $item) {

    $collection->push($item);
}

$paginated_collection = $collection->paginate(10);
Hanahanae answered 12/1, 2022 at 0:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.