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);
}
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);
}
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;
}
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;
}
}
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);
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)
Try following.
$arr = $pets->toArray();
$paginator->make($arr, count($arr), $perPage);
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.