paginate 分页

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

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

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

分页服务的启动

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

  1. class PaginationServiceProvider extends ServiceProvider
  2. {
  3. public function register()
  4. {
  5. Paginator::viewFactoryResolver(function () {
  6. return $this->app['view'];
  7. });
  8. Paginator::currentPathResolver(function () {
  9. return $this->app['request']->url();
  10. });
  11. Paginator::currentPageResolver(function ($pageName = 'page') {
  12. $page = $this->app['request']->input($pageName);
  13. if (filter_var($page, FILTER_VALIDATE_INT) !== false && (int) $page >= 1) {
  14. return $page;
  15. }
  16. return 1;
  17. });
  18. }
  19. }

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

  • viewFactoryResolver 为 Paginator 设置了生成前端资源的类,用于获取分页前端代码。
  • currentPathResolver 为 Paginator 设置了 url 的地址。我们知道, 上一页下一页 等等都是可以执行翻页的操作,所以实际上这些按钮必然含有链接,而链接的地址就是当前请求的 url 地址,不同的按钮的链接地址只是 page 的参数不同而已。
  • currentPageResolver 为 Paginator 获取了当前的页数。
  1. public function boot()
  2. {
  3. $this->loadViewsFrom(__DIR__.'/resources/views', 'pagination');
  4. if ($this->app->runningInConsole()) {
  5. $this->publishes([
  6. __DIR__.'/resources/views' => $this->app->resourcePath('views/vendor/pagination'),
  7. ], 'laravel-pagination');
  8. }
  9. }
  10. protected function loadViewsFrom($path, $namespace)
  11. {
  12. if (is_dir($appPath = $this->app->resourcePath().'/views/vendor/'.$namespace)) {
  13. $this->app['view']->addNamespace($namespace, $appPath);
  14. }
  15. $this->app['view']->addNamespace($namespace, $path);
  16. }

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

分页服务 paginator

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

  1. public function paginate($perPage = 15, $columns = ['*'], $pageName = 'page', $page = null)
  2. {
  3. $page = $page ?: Paginator::resolveCurrentPage($pageName);
  4. $total = $this->getCountForPagination($columns);
  5. $results = $total
  6. ? $this->forPage($page, $perPage)->get($columns) : collect();
  7. return $this->paginator($results, $total, $perPage, $page, [
  8. 'path' => Paginator::resolveCurrentPath(),
  9. 'pageName' => $pageName,
  10. ]);
  11. }
  12. protected function paginator($items, $total, $perPage, $currentPage, $options)
  13. {
  14. return Container::getInstance()->makeWith(LengthAwarePaginator::class, compact(
  15. 'items', 'total', 'perPage', 'currentPage', 'options'
  16. ));
  17. }

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

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

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

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

获取当前页

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

  1. public static function resolveCurrentPage($pageName = 'page', $default = 1)
  2. {
  3. if (isset(static::$currentPageResolver)) {
  4. return call_user_func(static::$currentPageResolver, $pageName);
  5. }
  6. return $default;
  7. }

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

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

获取数据库总记录数

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

  1. public function getCountForPagination($columns = ['*'])
  2. {
  3. $results = $this->runPaginationCountQuery($columns);
  4. if (isset($this->groups)) {
  5. return count($results);
  6. } elseif (! isset($results[0])) {
  7. return 0;
  8. } elseif (is_object($results[0])) {
  9. return (int) $results[0]->aggregate;
  10. } else {
  11. return (int) array_change_key_case((array) $results[0])['aggregate'];
  12. }
  13. }
  14. protected function runPaginationCountQuery($columns = ['*'])
  15. {
  16. return $this->cloneWithout(['columns', 'orders', 'limit', 'offset'])
  17. ->cloneWithoutBindings(['select', 'order'])
  18. ->setAggregate('count', $this->withoutSelectAliases($columns))
  19. ->get()->all();
  20. }

获取当前页数据

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

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

初始化 LengthAwarePaginator

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

  1. protected function paginator($items, $total, $perPage, $currentPage, $options)
  2. {
  3. return Container::getInstance()->makeWith(LengthAwarePaginator::class, compact(
  4. 'items', 'total', 'perPage', 'currentPage', 'options'
  5. ));
  6. }

