Laravel предоставляет три метода пагинации: paginate() (с полной информацией о страницах), simplePaginate() (только prev/next) и cursorPaginate() (курсорная пагинация). Каждый метод подходит для определённых сценариев.
Типы пагинации
paginate() -- Полная пагинация
Выполняет два запроса: COUNT(*) для общего числа записей и SELECT с LIMIT/OFFSET для текущей страницы.
use App\Models\User;
// Basic pagination (15 items per page by default)
$users = User::paginate();
// Custom per page
$users = User::paginate(perPage: 25);
// With conditions
$users = User::where('active', true)
->orderBy('name')
->paginate(20);
// Specifying the page column name
$users = User::paginate(
perPage: 15,
pageName: 'users_page'
);
// With specific columns
$users = User::paginate(
perPage: 15,
columns: ['id', 'name', 'email']
);
simplePaginate() -- Упрощённая пагинация
Выполняет один запрос (без COUNT). Знает только о наличии следующей/предыдущей страницы.
use App\Models\Post;
// Simple pagination (no total count)
$posts = Post::simplePaginate(15);
// More efficient for large datasets
$posts = Post::where('published', true)
->orderBy('created_at', 'desc')
->simplePaginate(20);
cursorPaginate() -- Курсорная пагинация
Использует cursor-based подход (keyset pagination). Не использует OFFSET. Идеален для бесконечной прокрутки и больших таблиц.
use App\Models\User;
// Cursor-based pagination
$users = User::orderBy('id')->cursorPaginate(15);
// The cursor is based on the order column
$users = User::orderBy('created_at', 'desc')
->orderBy('id', 'desc')
->cursorPaginate(15);
Сравнение методов пагинации
| Характеристика | paginate() | simplePaginate() | cursorPaginate() |
|---|---|---|---|
| SQL-запросов | 2 (COUNT + SELECT) | 1 (SELECT) | 1 (SELECT) |
| Общее число страниц | Да | Нет | Нет |
| Номера страниц | Да | Нет | Нет |
| Прямой переход на страницу | Да | Нет | Нет |
| Навигация | Полная | Prev/Next | Prev/Next |
| Производительность | Средняя | Хорошая | Лучшая |
| Большие таблицы | Медленно (COUNT) | Хорошо | Отлично |
| URL-параметр | ?page=5 | ?page=5 | ?cursor=eyJpZ... |
| Стабильность при INSERT | Может дублировать | Может дублировать | Стабильная |
Использование с Query Builder
use Illuminate\Support\Facades\DB;
// Query Builder pagination
$users = DB::table('users')->paginate(15);
$users = DB::table('users')->simplePaginate(15);
$users = DB::table('users')->orderBy('id')->cursorPaginate(15);
Работа с результатами пагинации
Свойства и методы
$users = User::paginate(15);
// Paginator methods
$users->count(); // Number of items on current page
$users->currentPage(); // Current page number
$users->firstItem(); // Index of the first item
$users->getOptions(); // Paginator options
$users->getUrlRange(1, 5); // URLs for pages 1-5
$users->hasMorePages(); // Are there more pages?
$users->hasPages(); // Enough items for multiple pages?
$users->items(); // Items on the current page
$users->lastItem(); // Index of the last item
$users->lastPage(); // Last page number (paginate only)
$users->nextPageUrl(); // URL for the next page
$users->onFirstPage(); // Is on the first page?
$users->onLastPage(); // Is on the last page?
$users->perPage(); // Items per page
$users->previousPageUrl(); // URL for the previous page
$users->total(); // Total items (paginate only)
$users->url(5); // URL for a specific page
// Cursor paginator specific
$cursorUsers = User::orderBy('id')->cursorPaginate(15);
$cursorUsers->cursor(); // Current cursor
$cursorUsers->nextCursor(); // Cursor for next page
$cursorUsers->previousCursor(); // Cursor for previous page
$cursorUsers->getCursorForItem($user); // Cursor for a specific item
Итерация
$users = User::paginate(15);
// Iterate over items
foreach ($users as $user) {
echo $user->name;
}
// Append query string to pagination links
$users->appends(['sort' => 'name'])->links();
// Or append all current query string
$users->withQueryString()->links();
// Custom fragment
$users->fragment('users')->links();
Отображение в Blade
{{-- Display paginated items --}}
<div class="container">
@foreach ($users as $user)
<p>{{ $user->name }} - {{ $user->email }}</p>
@endforeach
</div>
{{-- Display pagination links --}}
{{ $users->links() }}
{{-- Using a specific view --}}
{{ $users->links('vendor.pagination.bootstrap-5') }}
{{-- Simple pagination links --}}
{{ $users->links('pagination::simple-default') }}
Кастомизация пагинации Blade
# Publish pagination views
php artisan vendor:publish --tag=laravel-pagination
# Views will be placed in resources/views/vendor/pagination/
Дефолтный вид пагинации
<?php
declare(strict_types=1);
namespace App\Providers;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\ServiceProvider;
final class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
// Use Bootstrap 5 for pagination
Paginator::useBootstrapFive();
// Or Bootstrap 4
Paginator::useBootstrapFour();
// Default: Tailwind CSS
Paginator::useTailwind();
// Custom default view
Paginator::defaultView('custom-pagination');
Paginator::defaultSimpleView('custom-simple-pagination');
}
}
API пагинация (JSON)
При возврате пагинатора из маршрута Laravel автоматически преобразует результат в JSON.
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Http\Resources\UserResource;
use App\Models\User;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
final class UserController extends Controller
{
public function index(): AnonymousResourceCollection
{
return UserResource::collection(
User::where('active', true)
->orderBy('name')
->paginate(15)
);
}
}
JSON-структура paginate()
{
"data": [
{"id": 1, "name": "John"},
{"id": 2, "name": "Jane"}
],
"links": {
"first": "http://example.com/api/users?page=1",
"last": "http://example.com/api/users?page=10",
"prev": null,
"next": "http://example.com/api/users?page=2"
},
"meta": {
"current_page": 1,
"from": 1,
"last_page": 10,
"links": [
{"url": null, "label": "« Previous", "active": false},
{"url": "http://example.com/api/users?page=1", "label": "1", "active": true},
{"url": "http://example.com/api/users?page=2", "label": "2", "active": false},
{"url": "http://example.com/api/users?page=2", "label": "Next »", "active": false}
],
"path": "http://example.com/api/users",
"per_page": 15,
"to": 15,
"total": 150
}
}
JSON-структура cursorPaginate()
{
"data": [
{"id": 1, "name": "John"},
{"id": 2, "name": "Jane"}
],
"path": "http://example.com/api/users",
"per_page": 15,
"next_cursor": "eyJpZCI6MTUsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0",
"next_page_url": "http://example.com/api/users?cursor=eyJpZCI6MTUsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0",
"prev_cursor": null,
"prev_page_url": null
}
Кастомная пагинация
Ручная пагинация
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\Collection;
// Create a paginator from a collection
$collection = collect([/* items */]);
$page = Paginator::resolveCurrentPage();
$perPage = 15;
$items = $collection->slice(($page - 1) * $perPage, $perPage)->values();
$paginator = new LengthAwarePaginator(
items: $items,
total: $collection->count(),
perPage: $perPage,
currentPage: $page,
options: [
'path' => Paginator::resolveCurrentPath(),
'pageName' => 'page',
]
);
Пагинация с трансформацией
$users = User::paginate(15);
// Transform items while keeping pagination
$users->through(function ($user) {
return [
'id' => $user->id,
'name' => $user->name,
'initials' => strtoupper(substr($user->name, 0, 2)),
];
});
// Or use getCollection() / setCollection()
$items = $users->getCollection()->map(function ($user) {
$user->formatted_name = strtoupper($user->name);
return $user;
});
$users->setCollection($items);
Пагинация в Eloquent Relationships
// Paginate a relationship
$user = User::find(1);
$posts = $user->posts()->paginate(10);
// With eager loading
$posts = $user->posts()
->with('comments')
->where('published', true)
->paginate(10);
Ограничения курсорной пагинации
// ✅ Works: ordered by single column
User::orderBy('id')->cursorPaginate(15);
// ✅ Works: ordered by multiple columns
User::orderBy('name')->orderBy('id')->cursorPaginate(15);
// ❌ Does NOT work: no ORDER BY
User::cursorPaginate(15); // Requires explicit orderBy
// ❌ Does NOT work: raw expressions in orderBy
User::orderByRaw('FIELD(status, "active", "pending")')
->cursorPaginate(15);
// ⚠️ Limited: cursor values must be serializable to string
Тестирование пагинации
<?php
declare(strict_types=1);
namespace Tests\Feature;
use App\Models\User;
use Tests\TestCase;
final class UserPaginationTest extends TestCase
{
public function test_users_are_paginated(): void
{
User::factory()->count(50)->create();
$response = $this->getJson('/api/users');
$response->assertOk()
->assertJsonCount(15, 'data')
->assertJsonPath('meta.current_page', 1)
->assertJsonPath('meta.last_page', 4)
->assertJsonPath('meta.per_page', 15)
->assertJsonPath('meta.total', 50)
->assertJsonStructure([
'data' => [['id', 'name', 'email']],
'links' => ['first', 'last', 'prev', 'next'],
'meta' => ['current_page', 'from', 'last_page', 'per_page', 'to', 'total'],
]);
}
public function test_cursor_pagination(): void
{
User::factory()->count(30)->create();
$response = $this->getJson('/api/users?per_page=10');
$response->assertOk()
->assertJsonCount(10, 'data')
->assertJsonStructure([
'data',
'next_cursor',
'next_page_url',
'per_page',
'prev_cursor',
'prev_page_url',
]);
// Follow next page
$nextUrl = $response->json('next_page_url');
$nextResponse = $this->getJson($nextUrl);
$nextResponse->assertOk()
->assertJsonCount(10, 'data');
}
public function test_second_page(): void
{
User::factory()->count(30)->create();
$response = $this->getJson('/api/users?page=2&per_page=10');
$response->assertOk()
->assertJsonPath('meta.current_page', 2)
->assertJsonCount(10, 'data');
}
}