How can I paginate a merged collection in Laravel

2020-01-30 07:01发布

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);
}

7条回答
乱世女痞
2楼-- · 2020-01-30 07:18

Try following.

$arr = $pets->toArray();
$paginator->make($arr, count($arr), $perPage);
查看更多
狗以群分
3楼-- · 2020-01-30 07:21

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.

查看更多
Evening l夕情丶
4楼-- · 2020-01-30 07:24

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)
查看更多
时光不老,我们不散
5楼-- · 2020-01-30 07:26

You can add the following code for Collection in the Providers/AppServiceProvider.

    // 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);
查看更多
够拽才男人
6楼-- · 2020-01-30 07:30

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);
}
查看更多
啃猪蹄的小仙女
7楼-- · 2020-01-30 07:31

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;
}
查看更多
登录 后发表回答