LengthAwarePaginator 的初始化:

  1. public function __construct($items, $total, $perPage, $currentPage = null, array $options = [])
  2. {
  3. foreach ($options as $key => $value) {
  4. $this->{$key} = $value;
  5. }
  6. $this->total = $total;
  7. $this->perPage = $perPage;
  8. $this->lastPage = max((int) ceil($total / $perPage), 1);
  9. $this->path = $this->path !== '/' ? rtrim($this->path, '/') : $this->path;
  10. $this->currentPage = $this->setCurrentPage($currentPage, $this->pageName);
  11. $this->items = $items instanceof Collection ? $items : Collection::make($items);
  12. }

分页资源 render

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

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

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

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

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

  1. @if ($paginator->hasPages())
  2. <ul class="pagination">
  3. {{-- Previous Page Link --}}
  4. @if ($paginator->onFirstPage())
  5. <li class="disabled"><span><<</span></li>
  6. @else
  7. <li><a href="{{ $paginator->previousPageUrl() }}" rel="prev"><<</a></li>
  8. @endif
  9. {{-- Pagination Elements --}}
  10. @foreach ($elements as $element)
  11. {{-- "Three Dots" Separator --}}
  12. @if (is_string($element))
  13. <li class="disabled"><span>{{ $element }}</span></li>
  14. @endif
  15. {{-- Array Of Links --}}
  16. @if (is_array($element))
  17. @foreach ($element as $page => $url)
  18. @if ($page == $paginator->currentPage())
  19. <li class="active"><span>{{ $page }}</span></li>
  20. @else
  21. <li><a href="{{ $url }}">{{ $page }}</a></li>
  22. @endif
  23. @endforeach
  24. @endif
  25. @endforeach
  26. {{-- Next Page Link --}}
  27. @if ($paginator->hasMorePages())
  28. <li><a href="{{ $paginator->nextPageUrl() }}" rel="next">>></a></li>
  29. @else
  30. <li class="disabled"><span>>></span></li>
  31. @endif
  32. </ul>
  33. @endif

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

前一页

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

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

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

  1. public function previousPageUrl()
  2. {
  3. if ($this->currentPage() > 1) {
  4. return $this->url($this->currentPage() - 1);
  5. }
  6. }
  7. public function url($page)
  8. {
  9. if ($page <= 0) {
  10. $page = 1;
  11. }
  12. $parameters = [$this->pageName => $page];
  13. if (count($this->query) > 0) {
  14. $parameters = array_merge($this->query, $parameters);
  15. }
  16. return $this->path
  17. .(Str::contains($this->path, '?') ? '&' : '?')
  18. .http_build_query($parameters, '', '&')
  19. .$this->buildFragment();
  20. }

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

  1. public function appends($key, $value = null)
  2. {
  3. if (is_array($key)) {
  4. return $this->appendArray($key);
  5. }
  6. return $this->addQuery($key, $value);
  7. }
  8. protected function appendArray(array $keys)
  9. {
  10. foreach ($keys as $key => $value) {
  11. $this->addQuery($key, $value);
  12. }
  13. return $this;
  14. }

下一页

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

  1. public function hasMorePages()
  2. {
  3. return $this->currentPage() < $this->lastPage();
  4. }

下一页的链接:

  1. public function nextPageUrl()
  2. {
  3. if ($this->lastPage() > $this->currentPage()) {
  4. return $this->url($this->currentPage() + 1);
  5. }
  6. }

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

分页 elements

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

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

Laravel Database——分页原理与源码分析 - 图1

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

img

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

img

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

img

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

  1. protected function elements()
  2. {
  3. $window = UrlWindow::make($this);
  4. return array_filter([
  5. $window['first'],
  6. is_array($window['slider']) ? '...' : null,
  7. $window['slider'],
  8. is_array($window['last']) ? '...' : null,
  9. $window['last'],
  10. ]);
  11. }
  12. public static function make(PaginatorContract $paginator, $onEachSide = 3)
  13. {
  14. return (new static($paginator))->get($onEachSide);
  15. }
  16. public function get($onEachSide = 3)
  17. {
  18. if ($this->paginator->lastPage() < ($onEachSide * 2) + 6) {
  19. return $this->getSmallSlider();
  20. }
  21. return $this->getUrlSlider($onEachSide);
  22. }

