Laravel Database——Paginate 分页服务源码分析

paginate 分页

laravel 的分页用起来非常简单,只需要对 query 调用 paginate 函数,把返回的对象扔给前端 blade 文件,在 blade 文件调用函数 render 函数或者 link 函数,就可以得到 上一页、下一页 等等分页特效。

实际上,我们可以简单地把分页服务看作一个前端资源,render 函数或者 link 函数的结果就是分页前端代码。

如果你还对 laravel 的分页不是很熟悉,请先阅读官方文档 : 分页。

分页服务的启动

分页功能也是由一个服务提供者所启动的,PaginationServiceProvider 就是负责注册和启动分页服务的服务提供者:

class PaginationServiceProvider extends ServiceProvider
{
    public function register()
    {
        Paginator::viewFactoryResolver(function () {
            return $this->app['view'];
        });

        Paginator::currentPathResolver(function () {
            return $this->app['request']->url();
        });

        Paginator::currentPageResolver(function ($pageName = 'page') {
            $page = $this->app['request']->input($pageName);

            if (filter_var($page, FILTER_VALIDATE_INT) !== false && (int) $page >= 1) {
                return $page;
            }

            return 1;
        });
    }
}

我们看到,服务提供者的注册函数为 Paginator 设置三个闭包函数:

  • viewFactoryResolver 为 Paginator 设置了生成前端资源的类,用于获取分页前端代码。
  • currentPathResolver 为 Paginator 设置了 url 的地址。我们知道, 上一页、下一页 等等都是可以执行翻页的操作,所以实际上这些按钮必然含有链接,而链接的地址就是当前请求的 url 地址,不同的按钮的链接地址只是 page
    的参数不同而已。
  • currentPageResolver 为 Paginator 获取了当前的页数。
public function boot()
{
    $this->loadViewsFrom(__DIR__.'/resources/views', 'pagination');

    if ($this->app->runningInConsole()) {
        $this->publishes([
            __DIR__.'/resources/views' => $this->app->resourcePath('views/vendor/pagination'),
        ], 'laravel-pagination');
    }
}

protected function loadViewsFrom($path, $namespace)
{
    if (is_dir($appPath = $this->app->resourcePath().'/views/vendor/'.$namespace)) {
        $this->app['view']->addNamespace($namespace, $appPath);
    }

    $this->app['view']->addNamespace($namespace, $path);
}

服务的启动函数为分页服务设置了默认的前端分页资源。

分页服务 paginator

分页服务 paginator 函数用于 queryBuilder,用于获取分页的数据库数据:

public function paginate($perPage = 15, $columns = ['*'], $pageName = 'page', $page = null)
{
    $page = $page ?: Paginator::resolveCurrentPage($pageName);

    $total = $this->getCountForPagination($columns);

    $results = $total
        ? $this->forPage($page, $perPage)->get($columns) : collect();

    return $this->paginator($results, $total, $perPage, $page, [
        'path' => Paginator::resolveCurrentPath(),
        'pageName' => $pageName,
    ]);
}

protected function paginator($items, $total, $perPage, $currentPage, $options)
{
    return Container::getInstance()->makeWith(LengthAwarePaginator::class, compact(
        'items', 'total', 'perPage', 'currentPage', 'options'
    ));
}

也就是说,当我们写下这样的代码时:

DB::table('user')->select('*')->where('status',1)->paginator();

我们可以获取到一个 LengthAwarePaginator 类对象,对这个对象调用 render 函数就可以获取分页前端资源。

我们先来研究一下 paginator 函数。

获取当前页

我们可以看到,在这个函数中程序先获取当前页数:

public static function resolveCurrentPage($pageName = 'page', $default = 1)
{
    if (isset(static::$currentPageResolver)) {
        return call_user_func(static::$currentPageResolver, $pageName);
    }

    return $default;
}

currentPageResolver 就是上一节中 currentPageResolver 设置的闭包函数,这个闭包函数从请求参数中获取当前页:

$page = $this->app['request']->input($pageName);

获取数据库总记录数
计算数据库符合搜索条件的总记录数理所当然的是使用聚合函数 count :

