内容简介:Laravel Database——Paginate 分页服务源码分析
本文 GitBook 地址: https://www.gitbook.com/book/leoyang90/laravel-source-analysis
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 rel="nofollow" target="_blank" href="{{ $paginator->previousPageUrl() }}" rel="prev"><<</a></li>
@endif
{{-- Pagination Elements --}}
[@foreach](https://laravel-china.org/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://laravel-china.org/users/5651) ($element as $page => $url)
@if ($page == $paginator->currentPage())
<li class="active"><span>{{ $page }}</span></li>
@else
<li><a rel="nofollow" target="_blank" href="{{ $url }}">{{ $page }}</a></li>
@endif
@endforeach
@endif
@endforeach
{{-- Next Page Link --}}
@if ($paginator->hasMorePages())
<li><a rel="nofollow" target="_blank" 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 rel="nofollow" target="_blank" href="{{ $paginator->previousPageUrl() }}" rel="prev">@lang('pagination.previous')</a></li>
@endif
{{-- Next Page Link --}}
@if ($paginator->hasMorePages())
<li><a rel="nofollow" target="_blank" href="{{ $paginator->nextPageUrl() }}" rel="next">@lang('pagination.next')</a></li>
@else
<li class="disabled"><span>@lang('pagination.next')</span></li>
@endif
</ul>
@endif
可以看到,简单分页只有 上一页
、下一页
两个按钮。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 以太坊源码分析(36)ethdb源码分析
- [源码分析] kubelet源码分析(一)之 NewKubeletCommand
- libmodbus源码分析(3)从机(服务端)功能源码分析
- [源码分析] nfs-client-provisioner源码分析
- [源码分析] kubelet源码分析(三)之 Pod的创建
- Spring事务源码分析专题(一)JdbcTemplate使用及源码分析
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
The Art of Computer Programming, Volumes 1-3 Boxed Set
Donald E. Knuth / Addison-Wesley Professional / 1998-10-15 / USD 199.99
This multivolume work is widely recognized as the definitive description of classical computer science. The first three volumes have for decades been an invaluable resource in programming theory and p......一起来看看 《The Art of Computer Programming, Volumes 1-3 Boxed Set》 这本书的介绍吧!