小型分页 getSmallSlider

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

  1. protected function getSmallSlider()
  2. {
  3. return [
  4. 'first' => $this->paginator->getUrlRange(1, $this->lastPage()),
  5. 'slider' => null,
  6. 'last' => null,
  7. ];
  8. }
  9. public function getUrlRange($start, $end)
  10. {
  11. return collect(range($start, $end))->mapWithKeys(function ($page) {
  12. return [$page => $this->url($page)];
  13. })->all();
  14. }

CloseToBeginning 分页效果

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

  1. protected function getUrlSlider($onEachSide)
  2. {
  3. $window = $onEachSide * 2;
  4. if (! $this->hasPages()) {
  5. return ['first' => null, 'slider' => null, 'last' => null];
  6. }
  7. if ($this->currentPage() <= $window) {
  8. return $this->getSliderTooCloseToBeginning($window);
  9. }
  10. elseif ($this->currentPage() > ($this->lastPage() - $window)) {
  11. return $this->getSliderTooCloseToEnding($window);
  12. }
  13. return $this->getFullSlider($onEachSide);
  14. }
  15. protected function getSliderTooCloseToBeginning($window)
  16. {
  17. return [
  18. 'first' => $this->paginator->getUrlRange(1, $window + 2),
  19. 'slider' => null,
  20. 'last' => $this->getFinish(),
  21. ];
  22. }
  23. public function getFinish()
  24. {
  25. return $this->paginator->getUrlRange(
  26. $this->lastPage() - 1,
  27. $this->lastPage()
  28. );
  29. }

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

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

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

  1. protected function elements()
  2. {
  3. $window = UrlWindow::make($this);
  4. return array_filter([
  5. $window['first'],
  6. is_array($window['slider']) ? '...' : null,
  7. $window['slider'],
  8. is_array($window['last']) ? '...' : null,
  9. $window['last'],
  10. ]);
  11. }
  12. //返回结果
  13. [
  14. [
  15. 1 => '/www.example.com/example?page=1',
  16. 2 => '/www.example.com/example?page=2',
  17. 3 => '/www.example.com/example?page=3',
  18. 4 => '/www.example.com/example?page=4',
  19. 5 => '/www.example.com/example?page=5',
  20. 6 => '/www.example.com/example?page=6',
  21. 7 => '/www.example.com/example?page=7',
  22. 8 => '/www.example.com/example?page=8',
  23. ], //$window['first']
  24. ‘...’, //is_array($window['last']) ? '...' : null
  25. [
  26. 21 => '/www.example.com/example?page=21',
  27. 22 => '/www.example.com/example?page=22',
  28. ], //$window['last']
  29. ]

TooCloseToEnding 分页效果

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

  1. protected function getSliderTooCloseToEnding($window)
  2. {
  3. $last = $this->paginator->getUrlRange(
  4. $this->lastPage() - ($window + 2),
  5. $this->lastPage()
  6. );
  7. return [
  8. 'first' => $this->getStart(),
  9. 'slider' => null,
  10. 'last' => $last,
  11. ];
  12. }
  13. public function getStart()
  14. {
  15. return $this->paginator->getUrlRange(1, 2);
  16. }

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

  1. return [
  2. 'first' => [
  3. 1 => '/www.example.com/example?page=1',
  4. 2 => '/www.example.com/example?page=2'
  5. ],
  6. 'slider' => null,
  7. 'last' => [
  8. 15 => '/www.example.com/example?page=15',
  9. 16 => '/www.example.com/example?page=16',
  10. 17 => '/www.example.com/example?page=17',
  11. 18 => '/www.example.com/example?page=18',
  12. 19 => '/www.example.com/example?page=19',
  13. 20 => '/www.example.com/example?page=20',
  14. 21 => '/www.example.com/example?page=21',
  15. 22 => '/www.example.com/example?page=22',
  16. ],
  17. ];

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

  1. [
  2. [
  3. 1 => '/www.example.com/example?page=1',
  4. 2 => '/www.example.com/example?page=2'
  5. ],
  6. '...',
  7. [
  8. 15 => '/www.example.com/example?page=15',
  9. 16 => '/www.example.com/example?page=16',
  10. 17 => '/www.example.com/example?page=17',
  11. 18 => '/www.example.com/example?page=18',
  12. 19 => '/www.example.com/example?page=19',
  13. 20 => '/www.example.com/example?page=20',
  14. 21 => '/www.example.com/example?page=21',
  15. 22 => '/www.example.com/example?page=22',
  16. ]
  17. ]

