Может войдёшь?
Черновики Написать статью Профиль

Свой постраничный вывод с плюшками

laravel php paginator

Еще во времена laravel 3 поступило мне задание от очередного оптимизатора, которое заключалось в следующем:

  1. Постраничный вывод должен генерировать ссылки вида /news/page2, /news/page3 и тд
  2. Страницы /news/page1 не должно быть вообще, должно быть просто /news
  3. В title нужно добавить номер страницы как-то так: «оригинальный тайтл» — страница «номер страницы»
  4. Автоматическая генерация link rel="canonical" для страниц с номером, добавление link rel="prev" и link rel="next"

Посмотрев исходники я понял что проще написать свой FPagination. F — потому что fucking friendly.

Надежный как автомат Калашникова и простой как сатиновые трусы.

PHP
<?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(12).$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 && $this->currentPage $this->lastPage)
    {
      
$content $canonical.$prev.$next;
    }
    else
    {
      
$content $canonical.$prev;
    }

    return 
$content;
  }
}

Данная реализация заточена под Bulma framework но поменять классы на верстку Bootstrap или любую другую не сложно.

Как этим чудом пользоваться в Laravel?

Я делаю так:

Создаю функцию помощника, даже две

PHP
//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
<?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
<?php

namespace App\Http\Controllers;

class 
NewsController extends Controller {

    public function 
getIndex($currentPage null)
    {
        
$view view('news/index');

        
$view->news = \App\Models\News::pagination($currentPage30);

        return 
$view;
    }
}

Если метод контроллера принимает больше одного параметра, $currentPage = null должен быть последним.

В представлении 'news/index'

PHP
@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 пакет собирать лениво, мне проще так — как в старое доброе время.

Навеяно этим.

Как вы считаете, полезен ли этот материал? Да Нет

Написать комментарий

Разметка: ? ?

Авторизуйся, чтобы прокомментировать.