public function getCountForPagination($columns = ['*'])
{
    $results = $this->runPaginationCountQuery($columns);

    if (isset($this->groups)) {
        return count($results);
    } elseif (! isset($results[0])) {
        return 0;
    } elseif (is_object($results[0])) {
        return (int) $results[0]->aggregate;
    } else {
        return (int) array_change_key_case((array) $results[0])['aggregate'];
    }
}

protected function runPaginationCountQuery($columns = ['*'])
{
    return $this->cloneWithout(['columns', 'orders', 'limit', 'offset'])
                ->cloneWithoutBindings(['select', 'order'])
                ->setAggregate('count', $this->withoutSelectAliases($columns))
                ->get()->all();
}
获取当前页数据

获取当前页当然是使用 forPage 函数:

$results = $total
        ? $this->forPage($page, $perPage)->get($columns) : collect();

初始化 LengthAwarePaginator

paginator 函数利用 Ioc 容器来生成 LengthAwarePaginator 实例:

protected function paginator($items, $total, $perPage, $currentPage, $options)
{
    return Container::getInstance()->makeWith(LengthAwarePaginator::class, compact(
        'items', 'total', 'perPage', 'currentPage', 'options'
    ));
}

LengthAwarePaginator 的初始化:

public function __construct($items, $total, $perPage, $currentPage = null, array $options = [])
{
    foreach ($options as $key => $value) {
        $this->{$key} = $value;
    }

    $this->total = $total;
    $this->perPage = $perPage;
    $this->lastPage = max((int) ceil($total / $perPage), 1);
    $this->path = $this->path !== '/' ? rtrim($this->path, '/') : $this->path;
    $this->currentPage = $this->setCurrentPage($currentPage, $this->pageName);
    $this->items = $items instanceof Collection ? $items : Collection::make($items);
}

分页资源 render

对 LengthAwarePaginator 调用 render 函数会得到分页所需要的前端资源:

public function render($view = null, $data = [])
{
    return new HtmlString(static::viewFactory()->make($view ?: static::$defaultView, array_merge($data, [
        'paginator' => $this,
        'elements' => $this->elements(),
    ]))->render());
}

当我们使用默认的分页样式的时候,不需要向 render 函数传入 view 参数,此时程序会自动加载默认的前端资源:

public static $defaultView = 'pagination::default';

该资源的默认地址是 illuminate\Pagination\resources\views\default.blade.php:

