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.