Еще во времена laravel 3 поступило мне задание от очередного оптимизатора, которое заключалось в следующем:
- Постраничный вывод должен генерировать ссылки вида /news/page2, /news/page3 и тд
- Страницы /news/page1 не должно быть вообще, должно быть просто /news
- В title нужно добавить номер страницы как-то так: «оригинальный тайтл» — страница «номер страницы»
- Автоматическая генерация link rel="canonical" для страниц с номером, добавление link rel="prev" и link rel="next"
Посмотрев исходники я понял что проще написать свой FPagination. F — потому что fucking friendly.
Надежный как автомат Калашникова и простой как сатиновые трусы.
<?php
namespace App\Classes;
class FPagination {
public function __construct($items, $currentPage, $lastPage, $perPage = '20', $pageName = 'page', $url)
{
$this->items = $items;
$this->currentPage = $currentPage;
$this->lastPage = $lastPage;
$this->pageName = $pageName;
$this->url = $url;
}
public function links()
{
if($this->lastPage > 1)
{
if($this->lastPage < 13)
{
$content = $this->getPageRange(1, $this->lastPage);
}
else
{
$content = $this->getPageSlider();
}
$pagination = '<nav class="pagination">'
.$this->getPreviousTouch()
.$this->getNextTouch()
.'<ul class="pagination-list is-hidden-mobile">'
.$this->getPrevious()
.$content
.$this->getNext()
.'</ul></nav>';
}
else
{
$pagination = '';
}
return $pagination;
}
public function getPageRange($start, $end)
{
$pages = '';
for($page = $start; $page <= $end; $page++)
{
if($this->currentPage == $page)
{
$pages .= $this->getActivePageWrapper($page);
}
else if($page == 1)
{
$pages .= $this->getBase($page);
}
else
{
$pages .= $this->getLink($page);
}
}
return $pages;
}
public function getActivePageWrapper($text)
{
return '<span class="pagination-link is-current">'.$text.'</span>';
}
public function getBase($page)
{
$link = '<a class="pagination-link" href="'.$this->url.'">'.$page.'</a>';
return $this->getPageLinkWrapper($link);
}
public function getPageLinkWrapper($link)
{
return '<li>'.$link.'</li>';
}
public function getLink($page)
{
$link = '<a class="pagination-link" href="'.$this->url.'/'.$this->pageName.$page.'">'.$page.'</a>';
return $this->getPageLinkWrapper($link);
}
protected function getPageSlider()
{
$window = 6;
if($this->currentPage <= $window)
{
$content = $this->getPageRange(1, $window + 2).$this->getFinish();
}
else if($this->currentPage >= $this->lastPage - $window)
{
$start = $this->lastPage - 8;
$content = $this->getStart().$this->getPageRange($start, $this->lastPage);
}
else
{
$content = $this->getStart().$this->getAdjacentRange().$this->getFinish();
}
return $content;
}
public function getFinish()
{
return $this->getDots().$this->getPageRange($this->lastPage - 1, $this->lastPage);
}
public function getDots()
{
return '<li class="pagination-ellipsis">...</li>';
}
public function getStart()
{
return $this->getPageRange(1, 2).$this->getDots();
}
public function getAdjacentRange()
{
return $this->getPageRange($this->currentPage - 3, $this->currentPage + 3);
}
public function getPreviousTouch($text = '« Предыдущая')
{
if($this->currentPage <= 1)
{
$link = '<span class="pagination-previous is-hidden-tablet" disabled="disabled">'.$text.'</span>';
}
else
{
$link = '<a class="pagination-previous is-hidden-tablet" href="'.$this->prevUrl().'">'.$text.'</a>';
}
return $link;
}
public function prevUrl()
{
if($this->currentPage <= 2)
{
$url = $this->url;
}
else
{
$url = $this->url.'/'.$this->pageName.($this->currentPage - 1);
}
return $url;
}
public function getNextTouch($text = 'Следующая »')
{
if($this->currentPage >= $this->lastPage)
{
$link = '<span class="pagination-next is-hidden-tablet" disabled="disabled">'.$text.'</span>';
}
else
{
$link = '<a class="pagination-next is-hidden-tablet" href="'.$this->nextUrl().'">'.$text.'</a>';
}
return $link;
}
public function nextUrl()
{
return $this->url.'/'.$this->pageName.($this->currentPage + 1);
}
public function getPrevious($text = '«')
{
if($this->currentPage <= 1)
{
return $this->getDisabledTextWrapper($text);
}
else
{
$link = '<a class="pagination-link" href="'.$this->prevUrl().'">'.$text.'</a>';
}
return $this->getPageLinkWrapper($link);
}
public function getDisabledTextWrapper($text)
{
return '<li class="pagination-link" disabled="disabled">'.$text.'</li>';
}
public function getNext($text = '»')
{
if($this->currentPage >= $this->lastPage)
{
return $this->getDisabledTextWrapper($text);
}
$link = '<a class="pagination-link" href="'.$this->nextUrl().'">'.$text.'</a>';
return $this->getPageLinkWrapper($link);
}
public function getTitleAppend($text)
{
if($this->currentPage > 1)
{
$title = $text.' - страница '.$this->currentPage;
}
else
{
$title = $text;
}
return $title;
}
public function getSeoBlock()
{
$canonical = '<link rel="canonical" href="'.$this->url.'">';
$prev = '<link rel="prev" href="'.$this->prevUrl().'">';
$next = '<link rel="next" href="'.$this->nextUrl().'">';
if($this->currentPage <= 1)
{
$content = $next;
}
else if($this->currentPage > 1 && $this->currentPage < $this->lastPage)
{
$content = $canonical.$prev.$next;
}
else
{
$content = $canonical.$prev;
}
return $content;
}
}
Данная реализация заточена под Bulma framework но поменять классы на верстку Bootstrap или любую другую не сложно.
Как этим чудом пользоваться в Laravel?
Создаю функцию помощника, даже две
//resources/helpers.php
<?php
// эта функция приводит $currentPage к нужному для FPagination виду. Можно и без нее, если не нужна дополнительная логика, или если она реализована иначе в другом месте. Например через Middleware
function currentPage($currentPage = null)
{
$pageName = 'page';
$oldPage = $currentPage;
if(is_null($currentPage))
{
$currentPage = 1;
}
else
{
$currentPage = str_replace($pageName, '', $currentPage);
$currentPage = (int)preg_replace("/[^0-9]/", '', $currentPage);
$newPage = $pageName.$currentPage;
if($newPage != $oldPage or $newPage == 'page1')
{
abort(404);
}
}
return $currentPage;
}
// эта функция формирует параметры для передачи в FPagination и возвращает результат. Да, комментатор из меня тот еще
function FPagination($sql, $currentPage, $perPage = 20, $action = null)
{
$pageName = 'page';
if(is_null($action))
{
$action = str_replace('App\Http\Controllers\\', '', \Route::currentRouteAction());
}
$currentPage = currentPage($currentPage);
$total = $sql->count();
$lastPage = ceil($total / $perPage);
if($currentPage > $lastPage)
{
abort(404);
}
else
{
$items = $sql->skip(($currentPage * $perPage) - $perPage)
->take($perPage)
->get();
$routeParameters = array_values(\Route::current()
->parameters());
$count = count($routeParameters);
if($count >= 1)
{
if(starts_with($routeParameters[$count - 1], $pageName))
{
unset($routeParameters[$count - 1]);
}
}
$url = action($action, $routeParameters);
return new \App\Classes\FPagination($items, $currentPage, $lastPage, $perPage, $pageName, $url);
}
}
Еще нужно добавить scope в модель. Можно сделать глобальный, но я предпочитаю локальные, на случай дополнительных доработок. Можно и без него, это скорее для красоты.
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class News extends Model {
public function scopePagination($query, $currentPage, $perPage = 20, $action = null)
{
return FPagination($query, $currentPage, $perPage, $action);
}
}
В контроллере это выглядит так:
<?php
namespace App\Http\Controllers;
class NewsController extends Controller {
public function getIndex($currentPage = null)
{
$view = view('news/index');
$view->news = \App\Models\News::pagination($currentPage, 30);
return $view;
}
}
Если метод контроллера принимает больше одного параметра, $currentPage = null должен быть последним.
@extends('base/page')
@section('meta')
{{ $users->getSeoBlock() }}
@endsection
@section('title')
{{ $users->getTitleAppend('Новости') }}
@endsection
@section('content')
@foreach($news->items as $item)
@endforeach
{{ $news->links() }}
@endsection
Данный класс удобно тягать с собой из проекта в проект, довольно просто настраивается под любую верстку, если почесать репу можно прикрутить и к другим фреймворкам или формировать им постраничный вывод для массивов\коллекций. Composter Composer пакет собирать лениво, мне проще так — как в старое доброе время.
Навеяно этим.