@if ($paginator->hasPages())
    <ul class="pagination">
        {{-- Previous Page Link --}}
        @if ($paginator->onFirstPage())
            <li class="disabled"><span><<</span></li>
        @else
            <li><a href="{{ $paginator->previousPageUrl() }}" rel="prev"><<</a></li>
        @endif

        {{-- Pagination Elements --}}
        [@foreach](https://learnku.com/users/5651) ($elements as $element)
            {{-- "Three Dots" Separator --}}
            @if (is_string($element))
                <li class="disabled"><span>{{ $element }}</span></li>
            @endif

            {{-- Array Of Links --}}
            @if (is_array($element))
                [@foreach](https://learnku.com/users/5651) ($element as $page => $url)
                    @if ($page == $paginator->currentPage())
                        <li class="active"><span>{{ $page }}</span></li>
                    @else
                        <li><a href="{{ $url }}">{{ $page }}</a></li>
                    @endif
                @endforeach
            @endif
        @endforeach

        {{-- Next Page Link --}}
        @if ($paginator->hasMorePages())
            <li><a href="{{ $paginator->nextPageUrl() }}" rel="next">>></a></li>
        @else
            <li class="disabled"><span>>></span></li>
        @endif
    </ul>
@endif

可以看到,分页效果的代码分为三部分:前一页、后一页、分页元素。

前一页

如果当前页是第一页的话,前一页 按钮需要置灰:

public function onFirstPage()
{
    return $this->currentPage() <= 1;
}

否则的话,就要为 前一页 按钮赋予链接:

public function previousPageUrl()
{
    if ($this->currentPage() > 1) {
        return $this->url($this->currentPage() - 1);
    }
}

public function url($page)
{
    if ($page <= 0) {
        $page = 1;
    }

    $parameters = [$this->pageName => $page];

    if (count($this->query) > 0) {
        $parameters = array_merge($this->query, $parameters);
    }

    return $this->path
                    .(Str::contains($this->path, '?') ? '&' : '?')
                    .http_build_query($parameters, '', '&')
                    .$this->buildFragment();
}

如果列表页中存在一些搜索条件,这些搜索条件会被加载到 $this->query 成员变量中,生成 url 的时候,这些搜索添加会被加到 request 的参数中。可以使用 append 方法附加查询参数到分页链接中:

public function appends($key, $value = null)
{
    if (is_array($key)) {
        return $this->appendArray($key);
    }

    return $this->addQuery($key, $value);
}

protected function appendArray(array $keys)
{
    foreach ($keys as $key => $value) {
        $this->addQuery($key, $value);
    }

    return $this;
}
下一页

与 前一页 类似,如果已经在最后一页,那么 下一页 按钮将会被置灰:

public function hasMorePages()
{
    return $this->currentPage() < $this->lastPage();
}

下一页的链接:

public function nextPageUrl()
{
    if ($this->lastPage() > $this->currentPage()) {
        return $this->url($this->currentPage() + 1);
    }
}

上一页 与 下一页 按钮的功能比较简单,至于中间的分页特效比较复杂,我们由下一节来说。

分页 elements

我们先说一下不同的分页样式:

当我们设置两侧页数为 3 时,当前数据总页数小于 8 页时分页效果:

总页数大于 6 页,且当前页在前 8 页(2 * 3 + 2)时分页效果:

当前页在前 6 页与后 6 页之间分页效果:

当前页在最后 6 页时分页效果:

分页效果样式的关键来源于 UrlWindow,这个类用于根据总页数与当前页的不同来控制不同的分页样式。

protected function elements()
{
    $window = UrlWindow::make($this);

    return array_filter([
        $window['first'],
        is_array($window['slider']) ? '...' : null,
        $window['slider'],
        is_array($window['last']) ? '...' : null,
        $window['last'],
    ]);
}

public static function make(PaginatorContract $paginator, $onEachSide = 3)
{
    return (new static($paginator))->get($onEachSide);
}

public function get($onEachSide = 3)
{
    if ($this->paginator->lastPage() < ($onEachSide * 2) + 6) {
        return $this->getSmallSlider();
    }

    return $this->getUrlSlider($onEachSide);
}
小型分页 getSmallSlider

如果当前总页数小于 ($onEachSide * 2) + 6 的话,就会调用小型分页效果,这种小型分页效果直接将所有页数全部显示:

protected function getSmallSlider()
{
    return [
        'first'  => $this->paginator->getUrlRange(1, $this->lastPage()),
        'slider' => null,
        'last'   => null,
    ];
}

public function getUrlRange($start, $end)
{
    return collect(range($start, $end))->mapWithKeys(function ($page) {
        return [$page => $this->url($page)];
    })->all();
}
CloseToBeginning 分页效果

当前页数位于前 ($onEachSide * 2) 页时:

protected function getUrlSlider($onEachSide)
{
    $window = $onEachSide * 2;

    if (! $this->hasPages()) {
        return ['first' => null, 'slider' => null, 'last' => null];
    }

    if ($this->currentPage() <= $window) {
        return $this->getSliderTooCloseToBeginning($window);
    }

    elseif ($this->currentPage() > ($this->lastPage() - $window)) {
        return $this->getSliderTooCloseToEnding($window);
    }

    return $this->getFullSlider($onEachSide);
}

protected function getSliderTooCloseToBeginning($window)
{
    return [
        'first' => $this->paginator->getUrlRange(1, $window + 2),
        'slider' => null,
        'last' => $this->getFinish(),
    ];
}

public function getFinish()
{
    return $this->paginator->getUrlRange(
        $this->lastPage() - 1,
        $this->lastPage()
    );
}

假设我们设置当前两侧页数为 3,当前页为 5,总页数 22,函数 getSliderTooCloseToBeginning 返回结果为:

return [
    'first' => [
        1 => '/www.example.com/example?page=1', 
        2 => '/www.example.com/example?page=2'
        3 => '/www.example.com/example?page=3'
        4 => '/www.example.com/example?page=4'
        5 => '/www.example.com/example?page=5'
        6 => '/www.example.com/example?page=6'
        7 => '/www.example.com/example?page=7'
        8 => '/www.example.com/example?page=8'],
    'slider' => null,
    'last' => [
        21 => '/www.example.com/example?page=21',
        22 => '/www.example.com/example?page=22'],
];

这个时候 element 函数返回数据:

protected function elements()
{
    $window = UrlWindow::make($this);

    return array_filter([
        $window['first'],
        is_array($window['slider']) ? '...' : null,
        $window['slider'],
        is_array($window['last']) ? '...' : null,
        $window['last'],
    ]);
}

//返回结果
[
    [
        1 => '/www.example.com/example?page=1', 
        2 => '/www.example.com/example?page=2',
        3 => '/www.example.com/example?page=3',
        4 => '/www.example.com/example?page=4',
        5 => '/www.example.com/example?page=5',
        6 => '/www.example.com/example?page=6',
        7 => '/www.example.com/example?page=7',
        8 => '/www.example.com/example?page=8',
    ],                                        //$window['first']...,                                    //is_array($window['last']) ? '...' : null
    [
        21 => '/www.example.com/example?page=21',
        22 => '/www.example.com/example?page=22',
    ],                                        //$window['last']
]
TooCloseToEnding 分页效果

当前页数位于后 ($onEachSide * 2) 页时:

protected function getSliderTooCloseToEnding($window)
{
    $last = $this->paginator->getUrlRange(
        $this->lastPage() - ($window + 2),
        $this->lastPage()
    );

    return [
        'first' => $this->getStart(),
        'slider' => null,
        'last' => $last,
    ];
}

public function getStart()
{
    return $this->paginator->getUrlRange(1, 2);
}

假设我们设置当前两侧页数为 3,当前页为 18,总页数 22,函数 getSliderTooCloseToEnding 返回结果为:

return [
    'first' => [
        1 => '/www.example.com/example?page=1', 
        2 => '/www.example.com/example?page=2'
    ],
    'slider' => null,
    'last' => [
        15 => '/www.example.com/example?page=15',
        16 => '/www.example.com/example?page=16',
        17 => '/www.example.com/example?page=17',
        18 => '/www.example.com/example?page=18',
        19 => '/www.example.com/example?page=19',
        20 => '/www.example.com/example?page=20',
        21 => '/www.example.com/example?page=21',
        22 => '/www.example.com/example?page=22',
    ],
];

这个时候 element 函数返回数据:

[
    [
        1 => '/www.example.com/example?page=1', 
        2 => '/www.example.com/example?page=2'
   ],
   '...',
   [
        15 => '/www.example.com/example?page=15',
        16 => '/www.example.com/example?page=16',
        17 => '/www.example.com/example?page=17',
        18 => '/www.example.com/example?page=18',
        19 => '/www.example.com/example?page=19',
        20 => '/www.example.com/example?page=20',
        21 => '/www.example.com/example?page=21',
        22 => '/www.example.com/example?page=22',
   ]
]
FullSlider 分页效果

当前页数位于中间时:

protected function getFullSlider($onEachSide)
{
    return [
        'first'  => $this->getStart(),
        'slider' => $this->getAdjacentUrlRange($onEachSide),
        'last'   => $this->getFinish(),
    ];
}

public function getAdjacentUrlRange($onEachSide)
{
    return $this->paginator->getUrlRange(
        $this->currentPage() - $onEachSide,
        $this->currentPage() + $onEachSide
    );
}

假设我们设置当前两侧页数为 3,当前页为 10,总页数 22,函数 getFullSlider 返回结果为:

return [
    'first' => [
        1 => '/www.example.com/example?page=1', 
        2 => '/www.example.com/example?page=2'
    ],
    'slider' => [
        7 => '/www.example.com/example?page=7', 
        8 => '/www.example.com/example?page=8', 
        9 => '/www.example.com/example?page=9',
        10 => '/www.example.com/example?page=10', 
        11 => '/www.example.com/example?page=11', 
        12 => '/www.example.com/example?page=12',
        13 => '/www.example.com/example?page=13', 
    ],
    'last' => [
        21 => '/www.example.com/example?page=21',
        22 => '/www.example.com/example?page=22',
    ],
];

这个时候 element 函数返回数据:

[
    [
        1 => '/www.example.com/example?page=1', 
        2 => '/www.example.com/example?page=2'
   ],
   '...',
   [
       7 => '/www.example.com/example?page=7', 
       8 => '/www.example.com/example?page=8', 
       9 => '/www.example.com/example?page=9',
       10 => '/www.example.com/example?page=10', 
       11 => '/www.example.com/example?page=11', 
       12 => '/www.example.com/example?page=12',
       13 => '/www.example.com/example?page=13',
   ],
   '...',
   [
        21 => '/www.example.com/example?page=21',
        22 => '/www.example.com/example?page=22',
   ]
]
 

simplePaginate 简单分页

简单分页相比以上的功能来说,精简了 elements 的特效:

public function simplePaginate($perPage = 15, $columns = ['*'], $pageName = 'page', $page = null)
{
    $page = $page ?: Paginator::resolveCurrentPage($pageName);

    $this->skip(($page - 1) * $perPage)->take($perPage + 1);

    return $this->simplePaginator($this->get($columns), $perPage, $page, [
        'path' => Paginator::resolveCurrentPath(),
        'pageName' => $pageName,
    ]);
}

protected function simplePaginator($items, $perPage, $currentPage, $options)
{
    return Container::getInstance()->makeWith(Paginator::class, compact(
        'items', 'perPage', 'currentPage', 'options'
    ));
}

分页服务的类不再使用 LengthAwarePaginator 类,而开始使用 Paginator,这两个类最大的不同在于

render 函数:
public static $defaultSimpleView = 'pagination::simple-default';

public function render($view = null, $data = [])
{
    return new HtmlString(
        static::viewFactory()->make($view ?: static::$defaultSimpleView, array_merge($data, [
            'paginator' => $this,
        ]))->render()
    );
}

render 函数调用的前端资源默认地址为 illuminate\Pagination\resources\views\simple-default.blade.php:

@if ($paginator->hasPages())
    <ul class="pagination">
        {{-- Previous Page Link --}}
        @if ($paginator->onFirstPage())
            <li class="disabled"><span>@lang('pagination.previous')</span></li>
        @else
            <li><a href="{{ $paginator->previousPageUrl() }}" rel="prev">@lang('pagination.previous')</a></li>
        @endif

        {{-- Next Page Link --}}
        @if ($paginator->hasMorePages())
            <li><a href="{{ $paginator->nextPageUrl() }}" rel="next">@lang('pagination.next')</a></li>
        @else
            <li class="disabled"><span>@lang('pagination.next')</span></li>
        @endif
    </ul>
@endif

可以看到,简单分页只有 上一页、下一页 两个按钮。

————————————————
原文作者:leoyang
转自链接:https://learnku.com/articles/6285/laravel-database-paginate-paging-service-source-code-analysis
版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请保留以上作者信息和原文链接。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Laravel 中,我们可以通过自定义 Paginator 类来实现自定义分页数据结构。 首先,我们需要创建一个新的 Paginator 类,可以在 `app` 目录下创建一个 `CustomPaginator.php` 文件。在这个类中,我们需要继承 Laravel 内置的 Paginator 类,并实现 `__construct` 和 `toArray` 方法。例如: ```php <?php namespace App; use Illuminate\Pagination\Paginator; class CustomPaginator extends Paginator { public function __construct($items, $perPage, $currentPage, $options = []) { parent::__construct($items, $perPage, $currentPage, $options); } public function toArray() { return [ 'data' => $this->items->toArray(), 'pagination' => [ 'total' => $this->total(), 'count' => $this->count(), 'per_page' => $this->perPage(), 'current_page' => $this->currentPage(), 'total_pages' => $this->lastPage() ] ]; } } ``` 在上面的代码中,我们在 `toArray` 方法中定义了自定义的分页数据结构,包括数据和分页信息。其中,`data` 表示当前页的数据,`pagination` 表示分页信息。你可以根据自己的需求修改这个数据结构。 然后,在使用自定义分页数据结构的时候,可以使用 `CustomPaginator` 类来实现。例如: ```php use App\CustomPaginator; $items = DB::table('users')->paginate(15); $customPaginator = new CustomPaginator($items->items(), $items->perPage(), $items->currentPage()); $result = $customPaginator->toArray(); ``` 在上面的代码中,我们先使用内置的 `paginate` 方法来获取分页数据,然后将其转换成自定义的分页数据结构。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值