FullSlider 分页效果

当前页数位于中间时:

  1. protected function getFullSlider($onEachSide)
  2. {
  3. return [
  4. 'first' => $this->getStart(),
  5. 'slider' => $this->getAdjacentUrlRange($onEachSide),
  6. 'last' => $this->getFinish(),
  7. ];
  8. }
  9. public function getAdjacentUrlRange($onEachSide)
  10. {
  11. return $this->paginator->getUrlRange(
  12. $this->currentPage() - $onEachSide,
  13. $this->currentPage() + $onEachSide
  14. );
  15. }

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

  1. return [
  2. 'first' => [
  3. 1 => '/www.example.com/example?page=1',
  4. 2 => '/www.example.com/example?page=2'
  5. ],
  6. 'slider' => [
  7. 7 => '/www.example.com/example?page=7',
  8. 8 => '/www.example.com/example?page=8',
  9. 9 => '/www.example.com/example?page=9',
  10. 10 => '/www.example.com/example?page=10',
  11. 11 => '/www.example.com/example?page=11',
  12. 12 => '/www.example.com/example?page=12',
  13. 13 => '/www.example.com/example?page=13',
  14. ],
  15. 'last' => [
  16. 21 => '/www.example.com/example?page=21',
  17. 22 => '/www.example.com/example?page=22',
  18. ],
  19. ];

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

  1. [
  2. [
  3. 1 => '/www.example.com/example?page=1',
  4. 2 => '/www.example.com/example?page=2'
  5. ],
  6. '...',
  7. [
  8. 7 => '/www.example.com/example?page=7',
  9. 8 => '/www.example.com/example?page=8',
  10. 9 => '/www.example.com/example?page=9',
  11. 10 => '/www.example.com/example?page=10',
  12. 11 => '/www.example.com/example?page=11',
  13. 12 => '/www.example.com/example?page=12',
  14. 13 => '/www.example.com/example?page=13',
  15. ],
  16. '...',
  17. [
  18. 21 => '/www.example.com/example?page=21',
  19. 22 => '/www.example.com/example?page=22',
  20. ]
  21. ]

simplePaginate 简单分页

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

  1. public function simplePaginate($perPage = 15, $columns = ['*'], $pageName = 'page', $page = null)
  2. {
  3. $page = $page ?: Paginator::resolveCurrentPage($pageName);
  4. $this->skip(($page - 1) * $perPage)->take($perPage + 1);
  5. return $this->simplePaginator($this->get($columns), $perPage, $page, [
  6. 'path' => Paginator::resolveCurrentPath(),
  7. 'pageName' => $pageName,
  8. ]);
  9. }
  10. protected function simplePaginator($items, $perPage, $currentPage, $options)
  11. {
  12. return Container::getInstance()->makeWith(Paginator::class, compact(
  13. 'items', 'perPage', 'currentPage', 'options'
  14. ));
  15. }

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

  1. public static $defaultSimpleView = 'pagination::simple-default';
  2. public function render($view = null, $data = [])
  3. {
  4. return new HtmlString(
  5. static::viewFactory()->make($view ?: static::$defaultSimpleView, array_merge($data, [
  6. 'paginator' => $this,
  7. ]))->render()
  8. );
  9. }

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

  1. @if ($paginator->hasPages())
  2. <ul class="pagination">
  3. {{-- Previous Page Link --}}
  4. @if ($paginator->onFirstPage())
  5. <li class="disabled"><span>@lang('pagination.previous')</span></li>
  6. @else
  7. <li><a href="{{ $paginator->previousPageUrl() }}" rel="prev">@lang('pagination.previous')</a></li>
  8. @endif
  9. {{-- Next Page Link --}}
  10. @if ($paginator->hasMorePages())
  11. <li><a href="{{ $paginator->nextPageUrl() }}" rel="next">@lang('pagination.next')</a></li>
  12. @else
  13. <li class="disabled"><span>@lang('pagination.next')</span></li>
  14. @endif
  15. </ul>
  16. @endif

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